Clo is a dynamically-typed programming language with garbage collection.
<comment> := "#" [^\n]* "\n"
<intrinsic> := "." [^\s]+
<binding> := <var> <expr>
<callee> := <intrinsic> | <expr>
<expr> := <int64_t-literal> | <ASCII-str-literal> | <var>
| lambda ( <var>* ) <expr>
| letrec ( <binding>* ) <expr>
| if <expr> <expr> <expr>
| { <expr>+ } // sequenced evaluation
| ( <callee> <expr>* )
| @ <var> <expr> // access var in closure's env (can simulate structs)
The distinguished feature of clo is serializing
the program state as a string.
The built-in function .forkstate returns
a string encoding the current state,
and when the state is resumed using .eval it starts
right after the .forkstate call but with a return value of Void type.
This resembles Linux's fork, Lisp's call/cc, etc.
For example, the following program outputs 0, 1, 2, 2, 3 in order.
Note: .eval works on both source code and serialized program state.
{
(.putstr "0\n")
(.putstr "1\n")
letrec (state (.forkstate)) {
(.putstr "2\n")
if (.= (.type state) 0) # if it is a void value
(.putstr "3\n")
(.eval state)
}
}
See test/ for more code examples (*.clo).
The source code of the interpreter
is standard C++20 and thus can be compiled
by any C++20-conforming compiler.
The current Makefile and CI.py
need clang++ with C++20 support (for sanitizer), make, and python3.
make -C src/ release
bin/clo <source-path>
python3 CI.py (re-)builds the interpreter and runs all tests.