DietLISP is an interpreter for an untyped Lisp variant written in Haskell. This is a toy project that I started working on during my winter break in the Doon valley.
DietLISP is a minimalist, lazy, purely functional lisp. It does not have a static type system. The distinguishing feature of DietLISP is that all executable forms in it are semi-operatives.
The notion of a semi-operative is similar to that of an operative in the Kernel programming language. Semi-operatives combine macros and functions into one unified concept but are more restrictive than fexprs.
I like to describe a semi-operative as a hook into the denotational semantics of the interpreter. You see, in a regular lisp, functions don't directly influence the interpreter and macros are hooks into the parser. Semi-operatives go a little deeper and we get a macro-function hybrid.
A semi-operative, like a macro, gets the bits of AST it has been invoked with, and (unlike a macro) the environment where it was invoked. It has access to functions that evaluate an AST in a specific environment (eval and eval*), and functions to extend the environment (add-binding). The result of the semi-operative invocation is the value (rather, the domain value) the interpreter gets by executing the semi-operative. I think it is useful to think of semi-operatives as hooks that arbitrarily map program phrases into domain values.
As an example of what this makes possible, look at samples/SimpleOperatives.dlisp:
;; The #builtin# directives return an semi-operative defined inside the interpreter in Haskell. global-bind binds them to the global lexical scope. (global-bind let (#builtin# #let#)) (global-bind eval (#builtin# #eval#)) (global-bind wormhole (operative () env (eval env x))) (let (x 42) (wormhole)) ;; Prints 42, since wormhole evaluates x in the environment inside let x (42)
eval (ast, env) evaluates ast in the environment env. eval* (ast, env) evaluates ast in the context of the current environment and then evaluates this result in the context of env (ast not evaluating to an AST is an error). This allows you to write functions this way:
(operative (number) env (let (evaluated-number (eval* env number)) (+ evaluated-number 5)))
In the above case, eval* first evaluates number in the current lexical scope (which gives us the AST passed to the semi-operative during evaluation) and then evaluates that ast in env (which gives us some result to which we then add 5).
Conditional evaluation makes writing macros easy. Here is a when macro:
(global-bind when (operative (condition action) env (if (eval* env condition) (eval* env action) 0)))
which evaluates to action if condition evaluates to true else evaluates to 0.
Two primitives useful when writing macros are unwrap-ast and wrap-to-ast, which convert an AST to a domain value and vice-versa; respectively. A list can't be directly evaluated but needs to be wrapped into an AST before evaluation. Similarly, an AST is impervious to direct introspection till it has been unwrapped into a regular domain value.
So, will this all work out? I don't know. I have a hunch that semi-operatives might lead to a cleaner and more orthogonal language; I'll keep updating this README as I explore and learn more.
As it is probably evident by now, this is more of a academic project. I found the following texts very helpful:
- Structure and Interpretation of Computer Programs (Hal Abelson, Jerry Sussman, Julie Sussman)
- Essential of Programming Languages (Daniel P. Friedman, Mitchell Wand, Christopher T. Haynes)
- Design Concepts in Programming Languages (Franklyn Turbak, David Gifford, Mark A. Sheldon)
I'd at recommend at least the first two texts to any serious software engineer.