One of Deno's biggest misses was not figuring out https://github.com/denoland/deno/issues/378 in time. It's also the oldest still-open issue on the repo, which gives you a sense of how obvious this was and how long it was wanted for.
It's basically impossible to do this at the runtime level. I really want to see this work at the language level.
I propose some syntax variation on the following:
let third_party_module = @import("third_party_module", .{ permissions: .{ read: false } });
I think permissions are one of those things that really needs to get locked in before a ton of code gets written for wl.
I think there's definitely some usability quirks to work out. Here's just some tests we could run:
Milestone: 0.3.0.
note: considering swapping out side-effect stuff for the imported module
rough idea example:
const myLog = ()=>0
const myConsole = {...console, log: myLog}
import "thing.js" with { swap: { console: myConsole }}
@ paperdave's note:
both of these need to solve the problem of "what happens when two modules, specifically of different permission sets, import the same module"
@lino-levan I have an idea you'll probably find exciting, and I could use help thinking about it.
what happens when two modules, specifically of different permission sets, import the same module
There's three ways I can imagine for dealing with this^ problem, with only the last one being exciting (IMO).
I think the behivorial differences can be shown with this example:
a_lib.wl
//
// setter/getter for a
//
var a = ""
export fn getA() {
return a
}
export fn setA(val) {
@external("console").log("setting a_lib.a to be: "+val)
a = val
}
//
// setter/getter for b
//
var b = fn (stringArg) { return "" }
export fn getB() {
return b
}
export fn setB(val) {
@external("console").log("setting a_lib.b to be: "+val)
b = val
}
export fn callB(val) {
return b(val)
}
//
// main
//
var mainDidRun = false
export fn main() {
if (!mainDidRun) {
mainDidRun = true
setA("init")
setB(@external("fetch"))
}
}
location1.wl
const aLib1 = @import("a_lib.wl")
export fn getA() {
return aLib1.getA()
}
export fn callB(val) {
return aLib1.callB(val)
}
export fn main() {
aLib1.main()
aLib1.setA("loc1")
}
location2.wl
const aLib2 = @import("a_lib.wl")
export fn getA() {
return aLib2getA()
}
export fn main() {
aLib2.main()
aLib2.setA("loc2")
}
main.wl
const loc1 = @import("location1.wl") // full permissions
const loc2 = @import("location2.wl") // full permissions
const aLib3 = @import("a_lib.wl", .{ permissions: .{ fetch: false } }) // restricted
fn main() {
loc1.main()
loc2.main()
aLib3.main()
@extern("console").log("value equal?", loc1.getA() == loc2.getA())
@extern("console").log("addr equal?", @memAddress(loc1.getA()) == @memAddress(loc2.getA()) )
@extern("console").log("value equal?", loc2.getA() == aLib3.getA())
@extern("console").log("addr equal?", @memAddress(loc2.getA()) == @memAddress(aLib3.getA()) )
loc1.callB("https://httpbin.org/anything")
aLib3.callB("https://httpbin.org/anything")
}
aLib1, and aLib2 are the same object, but aLib3 is different.aLib1, aLib2, and aLib3 are all different. Each has their own aaLib1, aLib2, and aLib3 are all the same object, BUT have different attached contexts. Think of it like javascript .bind(). Replace @extern with this.@extern and then treat @import() as @import().bind({ custom thing })But you might already see the difficulty for permissions and Bound-imports:
loc1 (full permissions) runs a_lib's main firstloc1's @extern allows accessing fetch, so it sets b = @extern("fetch")aLib3 (restricted fetch permisisons) tries to run .callBits very unclear how permissions should work with lambda functions
actually, before this can be properly considered, we need to have a baseline for what the permissions model will actually look like.
read is an odd permission name, since this project is a compiler and not a runtime. actions such as reading a file are going to be implemented in terms of simpler primitives like @extern and @jsImport. if you have some arbitrary gate in std.fs to block or filter file reads, one could just create bindings themselves (worst case is getting a handle to globalThis and calling eval on it).
a solution to that may be to consider unsafe code (calling/casting an opaque value, etc) a special permission that is to be treated as the upmost dangerous. then, make the rest of the permissions userland, where the read permission is enforced by std.fs using a set of new built-in functions specifically for asserting permissions are set correctly.
some of this might also benefit from waiting until some standard library features like std.fs get implemented.
a solution to that may be to consider unsafe code (calling/casting an opaque value, etc) a special permission that is to be treated as the upmost dangerous. then, make the rest of the permissions userland, where the read permission is enforced by std.fs using a set of new built-in functions specifically for asserting permissions are set correctly.
Yep. I want to clarify here though that "userland" means, in practice, the standard library.
worst case is getting a handle to
globalThisand callingevalon it).
Which is kinda why I was considering a model that patches what globals are accessible. Ex: being unable to call @extern
I know it might seem like a meme for me to ask, but is there a particular reason @extern stuff isn't a static import? Like something that must be top-level known at compile time. Seems kinda hard to manage permissions on something as "loose" as @extern(userInput)
@extern(VALUE) must be known at compile time (dave correct me if I'm wrong). That stops a lot of problems by itself.
Okay that helps significantly
This has a lot of (possibly good) consequences for the permission model
This should help for thinking about "what happens if a module is imported twice with different bindings"
import absolutePath from "path_utils"
^this function actually depends on the host OS.
import posixAbsolutePath from "path_utils" override { extern: { OS: "unix" } }
import windowsAbsolutePath from "path_utils" override { extern: { OS: "win32" } }