A Lisp interpreter written in Lisp (2017)
lvguowei.me
lvguowei.me
┬─┬ ────────────────────────────────────────────────────┬─┬──
└─┤ ────────────────────────────────────────────────────┼─┼─┬
│ ┬───────────────────────────────────────────────────┼─┼─┼
│ │ ──┬────────────────────────────────────────────── ├─┘ │
│ │ ┬─┼─────────────────────────────────────────────┬ │ │
│ │ ┼─┼─┬─────────────┬───────────────────┬─────────┼ │ │
│ │ ┼─┼─┼───┬─────────┼─┬───────────┬─────┼─┬───────┼ │ │
│ │ │ ┼─┼─┬─┼─────────┼─┼─────────┬─┼───┬─┼─┼─────┬─┼ │ │
│ │ │ │ │ ┼─┼─┬───────┼─┼─┬────── │ │ ┬─┼ ┼─┼─────┼─┼ │ │
│ │ │ │ │ │ │ ┼─────┬ │ ┼─┼───┬── │ │ ├─┘ │ ┼─┬───┼ │ │ │
│ │ │ │ │ │ │ ┼───┬─┼ │ │ ┼─┬─┼─┬ │ ├─┘ │ │ ┼─┬─┼ │ │ │
│ │ │ │ │ │ │ │ ┬─┼─┼ │ │ └─┤ ├─┘ └─┤ │ │ │ ├─┘ │ │ │
│ │ │ │ │ │ │ │ └─┤ │ │ │ ├─┘ │ │ │ ├─┘ │ │ │
│ │ │ │ │ │ │ │ ├─┘ │ ├───┘ │ │ ├─┘ │ │ │
│ │ │ │ │ │ │ ├───┘ ├─┘ │ └─┤ │ │ │
│ │ │ │ │ │ ├─┘ │ │ ├───────┘ │ │
│ │ │ │ │ └─┤ │ ├───────┘ │ │
│ │ │ │ │ ├─────────┘ │ │ │
│ │ │ │ ├───┘ │ │ │
│ │ │ └─┤ │ │ │
│ │ │ ├───────────────────────────┘ │ │
│ │ ├───┘ │ │
│ └─┤ │ │
│ └─────────────────────────────────────────────────┤ │
│ ├───┘
└─────────────────────────────────────────────────────┘
Or in non-graphical notation: (λ 11)(λ λ λ 1(λ λ λ λ 3(λ 5(3(λ 2(3(λ λ 3(λ 123)))(4(λ 4(λ 31(21)))))) (1(2(λ 12))(λ 4(λ 4(λ 2(14)))5))))(33)2)(λ 1((λ 11)(λ 11)))See https://tromp.github.io/cl/cl.html and https://tromp.github.io/cl/Binary_lambda_calculus.html for details...
For anyone interested in the graphical representation of terms like this I recommend To Dissect a Mockingbird: https://dkeenan.com/Lambda/
E = Y(\e m.m (\x.x) (\m n.(e m)(e n)) (\m v.e (m v)))
is probably easier to remember :) I thank Ben Lynn for introducing me to it on https://crypto.stanford.edu/~blynn/lambda/Just in case people mistakenly assume Lisp is in some way more practical, please checkout https://crypto.stanford.edu/~blynn/compiler/
If you represent a set as indicator function, then the Russell's set R = \x. N (x x), where N is logical negation. Now let's ask it whether it's a member of itself, and you get (\x. N (x x)) (\x. N (x x)), which essentially tries to find a fix-point of logical negation (and tragically, diverges in the process). Now abstract N away, and you have the fully general Y you know and love: \f. (\x. f (x x)) (\x. f (x x))
I've seen this trick just this morning at [0], and I am absolutely enchanted with this observation.
For sets, the way Russell's paradox appears is this: letting Set be the class of all sets, then every set x determines a predicate, which is a function Set -> Bool that for each x returns true or false depending on whether or not y is an element of x. Have I : Set -> (Set -> Bool) be the function that takes a set and turns it into a predicate: I x = \y, y ∈ x. An axiom of set theory (extensionality) is that I is injective. What if I were also surjective? (This is the non-axiom "unrestricted comprehension", that every predicate determines a set.) Well, then we could apply the fixed point theorem to negation N : Bool -> Bool, but N obviously doesn't have a fixed point! (Using the above notation, q = \y, N (y ∈ y) is the predicate that checks whether a set doesn't contain itself, p = {y | N (y ∈ y)} is the supposed set of all sets that don't contain themselves, and s = (p ∈ p) is the impossible fixed point for N.)
Here's what the theorem says for lambda calculus. Suppose L is the set of all lambda expressions (where equivalent lambda expressions are equal). In lambda calculus, every expression is also a function, in the sense that if E is an expression then \x, E x is equivalent, so we can have an "interpreter" I : E -> (E -> E) be the identity function. The fixed point theorem says that if you have a function f : L -> L, then it has a fixed point. And what is it? Tracing through the construction, we see it's nothing other than \x, (f x x) (f x x)!
(The complexity in stating the theorem precisely is to be able to restrict what we mean by functions A -> B. For the lambda calculus example, we need E -> E to mean just the functions realizable as lambda expressions. This does work out because reflexive objects exist[2].)
[1] https://ncatlab.org/nlab/show/Lawvere%27s+fixed+point+theore...
While we're here, another cool application is that quines exist. I'll give a way that misuses the theorem (though in a correctable way) to derive a quine. Consider the meta-function quote : L -> L that takes a lambda expression and produces a representation of it (like the representation used by the self-interpreter two comments up). I say meta-function because this isn't implemented by a lambda expression itself. Applying the fixed-point theorem to the same I : L -> (L -> L) with quote, if it were an actual lambda expression, we'd get a lambda expression s with s = quote s. That is, the expression s would evaluate to its own representation!
The fixed point is purportedly (\x, quote (x x)) (\x, quote (x x)), which doesn't make sense since quote is not a function. However, suppose q is a lambda expression that takes representations of lambda expressions and quotes those, so it satisfies the equation q (quote x) = quote (quote x) for all lambda expressions x. Also, let app : L -> L -> L be the constructor for application. Then (\x, q (app x (q x))) (quote (\x, q (app x (q x)))) fixes the problems and is a quine:
(\x, q (app x (q x))) (quote (\x, q (app x (q x))))
= q (app (quote (\x, q (app x (q x))))
(q (quote (\x, q (app x (q x))))))
= quote ((\x, q (app x (q x)))
(quote (\x, q (app x (q x)))))
(A way to do this all above board is to use the self-interpreter and somehow use q in the thing we're trying to find a fixed point of.)- a C++ compiler written in C++ in millions of lines of code
- a Perl (PHP, ...) interpreter written in the same language in one line of code (along the lines of `eval $1`)
Given the above the Lisp interpreter written in Lisp is somewhere in the middle and it doesn't really say much about the language. Just that you can use the built-in facilities of the language and write an interpreter that will implement those facilities using... themselves.
In this regard, how is it fundamentally different from `eval $1`?
The few primitives necessary for a metacircular interpreter can be easily implemented from scratch (hence the name "primitives", which clearly does not apply to `eval`).
But I don't think the program presented in this link was its implementation.
https://wiki.c2.com/?MetaCircularEvaluator
TCL is an edge case, since it simply represents everything as text, and its execution model is defined by passing and reevaluating everything as strings.
https://en.wikipedia.org/wiki/Homoiconicity
>In computer programming, homoiconicity (from the Greek words homo- meaning "the same" and icon meaning "representation") is a property of some programming languages. A language is homoiconic if a program written in it can be manipulated as data using the language, and thus the program's internal representation can be inferred just by reading the program itself. This property is often summarized by saying that the language treats "code as data".
https://wiki.c2.com/?HomoiconicLanguages
>Languages in which program code is represented as the language's fundamental data type are called 'homoiconic'. Such languages allow code and data to be DeeplyIntertwingled, so that new code can be generated and manipulated by the program itself at runtime. [...]
>Note that HomoiconicLanguages are strongly related to languages with a MetaCircularEvaluator, because it is always easy to make a MetaCircularEvaluator for a homoiconic language, but they are really two different topics. The Lisp example above is not metacircular, but it is homoiconic. You can write (with difficulty) an interpreter for C in C, but it will not make C homoiconic.
>Eliminate this category, mention that meta-circularity is a prerequisite for homoiconicity, but that it doesn't imply homoiconicity. Also, the "strong" vs. "pure" seems to be "real-world implementation" vs. "mathematical ideal". We should mention that (afaik, I could be wrong here) no implemented language is a 'pure homoiconic' language. [...]
>MetaCircularEvaluator (more commonly, "MetaCircularInterpreter"): it is possible to trivially implement a homoiconic language in itself. "Trivially" means that the semantics need not be specified explicitly; instead, they are implemented directly by the language construct being implemented. For instance, Lisp eval might be implemented by calling eval. If you don't already know what eval does, then reading the source code for the MetaCircularInterpreter might not enlighten you, it might just say "eval means eval". Thus "metacircular".
>It is famous that the core of the Lisp language can be written in about 20 lines of Lisp. This is possible because the implementation is a MetaCircularInterpreter. The average person who writes a C compiler or interpreter requires about 20,000 lines of C to do so, and must be (or become) moderately expert about compilers or interpreters. Implementing Lisp in Lisp as a MetaCircularInterpreter teaches one extremely little about compilers/interpreters for other languages.
https://wiki.c2.com/?HomoiconicFaq
>Q: What's this about Tcl?
>A: Tcl is homoiconic because evaluation of data is part of the language: force evaluation of data, and it becomes program, and that's part of the language definition - that's how while works, for instance, in Tcl: it forces evaluation of its first string argument, and if the result is true, then it forces evaluation of its second string argument. Essentially the same is true of Lisp S-expressions as program or data (and is not true of Lisp hash tables nor arrays). The same is true of a subset of Snobol. It is not true of C, Java, C++, even though they're TuringEquivalent.
Also: homoiconicity is something you can't just add to a programming language with a class or library or enough code or extensions. It's a property of the language definition itself.
>Q: Well, the C language doesn't natively support constructs to manipulate C code, true, but what if, when compiled, it...
>A: Nope. Doesn't matter. If a language isn't homoiconic at the source level, then no example of what can be done once it's compiled will change that. Why not? Because compilation means translation to a different language (such as machine language). Anything you can say about the compiled program is a statement about a language other than C.
>Q: Well, but I could write a C program that, when run, would...
>A: Nope. Doesn't matter. Same issue. You could write a C program that implements a Lisp interpreter. That doesn't make C into Lisp.
[ Epic flame war about whether or not machine language is homoiconic redacted! ;) But the final quesiton is interesting: ]
>Q: Is Homoiconic much ado about nothing ?
>A: Yes, after much wasted bandwidth on c2 this seems to be the only logical conclusion. In particular "homoiconicity" should, in principle, facilitate meta-programming techniques, on its own it is of very little value. Languages without homoiconicity have managed to accomplish a lot in this area using lighter techniques. See for example AspectWerkz, RubyOnRails, etc. In the same time some homoiconic languages like Common Lisp fall far short from being fully reflective environments, and this subtracts further from the value of being homoiconic. In the end, what the client programmer should ask for is results. Whether or not a language is fully, 50% or 0% homoiconic matters very little.
I've heard before that it's only "code is data" and not "data is code" since the data you manipulate will not always be code. Is this correct?
Any data that is going to be processed impacts the result of processing, so data is code, too.
That's most clearly true when data gets passed directly to an execution engine (eval, or sent to a database as SQL after some other bits get stuck onto it, etc.), but there's a perspective in which it is true generally, though not all data processed is unrestricted code.
However, by the time that $1 has been passed, the Lisp zeroes and ones of the eval function have been turned into the appropriate machine language of the CPU.
As of course different CPUs have different evaluation pipelines for integers, floats, vertices, branches etc.
So it evaluates a block of lisp as purely integers, floats, branches, vertices, and sends the appropriate data type off to the appropriate evaluation line, and reassembles it.
It will be machine code that accepts unicode strings, parses those unicode strings into Lisp, compiles the Lisp into machine code, then evaluates the machine code.
So essentially it's not the same as passing a Lisp variable to a Lisp function inside a Lisp text file.
One specificity of Lisp however is that the textual representation already looks like the abstract syntax tree that the Lisp variable which is passed to the Lisp function in the text file contains.
But I can easily admit that this fact can also be seen as some sort of illusion since in the end it is just a sequence of bytes.
#include </dev/tty>
This most beautiful program ever doesn't even need to be compiled, it's a JIT compiler in 1 line of code. $ cat lol.c
#include <stdio.h>
#include </dev/tty>
$ gcc lol.c
int main() { printf("lol\n"); } // hit Ctrl+D
$ ./a.out
lolThe part that I think I do a poor job of understanding/explaining is that lisp doesn't eval strings of code. Instead, it evals lists of tokenized data.
The book Structure and Interpretation of Computer Programs is organized by types of abstractions. The first three sections are about procedures, data, and modularity, but then the fourth is about abstracting language itself (called "metalinguistic abstraction"). This was a principle used often in classic AI research, where you built specific languages to implement different planning systems -- incidentally this is why it's called "scheme". By building a lisp in a lisp, you could make use of all the facilities of the host language and add new useful features. The book demonstrates how to add lazy evaluation, nondeterministic operators, and even Prolog-style logic programming to the language.
These sorts of interpreters depend on having a compatible host language. The last section is about how you can transcend limitations by essentially simulating an appropriate CPU in the host language (i.e., a virtual machine). This is how the original Scheme was implemented in its host Lisp, which was necessary because Scheme has tail recursion and Lisps haven't traditionally had that.
https://mitpress.mit.edu/sites/default/files/sicp/index.html
[0] https://www.cambridge.org/core/books/lisp-in-small-pieces/66...
[1] https://www.paracamplus.com/spip/?page=livre&isbn=978-2-9164...
Very well written, concise, and helps with getting a novice programmer going with Python. Which is a huge selling point to practically minded new programmers because you can trick them into it with "this is the best way to learn Python read this."
I haven't compared it to SICP though, which is much longer.
Dave Beazley's A Talk Near the Future of Python (a.k.a., Dave live-codes a WebAssembly Interpreter) (2019)
This was one of the most impressive and elegant demonstrations I've ever seen and I've been in computing since the 80s.
https://www.youtube.com/watch?v=OyfBQmvr2Hc
It's one of my favorite programs to play around with. I wrote about an Erlang implementation of this a short while back:
https://thingstoreadabout.substack.com/p/lisp-in-seven-parts
It was originally John Warnock's idea, that Glenn implemented. And it led to Adobe Acrobat's "Distiller". Acrobat is basically PostScript without the programming language.
No, you could not make it optimize itself by sending it to a PostScript printer two times in a row. It was not magic: all it did was intercept and capture the side-effects of the PostScript drawing commands (fill, stroke, show), read out the path, and optimize it in a uniform coordinate system. Since it didn't do any drawing, so it would just output an empty program if run on itself. (Take that, halting problem!)
https://donhopkins.com/home/archive/postscript/newerstill.ps...
>From: greid@adobe.com (Glenn Reid) Newsgroups: comp.lang.postscript Subject: release 10 of the Distillery Date: 10 Mar 89 10:21:52 GMT
>Here is another release of the PostScript Language Distillery. I know it's not terribly long after the last release, but there are some significant enhancements, and I thought it would be worthwhile.
>I finally took a closer look at user-defined fonts, which now seem to be working fairly well. In particular, it seems to handle the Macintosh screen bitmap fonts that get used if the native font is unavailable when the print file is genreated. The entire user-defined font is reverse-engineered to the output file as it stands, and is used exactly like the original file used it. I also fixed some rotate text bugs, rotated charpath, and a few other things.
>I want to emphasize that probably the two best uses of this program, currently, are to do speed comparisons of various PostScript language drivers and to convert "non-conforming" files into "conforming" files. It is not particularly well suited to carefully hand-written programs, especially not those which use looping constructs. It works (usually), but it unrolls the loops and makes the files much bigger.
It's also possible to simply write a metacircular PostScript evaluator in PostScript:
https://donhopkins.com/home/archive/NeWS/ps.ps
https://donhopkins.medium.com/the-shape-of-psiber-space-octo...
>Printing Distilled PostScript
>The data structure displays (including those of the Pseudo Scientific Visualizer, described below) can be printed on a PostScript printer by capturing the drawing commands in a file.
>Glenn Reid’s “Distillery” program is a PostScript optimizer, that executes a page description, and (in most cases) produces another smaller, more efficient PostScript program, that prints the same image. [Reid, The Distillery] The trick is to redefine the path consuming operators, like fill, stroke, and show, so they write out the path in device space, and incremental changes to the graphics state. Even though the program that computes the display may be quite complicated, the distilled graphical output is very simple and low level, with all the loops unrolled.
>The NeWS distillery uses the same basic technique as Glenn Reid’s Distillery, but it is much simpler, does not optimize as much, and is not as complete.
>The Metacircular Postscript Interpreter
>A program that interprets the language it is written in is said to be “metacircular”. [Abelson, Structure and Interpretation of Computer Programs] Since PostScript, like Scheme, is a simple yet powerful language, with procedures as first class data structures, implementing “ps.ps", a metacircular PostScript interpreter, turned out to be straightforward (or drawrofthgiarts, with respect to the syntax). A metacircular PostScript interpreter should be compatible with the "exec" operator (modulo bugs and limitations). Some of the key ideas came from Crispin Goswell's PostScript implementation. [Goswell, An Implementation of PostScript]
>The metacircular interpreter can be used as a debugging tool, to trace and single step through the execution of PostScript instructions. It calls a trace function before each instruction, that you can redefine to trace the execution in any way. One useful trace function animates the graphical stack on the PSIBER Space Deck step by step.
>The meta-execution stack is a PostScript array, into which the metacircular interpreter pushes continuations for control structures. (forall, loop, stopped, etc…) A continuation is represented as a dictionary in which the state needed by the control structure is stored (plus some other information to help with debugging).
>It is written in such a way that it can interpret itself: It has its own meta-execution stack to store the program’s state, and it stashes its own state on the execution stack of the interpreter that’s interpreting it, so the meta-interpreter’s state does not get in the way of the program it’s interpreting.
>It is possible to experiment with modifications and extensions to PostScript, by revectoring functions and operators, and modifying the metacircular interpreter.
>The metacircular interpreter can serve as a basis for PostScript algorithm animation. One very simple animation is a two dimensional plot of the operand stack depth (x), against the execution stack depth (y), over time.
I guess as beautiful as C++ and C code controlling the termodynamics of properllers in an hostile weather organism while looking for the presence of life.