How to run the code snippets
Click into an editor and press command-enter or ctrl-enter to run it.
Everything runs in a USER environment, so snippets may need to be run in order.
You can edit the code, but
It's possible to mess everything up. If you do, refresh.
Things that are bad
Code isn't sanitized or sandboxed.
Printing and error messages are horrible.
It's a little lispy. 'noop' is a variadic function that just returns. This is useful for doing multiple things in sequence, because arguments are reduced strictly in order before being passed to operators/functions.
Quotes let us treat code as data and data as code. They're used a lot in this language. They're best thought of as objects that contain code and reduce to themselves, rather than a special form or a mechanism for delaying intepretation. Printing things can be uninformative, but the 'json' function sometimes helps.
@snv and @dnv: shorthand for @nv's static and dynamic parent environments (more on this later).
The interpreter itself is just a function in the language called "run". It takes two arguments, a quote expression and an environment, and returns the result of reducing the expression in that environment. So, (run '@run nv) == run, and (run '@nv nv) == nv, for any nv.
Closures are what you'd think of as lambdas, but take their static environment as an explicit argument. The arguent list and body must be quoted to prevent them from being interpreted. It's a mess but we'll fix it soon.
(Closure '(x y)
'(+ (* x x) (* y y))
((Closure '(x y)
'(+ (* x x) (* y y))
@nv) 3 4))
This language doesn't have a 'let' special form, or any other special forms, but we'll make one soon. We do get 'elet' though, which takes an explicit environment and an identifier to assign a value.
This is also a mess, and we'll fix it now by writing a simple 'let' function using hybrid environments. When a Closure is appied to arguments, a hybrid environment is made consisting of 3 parts.
d: a personal dict for this application to store values.
snv: the static environment that the Closure remembered, which was captured when the Closure was created.
dnv: the dynamic environment which is the one from which the Closure was called.
Now we can write 'let' by using @dnv to get and modify the environment from which the 'let' Closure is called.
Some Fun Stuff
It's a little complicated, but we only have to do it once, and now we can simply use 'let'. Now we can fix that Closure mess, by writing 'lam'. Let's make some other fun stuff while we're at it, including short-circuiting functions, swap, and named functions. Demos below.
"closure without explicit nv"
(let 'lam (Closure '(args body) '(Closure args body @dnv) @nv))
"easily reduce value inside a quote"
(let 'run (lam '(body) '(@run body @dnv)))
"like run but in new scope"
(let 'block (lam '(body) '((Closure '() body @dnv))))
"short-circuiting if from _if"
(let 'if (lam '(x y z) '(@run (_if x y z) @dnv)))
"swap the values of 2 identifiers"
(let 'swap (lam '(ida idb) '(noop
(let 'tmp (eget @dnv ida))
(elet @dnv ida (eget @dnv idb))
(elet @dnv idb tmp))))
(let 'function (lam '(name args body nv)
(let 'd (dict))
(let 'C (Closure args body (Env d nv)))
(dset d name C)
"function without explicit nv"
(let 'fun (lam '(name args body)
'(elet @dnv name (function name args body @dnv))))
"runs quote with new hybrid env"
(let 'sqr (lam '(x) '(* x x)))
"sqr in scope"
(println (sqr 6))
"sqr out of scope"
(println sqr) (println "")
"never prints 3"
(println (if (println 1)
(let 'a 4)
(let 'b 5)
(println (+ a (+ ", " b)))
(swap 'a 'b)
(println (+ a (+ ", " b)))
'(if (== x 0)
'(* x (fact (- x 1)))))
(println (fact 5))
Here's a place to experiment. 'last' is a variadic function like 'noop', but it returns its last argument. You may want to
My first attempt at giving applications access to both the static and dynamic environments was to pass both to the interpret function, along with the code to be interpreted. This doesn't work though, because it makes it impossible to get the dynamic parent of the dynamic parent of an environment. For example, if interp/@run took (code, snv, dnv), the short circuiting 'if' would look like:
(let 'if (lam '(x y z) '(@run (_if x y z) @dnv false)))
(if true '(if true '1 '2) '3)
The outer 'if' would want to run the inner 'if' in the dynamic environment from which it was called. However, it wouldn't have access to that environment's dynamic parent, which the inner 'if' needs.
This achieves the original goals of making a language...
with as few special forms as possible: There are none, unless you count quotes, but they're more like objects.
where as much as possible is first class: The current interpreter and environment are accessible and manipulable. Identifiers and expressions can be "used" by quoting them.
with a simple interpreter: The interpreter has only 4 cases: expressions, identifiers, hooks, and everything else which reduces to itself.
that is usable: For normal, less meta use, it's not bad at all, and only really gets tricky when writing "special forms". There's often an extra or missing quote. This is easier to debug in my version with better printing.
Hybrid environments are clearly neither safe, sane, nor fast. All code has access to all code. However, I think there is value in its lack of value. Starting with a fully powerful, small, horrible language gives perspective on what is necessary to make any language safer, saner, and faster. I'm particlarly interested in "safer". Is it possible to sandbox in such a chaotic language? One might have to perform static analysis dynamically. The language might need a full "understanding" of it's own semantics so it could reason about itself, along with the "assertive force" to trust itself.
Made by Alex Varga in 2016
Uses Ace for the editors