
Show HN: Lisp with copying GC in 537 lines of C (plus Lisp 1.5 on top) - krig
https://github.com/krig/LISP
======
blt
This is cool. I wrote a similar Lisp but got stuck debugging my copying GC.
The interaction between the Lisp environment chain and the C stack is tricky.

I prefer something like this over Norvig's Lisp interpreter written in Python.
Norvig's is good for understanding environments and eval/apply, but IMO it's
cheating to use the host language's GC for allocations.

I would add integers, even if it costs a few lines of code... using `itos` and
re-parsing the string when evaluating an int is just too painful for me.

~~~
slaymaker1907
To be fair to Norvig, malloc is also a relatively hefty abstraction comparable
to a GC on a systems level.

~~~
krig
That is certainly a good point. The problem I felt with dropping below malloc
is that the code becomes very platform-dependent.

------
FullyFunctional
Always good fun. Reminds me of XLISP which was my first exposure to Lisp.

There's a lot to be said for small implementations that gets to the heart of
the issues. Most great oeuvres of software had a kernel beginning like this.

Some fun directions you could take this: Change the GC to Baker-style
incremental (~ "real-time"), use cdr-coding, use tagged integers, add a
compiler and compile to byte-code or threaded code. EDIT: grammar.

~~~
krig
Thanks!

Yeah, I am thinking that adding a similarly minimal compiler to this would be
interesting as a next step.

There is a previous effort in the ”old-version” directory which had tagged
integers and strings and an attempt at macros, but it was a bit too unfocused
and I never finished it.

With this one the goal was to make it as small as possible but still be a
complete implementation including GC, which made for a clearer end goal.

~~~
FullyFunctional
Spot on. I have a partially finished byte-compiled Scheme which I abandoned
when it grew to big for its original target platform, sigh. So much to be said
for something that actually runs.

------
daef
ahahahaAHAHAhahaha...

    
    
        To prevent reading from continuing indefinitely, each packet should end with STOP followed by a large number of right parentheses. An unpaired right parenthesis will cause a read error and terminate reading.
    
        STOP )))))))))))))))))

------
waterhouse
Things I see...

1\. Mac OS apparently has a conflicting definition of "isnumber", as
"__DARWIN_CTYPE_TOP_inline int isnumber(int _c)" in
"/usr/include/ctype.h:323". Thanks, Mac OS. So I'm running this on another
machine of mine.

2\. My usual test: iteration expressed as anonymous recursion, which should be
possible to run in constant space:

    
    
      pi@raspberrypi:~/LISP $ echo '
      ((lambda (f) (f f 10 0))
       (lambda (f n tt)
         (cond ((equal? n 0) tt)
               (#t (f f (- n 1) (+ n tt))))))' | ./komplott
      55
      pi@raspberrypi:~/LISP $ echo '
      ((lambda (f) (f f 100 0))
       (lambda (f n tt)
         (cond ((equal? n 0) tt)
               (#t (f f (- n 1) (+ n tt))))))' | ./komplott
      5050
      pi@raspberrypi:~/LISP $ echo '
      ((lambda (f) (f f 1000 0))
       (lambda (f n tt)
         (cond ((equal? n 0) tt)
               (#t (f f (- n 1) (+ n tt))))))' | ./komplott
      Out of memory
      Aborted
    

I noticed from the source that eval did recursive calls to itself. I expected
that to be its bane, but I'm surprised it hit an OOM rather than a stack
overflow... It looks like, in lisp_eval's "apply" case, it'll call gc_protect
on all the relevant stuff, then only do gc_pop after everything returns;
perhaps that's why.

In the absence of other looping constructs, looping by tail recursion seems
the best available option, and therefore an important usage to support.
Implementing that, along with a moving GC, in a language that doesn't do its
own tail call elimination is a pain.

~~~
krig
Yeah the TCO is not complete, it allocates a call frame for each call
regardless which is why it ran out of memory. It doesn’t use up the C call
stack though, so you could in theory increase the GC heap size to make it
survive longer. Due to the primitive environment implementation it’ll get
slower and slower as it goes.

Thanks for the note on Mac OS, I’ll fix that.

------
kwccoin
Before I try, just always have a question - is it lisp-1 or lisp-2? Is it
scheme/lisp?

Given the lisp15.scm is a scm it seems to be underlying is a lisp 1.5 running
under scheme. But given it is doing make, is it just share code?

Too many of this but really want to have a code for simple C based lisp to
play with. Is that it?

~~~
hawkice
Considering the example code and test code is all (define func-name (lambda
(....) )), this has got to be a lisp-1.

------
rurban
I'm ok with minimalism, but interning strings in a simple array with linear
search only scales to ~100 entries. a hash table can be written in 15 lines.

~~~
shawn
This isn’t true. Emacs interns strings in a global obarray, which is a simple
array with linear search. I hear emacs scales well.

~~~
pg314
That is incorrect. An Emacs obarray is a kind of hash table [1].

[1]
[https://www.gnu.org/software/emacs/manual/html_node/elisp/Cr...](https://www.gnu.org/software/emacs/manual/html_node/elisp/Creating-
Symbols.html)

~~~
shawn
_(make-vector length 0)_

Interesting. That threw me off. I overlooked the previous paragraph:

 _In Emacs Lisp, an obarray is actually a vector. Each element of the vector
is a bucket; its value is either an interned symbol whose name hashes to that
bucket, or 0 if the bucket is empty. Each interned symbol has an internal link
(invisible to the user) to the next symbol in the bucket. Because these links
are invisible, there is no way to find all the symbols in an obarray except
using mapatoms (below). The order of symbols in a bucket is not significant._

Thanks!

------
brian_herman
Good job

~~~
krig
Thanks!

