Show HN: SectorLISP now fits in one sector
justine.lol
justine.lol
I once reached out to them personally to tell them I hope I can be like them when I grow up ;^)
On a serious note, this is one of the coolest things I've ever seen.
The person mentioned that helped them go from 700 -> 500 bytes, the said "Assembly Heisenberg" appears to be entirely obscure on GH too.
Seems like the right set of eyes just happened to find their way to the project at the right time:
I wish I was born like this.
However, I don't see "I wish I was born like this" -- their works encourages me to think and do lots of crazy shit. My previous limitations were just a lack of imagination and creativity.
SectorLISP also encourages the "do anything" viewpoint. The original version was published even though way larger than a sector. Other people contributed, sometimes only by publishing a related project (SectorFORTH). By having an idea and publishing something Justine created something unique.
Lastly the above page shows the "run program and watch its memory in realtime" technique, which is incredible in itself. The tool is called Blinkenlights https://justine.lol/blinkenlights/
I feel like this just distills the ethos of LISP so perfectly, I'm gonna use this to explain it to people now.
Throw yourself at the computer, and the computer will do interesting things with you, too.
If it's not working, go at it from a different angle. Sometimes it's helpful to go back in time to come across things. I'd argue that 90% of cool tech phenomenon arises from studying the past to make something cooler in the future rather than any force of sheer will.
A lot of Justine's projects seem to fit this mold (if I remember right she came across the idea for Cosmopolitan after finding that a legacy UNIX feature let you avoid specifying your shell; SectorLISP, McCarthy's metacircular, etcetera).
She even cites someone whose entire deal is doing precisely that: Nils M. Holm.
Yeah, I remember her posting about the original Unix source code. I've never read it but I probably should. I bet there's plenty to be learned.
Sometimes I come across papers from the 70s and 80s too and they blow me away. It's amazing how much our predecessors achieved. Sometimes it feels like we're trying to rediscover or reinvent old technology.
If it were hidden and secret, maybe people would take it more seriously.
(defun null (x) (eq x '()))
Replacing the defun with an env binding, I believe the value translates to: (lambda (x) (eq x '()))
I don't think this should be quoted? Even though lambda isn't typically assumed in the "seven primitives," it _is_ required by the runtime. Are your function calls implicitly dropping quotes when evaluating arguments? ((lambda (x)
(message "%S" x))
(1))
;; fails: no such function `1'
((lambda (x)
(message "%S" x))
(quote (1)))
;; prints (1)
The original evaluator is defined so that if the first item of a list, is a list whose first item is LAMBDA, then what it does is it calls Evlis() and Pairlis() which is sort of like a glorified version of Python's zip() function, to attach all the subsequent list items to the parameter name list. ((LAMBDA (ARG1 ARG2 ARG3)
<YOUR PROGRAM GOES HERE>)
(QUOTE VAL1)
(QUOTE VAL2)
(QUOTE VAL3))
This is the "venus fly trap" that the blog post mentions. The process of evaluation causes the expression the clamp in on itself where the args get digested into an association list which can be referenced and then the payload is evaluated. This was all non-obvious to me, even after using LISP casually for years, since we normally don't write code that way.Now it's possible to add extra code to the evaluator so that if a naked lambda is encountered, i.e. it sees (LAMBDA ...) rather than ((LAMBDA ...) ...), it just lets it pass through, so you don't need the quotes. We did that originally, but deemed it non-essential and was removed, since that would have made it much harder to hit the 512-byte goal.
((lambda (f)
(f))
(lambda() 42))
You might need to throw a 'funcall in there or something, but my hypothesis is that this will work (return 42) without needing the top-level argument to be quoted. 'lambda is a deep special case, basically.I understand that functions always zip through their arguments, evaluating each one. But when you quote the args in the call, I tend to expect the arguments to not be evaluated within 'evlis.
I'm surprised that quoting args makes the interpreter smaller! That's pretty interesting. But I think this interpreter will have some behavior that is surprising to anyone used to all extant lisps for examples like this:
((lambda (sym)
(cons sym 3))
(quote a))
Here there's no binding for 'a. I expect this to return (a . 3) in JMC's original Lisp.In a more fully-featured Lisp, the code (LAMBDA () 42) would evaluate to a special function data type with no direct representation in terms of cons cells. That’s a luxury this implementation can’t afford, so its data representation of a function is the same as its code representation: a cons cell whose car is the atom LAMBDA. With this design, the code (LAMBDA () 42) would be self-evaluating: it would evaluate to the same thing as the code (QUOTE (LAMBDA () 42)). But since the latter syntax works, there’s no need to waste bytes supporting the former syntax.
In the absence of that special support, the default meaning of the code (LAMBDA () 42) is to call a function named LAMBDA, just like the code (FOO () 42) calls a function named FOO.
((LAMBDA (ARG1 ARG2 ARG3)
<YOUR PROGRAM GOES HERE>)
(QUOTE VAL1)
(QUOTE VAL2)
(QUOTE VAL3))
Is the same syntax as a normal function call: (PROGRAM
(QUOTE VAL1)
(QUOTE VAL2)
(QUOTE VAL3))
The only difference is that, since it's the root-level program, the binding of the name PROGRAM can't have happened yet. So it's simply inlined into the expression. The Apply() part of the evaluator has a special case for recognizing this, which causes evaluation of the first argument to terminate: int Apply(int fn, int x, int a) {
int t1, si, ax;
if (ISATOM(fn)) {
switch (fn) {
case ATOM_CAR:
return Car(Car(x));
case ATOM_CDR:
return Cdr(Car(x));
case ATOM_ATOM:
return ISATOM(Car(x)) ? ATOM_T : NIL;
case ATOM_CONS:
return Cons(Car(x), Car(Cdr(x)));
case ATOM_EQ:
return Car(x) == Car(Cdr(x)) ? ATOM_T : NIL;
default:
// recurse to turn (FUNC ARG1 ARG2) into ((LAMBDA ...) ARG1 ARG2)
return Apply(Eval(fn, a), x, a);
}
}
// evaluate ((LAMBDA ...) ARG1 ARG2)
if (Car(fn) == ATOM_LAMBDA) {
t1 = Cdr(fn);
si = Pairlis(Car(t1), x, a);
ax = Car(Cdr(t1));
return Eval(ax, si);
}
return UNDEFINED;
}
The Eval() function which calls Apply() has already evaluated all the list items except for the first one, which is left to Apply() to figure out. The reason why things are this way is because, in order to save space, the REPL loop doesn't maintain a persistent set of global variables. Due to the NIL here, each expression is its own hermetic job: void Repl(void) {
for (;;) {
Print(Eval(Read(), NIL));
}
}
If you changed that NIL to be an alist that's populated by things like defun / setq / etc. in the global scope, then the usability of the language improves dramatically. We didn't do it due to the singular focus on size. But at the same time that simply means we left fun opportunities for improvement to anyone wishing to hack on the codebase and make it their own.I suspect mostly because the implementation stumbled into the dynamic binding/lexical binding/FEXPR traps that hit all the early Lisps.
> Here it becomes clear that, in its most bare essential form, beneath the macros and abstractions, LISP actually has an unpleasant nature where name bindings (or assignments) look like a venus fly trap.
Ayup.
A lot of these problems were addressed by the late John Shutt in his thesis about vau calculus in the Kernel programming language:
FEXPRS: https://en.wikipedia.org/wiki/Fexpr
John Shutt's vau calculus thesis: http://www.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unre...
John Shutt's Kernel Language--"Revised-1 Report on the Kernel Programming Language" https://core.ac.uk/download/pdf/47187352.pdf
The "magic" is that the "$vau" operative closes over the environment when it is defined so that it can execute later in that environment--this is the "static/lexical environment". However, when you actually execute the "$vau" operative, you also pass in the "execution/dynamic" environment so that the code that the "$vau" operative runs can choose to do what it wants with the arguments--do nothing, evaluate them in the lexical environment, or evaluate them in the dynamic environment.
It's a powerful concept, but difficult to compile so got kind of relegated from the Lisp/Scheme languages.
For something which seems to implement it properly, see the "Oh" UNIX shell:
https://github.com/michaelmacinnis/oh
https://www.youtube.com/watch?v=v1m-WEZz46U
I would mumble that the fact that "list" is defined as "cons pairs with a final nil()" is also a pain in the ass. I understand why it was originally done this way, but it makes the idea of a "sequence" abstraction so very much more painful. It's one of the reasons why Clojure dropped it.
Thanks for showing me Oh! It really has f-exprs?! I didn't immediately see it in https://github.com/michaelmacinnis/oh/blob/main/doc/manual.m...
It's such a shame, but "Kernel" and "Oh" sort of drives home the fact that good ideas also need good marketing to propagate. "Oh!" is also a poster child for "videos suck for searchability and discoverability--post a transcript and your slides."
It's particularly bad with "Oh" because he has some truly nifty hacks. The pipeline that is reconfiguring itself as a filter while it's filtering is stunning. The "Transmit the code in one place and execute it in another" is also clever.
The thing that dragged me to Kernel was that I needed a small language to be able to debug embedded systems on the fly. So, the interpreter couldn't be hamstrung and I never had a compile step available. I needed both small code and small data.
Kernel, the idea, fits the bill. Kernel, the implementations, for some reason all really obscure the point for reasons I'm not particularly clear on.
Maybe once I've got it all clear in my head, I'll try to do an implementation in Rust. That should help keep things clean since you can't rely on "metacircular" tricks to implement things.
But for commenters, there are lots of interesting people.
I realize that something had to go in order to fit it in the size limit, compatibility with 8088 CPUs isn't the worst choice.
[Edit] Sorry if this sounded too negative... I'm astounded that it's even possible to do this in a sector at all.
The opcodes that were not documented were either redundant, doing exactly the same thing as other documented opcodes, or they crashed the system.
Later, in 1982, 80186 and 80286 introduced invalid opcode exceptions, so starting with the IBM PC/AT any program intended for later CPUs should abort with an error message.
Most IBM PC/XT were made with Intel 8088, so the behavior of an 80386 program is unpredictable.
Some PC/XT clones were made with 80186 or 80188, or, more frequently, with NEC V20 processors. All these had invalid opcode exceptions, so they should behave like a PC/AT.
I wonder how high you can go with only 1MB say.
ps: ohh that paper was on the frontpage not long ago, I just didn't realize it was used in Roombas
all in all, I'd really love to have or make a 1MB minimalistic shell with a tiny lisp/prolog/smalltalk
C, C++, Modula-2, Pascal, AMOS, Clipper, FoxPro, Turbo Basic, Quick Basic, Turbo Prolog, PC-Lisp, Native Oberon,...
Keep in mind that IBM PC/XT had only 640 kB, but there were compilers and interpreters for any language, which were available for it.
Moreover, before IBM PC, a CP/M computer with Zilog Z80 or Intel 8080 had usually only 32 kB or 48 kB, but you could use without problems Basic interpreters, Pascal, Fortran, Cobol and PL/M compilers and many others.
However, in order to fit in 32 kB, the compilers themselves were typically written using a macro-assembler, and not in a high-level language. The C language became popular for such tasks somewhat later.
And that's the maximum (without bank switching schemes like EMS). The first version had only 64 KB. There were even plans for a 16 KB version with no disk drive but I don't think it was ever released.
And then there's FORTH, which is tiny even in a naive implementation, but can be taken to extremes as well: https://pygmy.utoh.org/3ins4th.html
As the tests hint, Mu doesn't do anything to try to reduce code size. It's small just by focusing on the essentials. What's left is written for ease of comprehension.
(An example of "essentials": Mu still cannot free memory. I run my programs in Qemu with 2GB which seems to suffice.)
More details: https://github.com/akkartik/mu
To answer your question, evaluate.mu does support lambda (I call it `fn`). My estimates of size were based on `ls -l a.bin` after `./translate shell/*.mu`.
I actually didn't really think of the micro interpretation of 'mu' until years after I started the project. The interpretation I had in mind was https://en.wikipedia.org/wiki/Mu_(negative)#%22Unasking%22_t...
(I haven't done this so far because I find metacircularity to not be very interesting. It was interesting when JMC proved it could be done. Mu's whole reason for existence is linear rather than circular bootstrapping. We can disagree over whether I get to call it a Lisp or not, but if it has the full power of macros I'm happy.)
I don't know if I'm an alien but whenever I see frugal yet non trivial application my brain rejoyces.
The funny thing is that even so many years later the original software is still in use in some places and there is a whole company centered around that core that has been re-written a couple of times to keep it up to date and to expand its functionality.
I highly doubt the present day version would fit in something that small. What's interesting to me is that that old stuff tended to be super productive to work with, zero distractions, just some clearly defined task in a clearly defined environment, if it worked it was bullet proof. No hackers, SaaS, a million connectivity options and no eye candy. Just that one job to be done and done as good as the hardware would allow you to.
The sad part to me is that most web apps reenact the same functions but in a css-transition-capable DOM. But functionally I'm not sure you get more.
Almost as batteries-included as python.
Error: your CPUID command does not support command 0x80000006 (AMD-style L2 cache information).
AMD Ryzen 7 1800X