TL;DR: It’s a scheme interpreter. It’s incomplete, it’s not particularly interesting. I wrote it as a learning experience.
GPLv3 or any later version
- Open programmbaukasten.html in your favorite standard-compliant web browser.
- Alternatively, you can run programmbaukasten.js in node and run `nodeREPL()`
- I wrote a hacky Scheme interpreter, which I had been planning to do for a long time. On the first day, I got much further than I expected.
- The interpreter handles basic data and operations. At the current time, progress still comes easy. For a list of limitations, see TODO-List at the end of the source code.
- After a few days of hacking, the interpreter supports
macros(see below) and tail recursion. I want to extend it to an R5RS-compliant version. Alternatively, I might limit myself to the subset of Scheme presented in SICP. - The interpreter was originally planned to be minimalistic. As such, it implemented even basic stuff like
div
,mod
or*
in Scheme. Recursively. I have changed that, although the original implementation can be activated by settingFEATURE_MINIMAL_MATH
totrue
in the js source. - I started programming by copying the ideas from Mary Rose Cook’s Little Lisp interpreter (https://www.youtube.com/watch?v=hqnTvuvXPCc). I quickly changed the ASTs from Arrays to my internal cons representation. The interpreter was initially written recursively. I changed eval and apply to use a trampoline function (see https://clojuredocs.org/clojure.core/trampoline) to enable tail call optimization. [WARNING: The current implementation is not proper TCO. I free the space on the js call stack, but I still keep all environments. I will have to write some code (in the if-block for the final statement in a function) that figures out if an environment won’t be needed later on and removes it from the environment chain. Nevertheless, the current implementation avoids “too much recursion”-errors for simple examples.]
- I wrote a “macro system” (if you can call it that), mostly to implement
if
in terms ofcond
. “Macros” in this system are like lambdas, except they don’t evaluate their arguments until they are used. The arguments will be evaluated in the dynamic environment, all other expressions will be evaluated in the lexical environments. In contrast to FEXPRs it is not possible to access the unevaluated arguments. If an argument is used twice in the macro’s body, it will be evaluated twice, mind the side effects! I don’t know which macro system I have reinvented, but it makes enough sense that I suppose I’m not the first one to have this idea. The next step towards real macros will be quasiquote. I want to support R5RS compatible macros, but I don’t want to slow down to understand those right now.
- Currently, the most complex program that the interpreter can successfully run is an incomplete scheme implementation of “CASTING SPELS IN LISP” (http://www.lisperati.com/casting.html). You can try it
by evaluating (load “examples/spels.scm”)(this was recently broken by new security rules in Firefox and Chrome; Instead you can copy the file’s contents into the REPL by hand). This example will be continuously updated to reflect the current state of my Scheme implementation. - For a list of all functions in the global environment, you can type “GLOBAL_ENV” (without the quotes) into your browser’s developer console
- The interpreter currently supports (all features are supposed to work as described in SICP unless stated otherwise):
- Data types
- Symbols (the allowed characters are specified by an ugly regexp):
atom?
- Numbers (positive and negative integers)
- Basic arithmetic:
+
,-
,div
,mod
,>
,>=
,<
,<=
,=
, - Additional functions:
gcd
,lcm
,abs
,even?
,odd
- Basic arithmetic:
- Pairs/Lists:
cons
,car
,cdr
,c***r
functions,list
,null?
- List functions:
drop
,list-tail
,list-ref
,length
,take
,append
,append-2
,reverse
,member
- association lists:
assoc
,
- List functions:
- Mutation:
set!
,set-car!
,set-cdr!
- Type predicates
type
,atom?
,boolean?
,pair?
,symbol?
,number?
,procedure?
- Symbols (the allowed characters are specified by an ugly regexp):
- Functions & Variables
- Anonymous functions with
lambda
- Scheme-style
define
(for both procedures and constants)- Variadic functions are supported by binding a list of remaining arguments to the parameter
&rest
if it is present in the parameter list.
- Variadic functions are supported by binding a list of remaining arguments to the parameter
- Tail recursion (mostly)
- Higher order functions:
map
,reduce
,filter
- Anonymous functions with
- Pseudo-Macros (incompatible with any lisp that I’m aware of): A “macro” is an expression of the form
(macro (args...) body ...)
. It works like lambda, except for the fact, that the arguments are put into the environment without evaluation. They are evaluated on each use (watch out for side effects!). Macros can be bound to identifiers using define. Arguments will be evaluated in the dynamic environment (i.e. like function parameters, just later), everything else will use the lexical environment like a normal function. - Control flow:
while
,begin
- Conditionals:
if
,and
,or
,not
,cond
- Streams as defined in SICP chapter 3. Work in progress. Set FEATURE_STREAMS in the js source to false to exclude this. (
cons-stream
,delay
,force
,stream-car
,stream-cdr
,the-empty-stream
,stream-null?
,stream-ref
,stream-map
,stream-for-each
,stream-enumerate-interval
,stream-filter
,integers-starting-from
,integers
,divisible?
,no-sevens
,sieve
,primes
) - The account example from SICP. Set FEATURE_ACCOUNT in the js source to false to exclude this. Check the source code for the implemented functions (Ctrl+F ACCOUNT).
quote
,quasi-quote
aka`expr
, andunquote
aka,expr
- System and host language interaction:
load
(currently broken for offline use),js-eval
,js-alert
- Data types
- Proper hygenic macros
call/cc
- Integers, Rationals
- Vectors
- …
- see ./TODO.org
- At its core, this is a very simple interpreter. Any user input is handled the following way:
- Read the input into a string
- Surround all parentheses with space characters
- Split the string at all space characters (this was inspired by https://maryrosecook.com/blog/post/little-lisp-interpreter , but Peter Norvig also used a similar algorithm in http://norvig.com/lispy.html)
- Take the resulting “token stream” and parse it into an AST. The nodes of this AST are javascript objects, e.g.:
{type: "number" value: 5}
{type: "symbol" value: "hello"}
{type: "cons" car: reference_to_car, cdr: reference_to_cdr}
- see the function
parseAST
for all possible types
- Pass the AST to the functions
lispeval
andlispapply
(both via the functiontrampoline
)trampoline
is called in a loop, which replaces tail-recursive calls from eval to apply and vice versa.trampoline
expects a single argument,packedArgs
.packedArgs[0]
is supposed to be one of “value”, “eval” or “apply”. “value” means thatpackedArgs[1]
can be returned as a result, “eval” and “apply” mean thatlispeval
orlispapply
should be called with the arguments inpackedArgs[1]
,packedArgs[2]
, …- According to this, eval and apply often return results of the form
["eval", someExpression, someEnvironment]
or["apply", aFunction, aParameterList, anEnvironment, aDebugName]
.
- Print the result
- Most things are implemented in a very naive way:
- Environments are javascript objects mapping symbols to values.
- Each environment contains a key “__parent” which links it to the surrounding scope.
- Lambdas contain a list of arguments, code and an environment linked to the lexical environment.
- The interpreter provides certain primitives, e.g. for arithmetic operations. To enable more introspection, the primitive versions are named
primitive-xyz
and are wrapped into corresponding scheme functions namedxyz
. Access to the primitive versions of these operations is limited.
- Minor implementation details (don’t rely on those):
- various interpreter features can be toggled on or off using the constants named
FEATURE_XYZ
at the beginning of the javascript code - the function
_test(expr, expected)
checks ifexpr
evaluates toexpected
(both should be provided as strings). the functiontestCases()
contains some example tests - some additional documentation is available as a long comment at the end of the interpreter’s source code
- various interpreter features can be toggled on or off using the constants named