I'd like to be able to have an environment where only symbols I specify are defined.
This is for an educational purpose where I want to define
E.g. an environment with only a nand(a, b)
function and then the user is expected to define a bunch of combinatorial logic from that.
Have you looked into baremodule
?
Yeah, but it still imports too much.
How is the user meant to interact with Julia?
It sounds like maybe you just should make a little DSL
The idea was to just get Julia semantics but with a clean slate that doesn't contain any of Core or Base.
Behind the scenes, they're still there, but the user can only interact with the names I give them or that they define themselves.
Maybe I can just build some machinery that tracks what symbols are in use and an eval that errors if you use any "undefined" symbol.
Again, how is the user meant to interact? repl? Some input box on a website?
Colin Caine said:
Maybe I can just build some machinery that tracks what symbols are in use and an eval that errors if you use any "undefined" symbol.
yeah, that's what I meant by a "little dsl"
As for input, files and a REPL.
What names are in a baremodule that you don't want? I think you might be able to get away with just using a baremodule and then having a blacklist for the remaining names, e.g. Core
and eval
I think I'd just want to expose nand(a, b)
, true
and false
. And all of the string and number literals would be absent.
Small languages are used a lot in some programming courses (e.g. Beginner's Student Language defined in racket), though I'm not sure one as basic as I'm suggesting would actually be helpful :)
Anyway, this doesn't seem super hard, maybe I'll just build something and see if it's easy and useful.
Okay, here's a demo with ReplMaker.jl that I think works:
using ReplMaker, MLStyle
baremodule Sandbox end
@eval Sandbox nand(x, y) = $(!)($(&)(x, y))
function sandbox_parser(s::String)
ex = Meta.parse(s)
filterer(ex) = @match ex begin
:Core => println("that's not allowed")
:eval => println("that's not allowed")
x::Bool => x
::Number => println("that's not allowed")
::String => println("that's not allowed")
::AbstractArray => println("that's not allowed")
x::Expr => Expr(x.head, filterer.(x.args)...)
x => x
end
:(@eval Sandbox $(filterer(ex)))
end
function valid_julia(s)
input = String(take!(copy(ReplMaker.LineEdit.buffer(s))))
ex = Meta.parse(input)
if ex isa Expr && ex.head == :incomplete
false
else
true
end
end
sandbox_mode = initrepl(sandbox_parser;
prompt_text="Sandbox> ",
prompt_color = :yellow,
startup_text = false,
mode_name = :sandbox,
valid_input_checker = valid_julia)
enter_mode!(sandbox_mode)
if you run that, you'll find yourself in the sandbox>
prompt and then you can run various stuff:
Sandbox> t = true; f = false;
Sandbox> nand(t,t)
false
Sandbox> nand(f,f)
true
Sandbox> not(x) = nand(x, true)
not (generic function with 1 method)
Sandbox> and(x, y) = not(nand(x, y))
and (generic function with 1 method)
Sandbox> and(t, t)
true
Sandbox> 1
that's not allowed
That's very neat. I was thinking I would have to do some name mangling as well as pattern matching
One potential issue is that they can always just hit the backspace button to exit the prompt and then do whatever they want
but I think you could delve into sandbox_mode.keymap_dict
and potentially remove that ability? I'm not sure.
Maybe safer would be to just make your own barebones repl that they can't escape from?
https://github.com/hanslub42/rlwrap will make it somewhat easy to make your own repl that supports things like history and such
Yeah, there's a few other things, like you can still import whatever modules you like, and there's some more tricky security things if I wanted to make a more secure sandbox.
But this is a very nice start, thank you :)
Mhm. You'd want to blacklist using
and import
for sure.
I wonder if there's a way to remove all the names from Core
, but still allow them to be defined, just not used.
I opened a discourse thread asking if anyone knows a way. https://discourse.julialang.org/t/even-more-bare-baremodule/56156
An alternative to a real module would be to repurpose some of the code from StaticModules.jl to make your own custom 'module' that does exactly what you want
I guess you'd also need to exclude Main
, as otherwise, I can call, e.g., Main.Base.eval
.
As a related note, I'm somewhat surprised that names(Sandbox; all = true, imported = true)
does not output anything.
Oh, it also looks like you can ccall
inside a baremodule
(since it's a syntax).
names(Sandbox)
does give output for me on Julia 1.5
Ah, I wasn't accurate. I meant to say an array with more than one element; e.g., I was hoping to see Array
etc.
At least a couple of months ago there were some quite big differences between the output of Meta.parse on 1.5 and 1.6 (with 1.6 giving lowered IR for some things). I wonder if that was ever cleared up.
@Takafumi Arakaki (tkf) ah, gotcha.
Expanded on @Mason Protter's example :)
using ReplMaker, MLStyle
baremodule Sandbox end
@eval Sandbox nand(x, y) = $(!)($(&)(x, y))
function eval_in_sandbox(ex)
forbidden_names = setdiff(names(Core; all=true, imported=true), names(Sandbox; all=true, imported=true))
filterer(ex) = @match ex begin
x::Bool => x
x::LineNumberNode => x
# Ban everything.
# Copy anything you want to allow to above this point.
# it's a bit complicated to work out when a symbol is being used as a
# reference and what scope it is in. I think the JuliaVariables people
# have maybe done something with that?
s::Symbol => s in forbidden_names ? throw("Sorry! You can't use that name in the sandbox") : s
# Not allowed to import stuff
Expr(:using, _...) ||
Expr(:import, _...) => error("Imports are not permitted in the sandbox")
# Julia literals, these are mostly or entirely harmless (I think)
# but you might want to omit them or rewrite them to something else or whatever.
::Bool ||
::Number ||
::String ||
Expr(:string, _...) || # string interpolation
::QuoteNode ||
Expr(:quote, _...) ||
Expr(:ref, _...) || # a[i], but also Int[]
Expr(:typed_vcat, _...) ||
Expr(:typed_hcat, _...) ||
Expr(:vect, _...) ||
Expr(:vcat, _...) ||
Expr(:hcat, _...) ||
Expr(:tuple, _...) || # also covers named tuples
Expr(:comprehension, _...) ||
Expr(:typed_comprehension, _...) ||
Expr(:generator, _...) => error("This literal is not permitted in the sandbox")
# TODO
# ccall (!!)
# Recurse
# Expr(head, args...) => Expr(head, filterer.(x.args)...)
x::Expr => Expr(x.head, filterer.(x.args)...)
# Fallback for other things
x => error("Unknown thing: $x")
end
:(@eval Sandbox $(filterer(ex)))
end
function sandbox_parser(s::String)
ex = Meta.parse(s)
eval_in_sandbox(ex)
end
function valid_julia(s)
input = String(take!(copy(ReplMaker.LineEdit.buffer(s))))
ex = Meta.parse(input)
!(ex isa Expr && ex.head == :incomplete)
end
sandbox_mode = initrepl(sandbox_parser;
prompt_text="Sandbox> ",
prompt_color = :yellow,
startup_text = false,
mode_name = :sandbox,
valid_input_checker = valid_julia)
enter_mode!(sandbox_mode)
I guess what I should actually do is just transparently rewrite symbols that match anything in Core. That would be quite simple.
For posterity: I posted a better version on discourse.
I made a little package for fun https://github.com/cmcaine/Sandboxes.jl
Last updated: Nov 06 2024 at 04:40 UTC