Kilo LISP
t3x.org
t3x.org
The actual GC cycle code is pretty small, and looks like a classic mark-and-sweep over a fixed heap:
int gc(void) {
int i, k;
k = 0;
for (i=0; Root[i]; i++) mark(Root[i][0]);
Freelist = NIL;
for (i=0; i<NNODES; i++) {
if (0 == (Tag[i] & MARK)) {
setcdr(i, Freelist);
Freelist = i;
k = k+1;
}
else {
Tag[i] &= ~MARK;
}
}
/* ... */
return k;
}Cutting out the verbose GC stuff, there is a finite number of nodes and a free list. Exactly what one might expect. Even sorta feels like hardware.
The function mark is a classic algorithm, the Schorr-Waite algorithm [1], which traverses a directed graph and reverses the pointers, which modifies the heap graph as it traverses it to keep track of how much the graph has.
This is an interesting choice, and gave me pause because in a traditional large heap this would be a fantastically bad decision. But it's pretty interesting that the entire memory image of KiloLisp can exist in side the L2 cache of a raspberry pi and and in some circumstances half the memory can be hot and still reside in L1.
So actually, this is pretty clever. I suspect it's a good case of mechanical sympathy in recognizing that filling L2 will probably remove any gains that a "fancier" algorithm or a non-constant algorithm would bring. Very cool.
[1]: https://www.cs.cornell.edu/courses/cs312/2007fa/lectures/lec... is a good reference for this. The original article is annoying to get and hard to read.
Kilo lisp is just over 1000 lines of code, pretty impressive given what it does. It uses the parallel rails memory representation for pairs as taught in sicp. It even seems to be able to load and save "images"!
I'm not totally sure how the evaluator works. It uses these kinds of opcodes? enum { MHALT = 0, MEXPR, MLIST, MBETA, MRETN, MAPPL, MPRED, MNOTP, MSETQ, MPROG }; do they represent the control stack or something?
Yes, it does use images. (SUSPEND 'NAME) creates an image file and KL NAME at the shell prompt will load the image.
The evaluator is basically multiple functions in one, where the variable "m" ("mode") controls which function to apply next. The codes (MHALT, MEXPR, etc) denote the functions. MEXPR means "evaluate any expression", MLIST evaluates the next element of a list, MBETA starts function application, MRETN finishes it, etc. Yes, they implement states on the control stack (mstack).
This small kernal bootstraps itself with 'klsrc' file, and then it's able to provide 'cond', 'or', 'and' and other keywords for a conventional Lisp syntax.
I am eager to characterize the performance of this implementation. Tiny tends to mean fast but not always.
AST interpreter < bytecode interpreter < native code compiler
and it's an AST interrpeter so it's (likely to be) on the lower end of the speed scale.I now wonder whether this was the case with McCarthy's Lisps as well.
I guess that the Lisp described by Graham does not describe any mathematical operations, since they aren't really required to construct a metacircular evaluator, which is the selling point of the whole article. You don't need numbers for purely symbolic processing; if absolutely required, you can emulate them using Church encoding. Or you could try and make your Lisp dialect practically useful, and extend it with numeric types and operations.
[1] http://lib.store.yahoo.net/lib/paulgraham/jmc.ps via http://www.paulgraham.com/rootsoflisp.html
Try this in kilo LISP:
(load 'src//nmath)
(times '(1 2 3) '(4)) * (load 'src//nmath)
? undefined: null
You have to first load klsrc which defines objects required by src/nmath: * (load 'klsrc)
t
* (load 'src//nmath)
t
* (times '(1 2 3) '(4))
(4 9 2)Edit: If you cannot/will not use MAKE, just do
(load 'klsrc)
(suspend 'klisp)
After that you do not have to load klsrc any longer.A bit funky if you're coming from a Lisp that actually does have numbers that evaluate to themselves, but when it hits you it hits you :)
Note: LISP, not Lisp -- I refuse to get the memo! :)
# julia --lisp
; _
; |_ _ _ |_ _ | . _ _
; | (-||||_(_)|__|_)|_)
;-------------------|----------------------------------------------------------
> (apply cons '(1 2))
(1 . 2)
[1]: https://github.com/JeffBezanson/femtolisp[3]: https://github.com/JuliaLang/julia/blob/d76a30a7178dd1e9b744...
Another language that tried this with a syntax change was Dylan. People interested in that can try OpenDylan.
I've seen other proposals online, but they felt untested to me. Leave out either "|" or "$" and the above system gets clumsy. I check for the equivalent of "$" in other proposals exactly as I check for marrow in a stew recipe: Did they really think this through?
One can't change existing habits. An idea that appeals to me: Write a Lisp preprocessor for Rust, and happen to use this syntax.
S-expressions enable homo-iconic macros and make compile-time computing super easy! I spent 35 years programming in C and C-like languages and then developed my own Common Lisp implementation (http://github.com/clasp-developers/clasp.git) - the only Common Lisp that interoperates with C++ and uses llvm as the backend. When I started with lisp I wrestled with the syntax and sweet-expressions and just decided to let it go. Now I've found language love all over again!
In a world that runs on Javascript, nobody should be able to complain about a few measly parentheses.
Obligatory relevant XKCD comic: https://xkcd.com/297/
Just using make out of the box shows 44k so not sure where the 512k is coming from?
Using my "standard" little gcc flags:
-Os -s -Wall -Wl,-z,norelro
it comes out to 19k on gcc 8.2.1 on x86-64.I was curious how quickly I could change the source to lowercase symbols. It turns out that all Lisp symbols are already lowercase in the C source, and all Lisp source is lowercase. The only uppercase is in the documentation. A nod to us "thawed out of glaciers" elders who actually remember old Lisps?
Caps for keywords was the first use of syntax "highlighting". :)
But it's 25k of code, the executables and memory requirements are both greater than 1k.
Did it start off as 1k in size, and since expanded?
Fun name either way.
That being said, I like S4M's explanation, too!
Sloccount on kl.c gives 1056, so either lose 56 lines, or 32 lines and put up with pedants.
Edit: Making specialp() return a single line, excluding 1 branch of the #ifdef and all the #defines gets you below 1024. So maybe you could argue the case?