Compiling a Lisp to x86-64: primitive functions
bernsteinbear.com
bernsteinbear.com
Great work, I especially like the use of C instead of a higher level language.
I’m getting interested in bare metal, I.e. no OS, booting direct to app, development.
These compilers are a goldmine of info.
> It’s important to note that we can only accomplish this tagging scheme because on modern computer systems the lower 3 bits of heap-allocated pointers are 0 because allocations are word-aligned — meaning that all pointers are numbers that are multiples of 8. This lets us a) differentiate real pointers from fake pointers and b) stuff some additional data there in the real pointers.
It's what I suspected, but I didn't know that it was actually safe to assume that.
The macro expander was a particularly desirable part to have in Scheme, as it since allowed more people to contribute to this complex but crucial component.
Clasp is exactly that! (https://www.cliki.net/Clasp)
There's also SICL, which includes the Cleavir framework (used by Clasp I think?). (https://cliki.net/SICL)
And there are also other non-Lisp languages that manage to attain similar levels of "meta" power and expressiveness. Terra is one example; it uses LLVM. (http://terralang.org/)
It's not a JIT compiler.
For example, you have a function that does a bunch of math without knowing the types of the arguments. A JIT compiler can first compile it with generic math, but later observe that it's almost always called with fixnums, and choose to create a specialized implementation for fixnums. SBCL relies on the programmer to implement and call the optimized version when appropriate.
It's then often used in the READ EVAL PRINT LOOP. The EVAL function is then implemented by first compiling the source code passed (SBCL compiles it to machine code) and then calling the compiled code. This is then a tiny AOT compilation step in each REPL usage. That feature (REPL default incremental AOT machine code compilation) is available in some Lisp implementations at least since the 80s - but I don't know if it was used even earlier. Might be. Often interpreters were used in the REPL -> one of the reasons was reduced latency. Popular always compiling REPLs used a fast (and thus a bit dumb) compiler, then.
Why should this not be called JIT in the SBCL case? Because even in this case, where the user enters code to a running Lisp, usually all of the source code (not byte code -> SBCL does not use a byte code engine) is unconditionally compiled to machine code before (!) it gets executed.
What people usually (outside of Lisp) experience as a JIT compiler is actually something like a combination of an AOT byte code compilation step and runtime JIT machine code generation - usually under control of some byte code engine, which decides when and what to compile. SBCL does not do the moral equivalent of that.
Now SBCL also has an optional source code (-> not byte code) interpreter, which might do fancy stuff when used.
C:\>nokolisp
1> (comp-debug t)
t
2> (ncompile ’(plus a 1))
$O9CB:$7149: MOV AX, [$01BA]
$O9CB:$714C: CALL $0F1D ; CALL NUMVAL
$O9CB:$714F: MOV BX,$01
$O9CB:$7152: ADD AX,BX
$O9CB:$7154: CALL $05C0 ; CALL MAKNUM
$O9CB:$7157: JMP $1DA7
(subru: eval=$7149, compile=$3BB0)I assume $1DA7 is the outer interpreter loop?
What is $3BB0 for?
JMP $1DA7 is basically return-from-closure.
(subru: eval=$7149, compile=$3BB0) means that compiled functions are treated differently, when called from intrepreter and compiler. It was direct machine-code CALL in compiled code.
I'd like to see the capability of recompiling functions while they have live stack frames, and this just working (within defined limits).
Most things people learn are "solved" problems for somebody.
Perhaps you could start an interesting discussion on how you see that first sentence being wrong?