The First Lisp Compiler
texdraft.github.io
texdraft.github.io
The fact that this was once the norm but has since been done away with saddens me. I've always been fascinated with "live environments" but felt they only went half way if they didn't include the source code itself. If I'm going to be updating something in a running system, I want to know what source code was used to get the system to the state its currently in, and preferably be able to query that source code as data. Of course, that code could be kept within source control, but then it's a shadow of the running system -- a map of the territory and not the territory.
As far as I know, the only languages/environments where this functionality is still available are Tcl, Smalltalk, and to some extent stored procedures within an RDBMS.
Doesn't FUNCTION-LAMBDA-EXPRESSION apply here?
As for CCL, it seems to work for me just fine as long as you set CCL:*SAVE-DEFINITIONS* to T:
Clozure Common Lisp Version 1.12.1 (v1.12.1) LinuxX8664
For more information about CCL, please see http://ccl.clozure.com.
CCL is free software. It is distributed under the terms of the Apache
Licence, Version 2.0.
? (defun foobar (a b c) (* a (+ b c)))
FOOBAR
? (function-lambda-expression #'foobar)
NIL
NIL
FOOBAR
? (setf ccl:*save-definitions* t)
T
? (defun foobar (a b c) (* a (+ b c)))
FOOBAR
? (function-lambda-expression #'foobar)
(LAMBDA (A B C) (DECLARE (CCL::GLOBAL-FUNCTION-NAME FOOBAR)) (BLOCK FOOBAR (* A (+ B C))))
NIL
FOOBARhttp://www.lispworks.com/documentation/lw80/lw/lw-dspecs-ug....
Common Lisp has standard function FUNCTION-LAMBDA-EXPRESSION:
* (defun foo (a)
(my-if (> a 10) 'big 'small))
FOO
* (function-lambda-expression #'foo)
(LAMBDA (A) (BLOCK FOO (MY-IF (> A 10) 'BIG 'SMALL)))
T
FOOIf the Forth compiler generates native code the one-to-one relationship with the source is lost and SEE will typically show the compiled code as Assembly Language.
And its easy to view the source code from the console (REPL). Some examples from Rebol2...
>> source func
func: func [
"Defines a user function with given spec and body."
[catch]
spec [block!] {Help string (opt) followed by arg words (and opt type and string)}
body [block!] "The body block of the function"
][
throw-on-error [make function! spec body]
]
>> source source
source: func [
"Prints the source code for a word."
'word [word!]
][
prin join word ": "
if not value? word [print "undefined" exit]
either any [native? get word op? get word action? get word] [
print ["native" mold third get word]
] [print mold get word]
]I had no idea that this was in LISP 1.5. If you had asked me, I would have sworn it was Steele, 1977. Wikipedia supports that[1], albeit one might not consider it the most reliable source.
So apparently (partial) TCE has been around since at least 1961. In that light, it's baffling that it's not supported more universally.
[edit]
I should also point out that just storing the names of all of the symbols in the common lisp specification exceeds the RAM requirements of uLisp. Obviously builtins can go in ROM, but it gives you an idea of the sizes involved.
Building lists out of pairs and then using them as your intermediate format creates a lot of garbage.
By today's standards, the RAM usage isn't necessarily huge.
Here is the TXR Lisp compiler recompiling stdlib/compiler.tl -> stdlib/compiler.tlo, as seen in top:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11488 kaz 20 0 17800 14804 2964 R 98.1 0.7 0:07.07 txr
^^^^^ ^^^^^
On the order of a bash session. It's a lot of RAM by 1982 standards at the institution level, and even 1992 standards at the consumer level, but today it means nothing.You can easily see a Bash process a footprint on that order.
It could be reduced by tuning the garbage collector. One way to do that is to build for less memory use (useful for embedded). Here it is with txr rebuilt using #define CONFIG_SMALL_MEM 1 in config.h:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12838 kaz 20 0 11964 9768 3140 R 99.0 0.5 0:10.39 txr
Bash footprints for comparison: $ ps aux | head -1 ; ps aux | grep bash
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
kaz 1093 0.0 0.1 9288 2132 pts/2 Ss+ May15 0:01 -bash
kaz 2833 0.0 0.0 8904 1992 pts/0 Ss May15 0:00 -bash
kaz 3509 0.0 0.2 10532 4988 pts/1 Ss+ May15 0:28 -bash
kaz 7898 0.0 0.1 8968 2212 pts/3 Ss+ May20 0:00 -bash
Lists are used for everything: the compiler produces a list-based assembly code which is used from then through assembly. There is an optimizer which divides it into basic blocks, which are objects put into a graph, but the instructions still being lists. The peephole pattern matching is done on lists. The compiler does not bother using destructive append (nconc) for stitching together fragments of code; just straight garbage-generating appends. Same with most of the other rewriting that happens later.In a computer in 1960, your compiler would be capped to the physical memory available. That would be the RAM use. The garbage collector would have to be called whenever the memory is exhausted, or else the show would stop. A successful compilation would demonstrate that the compiler needed no more memory than what the machine has. The closer its actual usage would be to the available memory, the longer it would take, due to the frequent garbage collections required to stay afloat.
I'd say that given people's expectations today, shaped by experiences with everyday software, they likely greatly overestimate how much RAM you need for Lisp compiling.
Here you can also see the full command, confirming the compile job:
$ pmap 24087
24087: ./txr --in-package=sys --compile=stdlib/compiler.tl:stdlib/compiler.tlo.tmp
08048000 1660K r-x-- txr
081e7000 4K r---- txr
081e8000 12K rw--- txr
081eb000 124K rw--- [ anon ]
08c03000 6188K rw--- [ anon ]
b7c5e000 8K rw--- [ anon ]
b7c60000 1876K r-x-- libc-2.27.so
b7e35000 4K ----- libc-2.27.so
b7e36000 8K r---- libc-2.27.so
b7e38000 4K rw--- libc-2.27.so
b7e39000 12K rw--- [ anon ]
b7e3c000 116K r-x-- libz.so.1.2.11
b7e59000 4K r---- libz.so.1.2.11
b7e5a000 4K rw--- libz.so.1.2.11
b7e5b000 28K r-x-- libffi.so.6.0.4
b7e62000 4K r---- libffi.so.6.0.4
b7e63000 4K rw--- libffi.so.6.0.4
b7e64000 12K r-x-- libdl-2.27.so
b7e67000 4K r---- libdl-2.27.so
b7e68000 4K rw--- libdl-2.27.so
b7e69000 36K r-x-- libcrypt-2.27.so
b7e72000 4K r---- libcrypt-2.27.so
b7e73000 4K rw--- libcrypt-2.27.so
b7e74000 156K rw--- [ anon ]
b7e9b000 1024K r-x-- libm-2.27.so
b7f9b000 4K r---- libm-2.27.so
b7f9c000 4K rw--- libm-2.27.so
b7fba000 8K rw--- [ anon ]
b7fbc000 12K r---- [ anon ]
b7fbf000 8K r-x-- [ anon ]
b7fc1000 152K r-x-- ld-2.27.so
b7fe7000 4K r---- ld-2.27.so
b7fe8000 4K rw--- ld-2.27.so
bf8d9000 200K rw--- [ stack ]
total 11700K
You can see the 11700K fairly closely matches the earlier VIRT figure of 11964.Anyway, look at the [ anon ] heap area: it's like 6-something megs. That's it. That's where all the dynamic Lisp stuff is. All the predefined symbols and function bindings and whatnot, and all the objects allocated during the compile job.
libz is new; I integrated libz into TXR in just the most recent release. It happens to be number 277, so I code named it (L)Z77.
I wonder if a Lisp-like language which used vectors rather than pairs as its fundamental data structure might be a better fit for severely memory-constrained systems? On average, vectors take up half the memory consumption of lists.
Such a language would end up looking rather different to Lisp though. Cons cells encourage CAR/CDR and recursion. A vector-centric language would naturally lead to a more iterative programming style.
One reason why such optimization isn't common is most likely due to the current usage of Lisp across the industry, and the commercial implementations being a niche product.
In practice though, you run into some issues (1) RPLACD/set-car! makes CDR-coding much more complicated (I suppose you can just ban it – in Racket, pairs are immutable by default, although there is also a separate mutable pair type); (2) CDR-coding generally only works if you construct the list up-front, the existence of CONS can encourage a coding style in which you don't do that; (3) to fix (2), you can force the list to be CDR-coded by duplicating it, but you have to remember to do that at right points – if you don't do it, you'll miss out on the benefits of CDR-coding, but if you do it when you don't have to you are unnecessarily harming performance; (4) since CDR-coded and non-CDR-coded lists are the same type, and indistinguishable without peeling off the covers of the implementation, it makes it harder for the programmer to address (2) and (3) correctly.
Hint, they support them since Interlisp-C, ZetaLisp, Common Lisp,... so there is enough documentation and books where to educate yourself.
It's Greenberg.
For the folks that insist Clojure is a Lisp, try doing that as an exercise.