Show HN: Lisp with GC in 436 Bytes
justine.lol
justine.lol
https://news.ycombinator.com/item?id=26271117 Redbean (2011) is a single file distributable web server. Here's a video of me giving a talk about it https://youtu.be/1ZTRb-2DZGs
https://news.ycombinator.com/item?id=24256883 αcτµαlly pδrταblε εxεcµταblε (2020) lets you build c/c++/fortran/etc. code so that it runs on seven operating systems
https://news.ycombinator.com/item?id=13769727 Operation Rosehub (2017) where I helped save a lot of people from an Apache security bug similar to Log4j issue today
I like doing these projects because they all fit together so well into a consistent story. For example, a lot of these past projects were what helped to make SectorLISP better. Thanks to APE you now have a 20kb quickly deployable and embeddable LISP interpreter. Thanks to Redbean we're going to have an online pastebin service that'll let you publish SectorLISP gists in a few days, for example: https://lisp.pub/1
Thank you Hacker News community for being so supportive and encouraging.
> αcτµαlly pδrταblε εxεcµταblε
You mean ακτυαλλγ πορταβλε εχεκυταβλε?
but for the question: for your projects, do you have like a stable buildplatform (like nixos, minimal debian stable or sth like that) for which you publish your build instructions for your project?
I like Make since it's able to build a repository with 17k .o files, 80 .a archives, and 661 .com executables from scratch in under a minute on one personal computer (if the kernel page cache is warm). I wrote a couple small helper commands to make the make config more manageable, like package.com, mkdeps.com, compile.com, ar.com, zipobj.com, and runit.com.
The reason why Make works for me, is because I think the root cause of needing things like Autoconf and Cmake is because most projects need to depend on seven different C libraries. I decided that, rather than focusing on writing a better build config, I'd rather use an unfancy build system and instead devote my energy towards having a single C library that runs on all seven of the platforms I'm targeting. I owe a lot of thanks to projects like musl, dlmalloc, dtoa, llvm, etc. from whom I borrowed source code. That enabled me to abstract portability at the libc level, rather than punting to #ifdefs and configs.
> SectorLISP uses what we call an ABC garbage collector and it took only 40 bytes of assembly.
> [...]
> Fast immediate garbage collection with zero memory overhead and perfect heap defragmentation is as easy as ABC when your language guarantees data structures are acyclic.
Neat, but my understanding was that even pure functional languages can't generally make this guarantee, because things like letrec complicate matters. If SectorLISP can take this approach, why doesn't Haskell? (Or does it?)
But yes, GCs have evolved in the past decade, so these things are subject to change
The linked paper at the bottom of the article explains how things work and have worked for a long time now. The source of cyclic references, and even references from old to new generations, comes from "thunks" (unevaluated lazy computations). Haskell guarantees that a thunk only ever gets evaluated once, and the way this is implemented is that the thunk is mutated with a pointer to the evaluation result.
So the garbage collector needs to deal with mutations, cyclic references, and references from old to new generations. (All that in a language with supposedly immutable data!)
...the terms are immutable and thus there can be no pointers modified on the old heap to point to the young heap
so old objects cannot refer to young objects. This means it doesn't have to deal with write barriers, etc.
In languages with no mutability at all (not even internal mutability like with these two features), you can't create cycles and refcounting becomes possible, but I'm not aware of non-academic general purpose languages that actually fall in this category.
Esoteric functional languages like Unlambda do make that guarantee, and implementations can use a ref counter as their GC.
I've written more about this here: https://terbium.io/2019/09/cyclic-immutable/
Erlang?
This doesn't happen when you've got mutation or laziness.
One language that I'm aware which makes this tradeoff is Erlang.
I think the subtext is really a statement about constant factors. The parent is saying “if trees are wide then lookups are cheap” and I am saying “if trees are wide then updates may be expensive” (to update an immutable binary tree of size n, you must allocate log2(n) nodes of size (say) 3. To update a 32-ary tree you need log2(n) / log2(32) = log2(n) / 5 nodes of size 33 so, as 33/5 = 6.6 > 3, the wider tree requires more allocation for updates.)
(LETREC ((IS-ODD? (QUOTE (LAMBDA (X) (CAR X))))
(IS-EVEN? (QUOTE (LAMBDA (X) (NOT (IS-ODD X))))))
(IS-EVEN? (QUOTE (NIL T NIL T))))
The oldskool LISP way of writing that is: ((LAMBDA (IS-ODD? IS-EVEN?)
(IS-EVEN? (QUOTE (NIL T NIL T))))
(QUOTE (LAMBDA (X) (CAR X)))
(QUOTE (LAMBDA (X) (NOT (IS-ODD X)))))
As far as I can tell, letrec is just a nicer syntax for the same thing. So I don't know why it should require any mutability. It works probably because LISP is dynamically scoped. Most languages and modern LISP implementations prefer different scoping models to make the Assoc() equation faster. However that comes at a cost, since it can turn the difficulty of LISP up to 11. Next thing you know, you're discovering things like the Y combinator. So I have major respect for what languages like Haskell and Scheme are doing, because it's incredibly difficult. One of the reasons why I created SectorLISP is because I wanted to have a fun bastion of simplicity when I don't need the more mature tools.SectorLISP doesn't have any kind of internal mutability last time I checked. But I have experimented with it. For example, the Peel() optimization described in the blog post is how we solve the "make Assoc() faster problem". As it's written in the blog post, it's 100% pure. But if you introduce a tiny bit of mutability, it can go even faster on benchmarks even though it doesn't change the complexity class. See https://justine.lol/sectorlisp2/fast.js
I think you’re right about avoiding cycles. I think the parent was talking about a let rec construction for data, e.g. in Haskell:
let funky_list = 1:2:3:4:funky_list
(Note that : is the cons operator).If this is allowed then you may have issues. I think the problem is perhaps that the GC won’t work if you have pointers in the opposite of the normal direction. The fix though is relatively simple: add forwarding pointers (somehow), then after copying from the ‘B’ region to the ‘C’ region you leave behind a forwarding pointer and, if you see it again in your copying, you don’t make a duplicate copy of the data. Then I think you can proceed as before. But representing forwarding pointers efficiently is hard, I suspect.
Actually I think I don’t understand the GC sufficiently. Do you end up with a lot of memory usage after this or a little?
((LAMBDA (D Z) (D Z))
(QUOTE (LAMBDA (X)
(COND
(X ((LAMBDA (Y) (CONS Y Y)) (D (CDR X))))
((QUOTE T) (QUOTE T)))))
(QUOTE (A A A A A A A A)))
This should get you a binary tree about 8 levels deep with T on every leaf, and I think it should use linear memory (in the depth of the tree) but I don’t understand how the GC avoids ending up with an exponential amount of memory being used?As for your example, it uses exponential memory because your example produces exponential data. The only thing I'd need to show is that SectorLISP's ABC garbage collector doesn't grow at a large rate as the input size increases. According to the simulator's "heap" statistic, your example needs 449 doublewords of memory to compute a data structure that's 266 double words in size. That seems linear, but it's not, because if you add a few A's then the gap narrows.
The cool thing about your example is that even though it's generating exponential data, that data structure doesn't require an exponential amount of memory to be stored internally. Currently the SectorLISP simulator only does things like de-duplication for DEFINEs which the simulator reports as "code" size in doublewords. However that's a friendly branch feature and the rest of the time SectorLISP just does plain "what you see is what you get" evaluation. I'm sure much more advanced techniques have been discovered since McCarthy wrote his original paper.
Is there actually any Lisp dialect where letrec works for data? I'm not aware of such, at least where it comes to major ones (CL and common implementations of Scheme).
> Alternatively, make letrec wrap the values in thunks.
Aaaand of course I forgot that as well. My bad. So basically full lexical scoping and the presence of lambda should be enough to create cyclic structures, even in the absence of mutability.
(list-length '#1=(1 2 3 4 . #1#))
But I think that isn’t what you mean.You know more Haskell than I do ;-)
What I had in mind were recursive and mutually recursive functions, defined with letrec, as examples of reference-cycles in functional programming.
Of course dynamic scoping will bite you earlier or later because of the upward and downward FUNARG problems. See my book (http://t3x.org/lfn/) for lots of entertaining details.
Show HN: SectorLISP now fits in one sector - https://news.ycombinator.com/item?id=29047584 - Oct 2021 (75 comments)
Oh yeah. Who wouldn't splurge if you have a massive 40 bytes left. Beautiful.
Curious why that might be, and TFA doesn't expound. What makes it not a "true" language?
There should ideally be a more compelling explanation of why Brainfuck is an esoteric toy and LISP isn't, than me simply saying "but that's what people believe". I considered going into more depth on this subject. I found a C to Brainfuck compiler written in OCaml and used it to compile a Brainfuck metacircular evaluator into Brainfuck. It ended up being 88,439 bytes of code, and it sadly didn't work, because the compiler isn't very polished.
Let's compare ratios. Brainfuck is 99 bytes, whereas Brainfuck written in Brainfuck is 88439 bytes. That's 893x larger. On the other hand, LISP is 436 bytes of systems code and LISP written in LISP is 1370 bytes of LISP. That's 3.14x larger. It's cool that the ratio between layers of simulation with LISP is pretty close to pi. It helps explain why LISP is viewed by human beings as being such a powerful tool, since it lets you accomplish more impact with less effort, without having the castles you build turn into a leaning tower of pisa. Since after all, the purpose of tools since prometheus has been to give people more power.
[1] https://www.ioccc.org/2012/tromp/hint.html
Brainfuck isn't a great interpreter language. It is a great perspective language. If you can break your tasks down sufficiently that you can express your program in brainfuck, you can take that understanding back to a more performant and expressive language. Doing Rosetta code exercises in esoteric languages forces your brain to model programs with more nuance and detail. If raw machine level performance was the only thing that mattered, we'd be writing everything in assembly. Different languages can have different utility, though, and while brainfuck is less useful as software, it is an excellent tool, not just a toy.
You can't name functions in Brainfuck or recursively call them. You can do so in LISP and Forth. That is also the entire reason Brainfuck explodes in size: it lacks the "semiotic" capabilities required to compress source code (and therefore, structure and refactor code, which is what humans need).
The main reason is that Brainfuck lacks composition, so what happens in practice is that people GENERATE Brainfuck code rather than writing it. When you write code, you rely heavily on your programming language's mechanisms for composition in order to make something that works.
Someone pointed me to people who DO actually program by hand in Brainfuck. There are some pretty big programs that are hand-written, not generated. Still I'm not entirely convinced, as they seem to be have been constructed through mental feats, not compositional language.
It's a curious fact that human languages and programming languages compose in a way that can be leveraged to build very large things that "work". I guess there is some equivalence between building up a large body of knowledge through language and making a program that works. Brainfuck doesn't seem to have this property.
The demonstrations of arithmetic and calculus in the post are nice examples of this in Lisp, and from there you have the rest of the world as your disposal (which is well documented in many books). With Brainfuck you're left crawling on your hands and knees, occasionally finding something that works, so to speak.
> A powerful programming language is more than just a means for instructing a computer to perform tasks. The language also serves as a framework within which we organize our ideas about processes. Thus, when we describe a language, we should pay particular attention to the means that the language provides for combining simple ideas to form more complex ideas. Every powerful language has three mechanisms for accomplishing this:
> * primitive expressions, which represent the simplest entities the language is concerned with,
> * means of combination, by which compound elements are built from simpler ones, and
> * means of abstraction, by which compound elements can be named and manipulated as units.
Brainfuck is missing any sort of means of abstraction. The primitives are instructions, and the only means of combination is writing instructions in sequence, but there's no way in the language to refer to any sequence of instructions --- no functions, no subroutines, not even labels. This makes it utterly useless as a way to organize your thoughts on how to compute something.
[0] http://sarabander.github.io/sicp/html/1_002e1.xhtml#g_t1_002...
To create something that doesn’t fir this mould has to be more like a “recurse centre” project - a labour of love you have to finance yourself.
Sometimes. Sometimes not.
The physical HW, i.e. RAM and HDD storage is cheap but price you pay for accidental complexity is high. E.g. when your non-tail-call recursive calculation eats away all available memory. And you need to persistently store the intermediary results - just a handful of long integers in a dbase. And that dbase must be set up and maintained. That means dealing with access rights, usernames, passwords, replication, backups etc. In every environment: prod, tests, on every development branch (separately!), etc.
In another dimension, read / write operations mean yet more side effects to deal with. And on top of that, dealing with intermediary results means yet more accidental complexity in the source code.
So no, the workaround for "the memory limitation problem" can be in fact the most expensive part of a project. And now imagine, the data grows day by day...
And yeah it is indeed impressive and maybe the coolest thing I've seen this year.
Crazy, DNA uses this trick!
"-Os" should search for possibilities here..
I'm wondering if such code breaks any CPUs. I could imagine that it could confuse the code translation that modern x86s do. I mean I bet they don't test this particular corner case...
> These comments are fascinating in their own right. Like C comments they have a start marker, like /*, and a stop marker, like */. But they have some more structure. Remember that DNA is like a tape - the comments need to be snipped out physically! The start of a comment is almost always indicated by the letters ‘GT’, which thus corresponds to /*, the end is signaled by ‘AG’, which is then like */.
I don't know a whole lot about biology, but learning cool facts like that makes me wonder what would happen if those comment markers were used as parenthesis.
Even though I understand little of the OP, I understand more than last time, and have the hope to be able understand more in the future.
Thanks for the inspiration OP!
> (ncompile '(cons 1 (cons 2 3)))
$09CB:$7163: MOV AX,$04 ; S-object 1
$09CB:$7166: PUSH AX
$09CB:$7167: MOV AX,$05 ; S-object 2
$09CB:$716A: PUSH AX
$09CB:$716B: MOV AX,$06 ; S-object 3
$09CB:$716E: MOV BX,AX
$09CB:$7170: POP AX
$09CB:$7171: CALL $05AE ; cons
$09CB:$7174: MOV BX,AX
$09CB:$7176: POP AX
$09CB:$7177: CALL $05AE ; cons
$09CB:$717A: JMP $1DA7
(subru: eval=$7163, compile=$3B6F) Cons: xchg %sp,%cx # Cons(m:di,a:ax):ax
push %di
push %ax
xchg %sp,%cx
mov %cx,%di
xchg %di,%ax
ret
Apply: ...
xchg %cx,%sp
Pairlis:test %di,%di # Pairlis(X:di,Y:si,a:dx):ax
jz 1f # for x,y in zip(X,Y)
push (%bx,%di) # (- . y)
push (%bx,%si) # (x . y)
push %sp # ((x . y))
push %dx # ((x . y) . a)
mov %sp,%dx # a = ((x . y) . a)
mov (%si),%si
mov (%di),%di
jmp Pairlis
1: xchg %cx,%sp
...But this proved not to be a bad start at all. Once you understand the limitations of the "compiler", you can modify the macros accordingly. One of the feature of the compiler was that it assigned absolute memory places for variables, so you could stop wasting stack and do early assignments to temporary variables.
Unfortunately the source is quite incomprehensible now because of insane use of nested macros: https://github.com/timonoko/nokolisp
But the example given above works, no doubt about it:
> (setq test (ncompile '(cons 1 (cons 2 3))))
> (test)
(1 2 . 3)https://news.ycombinator.com/item?id=29047584
I would expect that to add a constraint that makes the code bigger, not smaller? Or I'm probably misunderstanding how the different variants of the interpreter work.
Once I cleaned up the C code, I noticed that the entire program didn't use pointers at all! (Except of course to interop with Bestline, but that could be replaced with fgetwc() instead). That's when the idea occurred to me that, since it didn't use pointers, it was also technically valid JavaScript too. So I asked around on Twitter to see if anyone's done a C / JS polyglot before. I got some helpful tips from a code golfer in Estonia who experimented with the idea and he told me about the paragraph separator trick. https://twitter.com/angealbertini/status/1463755612345540611 So I updated the C code to hybridize the two: https://justine.lol/sectorlisp2/lisp.js It's a shell script and a C program and it runs in the browser unaltered without needing to be compiled.
B might be an interesting choice of language for an uber-minimalist compiler, by the way.
I absolutely LOVE projects that immediately build on themselves. I wrote a GameBoy emulator from scratch and without any prerequisite knowledge and it felt amazing because of how incremental it is: you can break it into thousands of sensibly sized pieces.
I’m looking for the next project that’s like that. At the moment it’s an ECS video game.
I think work is what makes me seek projects with really good feedback loops.
edit: there is always mr brown to help us out : http://ctyme.com/rbrown.htm
Anyway, amazing work as always!
As for the file, I suspect that you're referring to JonesForth, which is a heavily commented implementation of the first variety:
https://github.com/nornagon/jonesforth/blob/master/jonesfort...
So, is it compatible with C51 8080?
The differentiation function sounds really nifty, too!
(The actual document is a .ps at the bottom of the page)
There must be some reason Lisp is so popular on HN.
Though not popular as an industrial language, ideas from lisp have spread into javascript and front end dev
Pathway Tools is a genetic database search/comparison tool (I think, it's a bit over my head) and powers the https://biocyc.org/ website.
Both are pretty typical use cases. Back-end services dealing with heavy symbolic manipulations. I think the most common use of Lisp is to implement Lisp though. ;)
sicp is a really good book, but also: https://marktarver.com/bipolar.html
http://www.paulgraham.com/lisp.html
This site is written in Arc, his first Lisp: https://news.ycombinator.com/item?id=28155134
- I use Common Lisp at home for personal projects when shell scripts no longer cut it. When the Python 3 storm hit my ~/bin, it occurred to me that one of CL's best features is that its specification has been stable for several years (even while the ecosystem has evolved considerably).
And by "several", you mean "at least fifteen at the time"? ;)
Are you saying you've never seen AutoCAD or Emacs in your life? While possible, I find it somewhat difficult to believe.
Halo 1 reference: https://andrew.gg/scripts/00reference.html
Decompiled campaign scripts: https://github.com/Nibre/blamscript/blob/master/blamscript/h...