| Kernel[1] has the ability to make scoping more abstract, but in a clean way which doesn't introduce the "spooky action at distance" of full dynamic scoping. It's an improvement on the older fexprs. Essentially, you have a lambda-like combiner called an operative which does not reduce its operands, and implicitly receives a reference to the caller's dynamic environment as an additional argument. The operative body can then optionally evaluate anything as if it were the caller, with even the ability to mutate the caller's local scope. The parent scopes of the caller are accessible for evaluation, but not mutation. You can only mutate an environment for which you have a direct reference - and the environments themselves are first-class values which you can pass around and store in other environments. It is only possible to mutate the parent environment of a caller if the caller's caller has passed a reference to its own environment, and the caller forwards that reference explicitly. You can create empty environments or environments from an initial set of bindings, and build on them from there, and you can also isolate the static environment from a callee, ensuring that only a limited set of bindings are accessible to the callee. In effect, this enables a kind of "sandboxing" approach - where you can easily do things like load an external plugin which can be written using a limited subset of Kernel features, and only access the specific functions of your host program which you allow. It provides a lot of abstractive power, with the ability to have things like types and classes defined as libraries. The operatives can introduce "sub-languages", which can simulate the behavior of other language semantics. As you suspect, this runs pretty slow because it's almost impossible to optimize ahead-of-time. A given piece of code is just a tree of symbols and self-evaluating atoms, and the symbols in some code are only given any functional meaning by the environment it is evaluated in. Kernel busts the myth that "compilation vs interpretation is just an implementation choice," and explores what can happen when we go all-in on interpretation. The author has written about the nature of interpreted programming languages and how this differs from compiled languages.[2] There are opportunities to have compilation and performance without much loss of abstractive power, but this is not a focus of Kernel - whose design goals are described at detail in the report. I've done a lot of R&D on making performance acceptable for a Kernel-like language. The main insight is that you should be able to make some assumptions about the environment passed to an operative without knowing anything else about the environment. I do this by modelling environments as row polymorphic types, where the operative specifies a set of bindings it expects the caller's dynamic environment to contain, complete with type signatures, and assumes these bindings behave in some specific way. (Eg, that the binding `+` means addition, which is in no way guaranteed by Kernel, and that the type signature of `+` is `Number, Number -> Number`). --- A mostly complete implementation of Kernel: https://github.com/dbohdan/klisp (Cloned from Andres Navarro's bitbucket repo which is no longer available). Some performance improvements of klisp, with a lot of hand-written 32-bit x86 assembly: https://github.com/ghosthamlet/bronze-age-lisp (Cloned from Oto Havle's bitbucket repo, also no longer available). Another implementation of Kernel, with some examples of defining records, classes, objects, generators etc as libraries: https://github.com/vito/hummus/tree/master --- [1]:http://web.cs.wpi.edu/%7Ejshutt/kernel.html [2]:https://fexpr.blogspot.com/2016/08/interpreted-programming-l... |