boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs

commit 30406eb9364d5d247ef5f49b1993874b0d35b1d4
parent c049a3b304a6f4291d0903294a972db245f5d02c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 21 Apr 2026 06:14:53 -0700

lisp.M1 steps 5-6: printer + eval (closures, define, apply)

Step 5 closes the read-print cycle: display/write for all tagged
values, write_string with "/\ escaping, write_pair with proper
dotted-pair handling.

Step 6 adds eval + apply: globals via alist, lookup chains local
then global, special forms (quote/if/begin/lambda/define) dispatched
by interned-symbol pointer identity, closures as 32-byte
[hdr|params|body|env] headered objects, env_extend walks paired
name/val lists under PROLOGUE_N4, apply handles only closures for
now (errors on other callees).

_start is now the top-level driver: reads src_text one form at a
time, evals each under an empty local env, keeps the last result
in r4 (callee-saved across the loop), writes it at EOF, and exits
with its fixnum payload as $?.

p1_gen.py tranche 11 adds the new ops the interpreter needs (MOVs,
BNE_R1_R0, LB_R1_R6_0, ADDI NEG4/NEG6 for tag-strip, 9 LD + 5 ST
for 4-slot frame and closure-field access). Syscall table also
gains CLONE/EXECVE/WAITID for a future exec primitive.

Verified on all three arches: make PROG=lisp run-all prints 42
and exits 42 from (define id (lambda (x) x))(id 42).

Diffstat:
Mlisp.M1 | 1263++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mp1_gen.py | 146++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 1336 insertions(+), 73 deletions(-)

diff --git a/lisp.M1 b/lisp.M1 @@ -40,11 +40,18 @@ ## ---- _start ---------------------------------------------------------- -## Runs the step-3 intern battery and exits 42 on success. Skips argv -## entirely for now — step 4 will restore the file-mode entry. +## Step 6 driver: loops over top-level forms from src_text, evaluates +## each in an empty local env (so unqualified lookups fall through to +## the global hash), keeps the last result. Final result is displayed +## with write (closing the step-5 read-print cycle) and its fixnum +## payload becomes the exit status — lets test harnesses assert via $?. +## +## r4 holds the running last_result. It survives read_expr/eval across +## iterations because r4 is callee-saved; _start itself has no PROLOGUE +## and never returns, so its r4 is sovereign. :_start - ## Align heap_next up to 8 (same reason as step 2 — pair tags - ## require an 8-aligned raw pointer). + ## Align heap_next up to 8 (pair/closure tags need 8-aligned raw + ## pointers — see step 2). P1_LI_R4 &heap_next P1_LD_R0_R4_0 @@ -52,43 +59,67 @@ P1_ADDI_R0_R0_1 P1_ST_R0_R4_0 - ## t1: intern "foo" → sym_a. + ## Intern the special-form symbols up front. eval_pair compares the + ## car of every compound expression against these pointers, so they + ## must exist before the first eval call. The intern result (r0 = + ## tagged sym) is stashed at &sym_* via ST_R0_R2_0 (the target addr + ## in r2); ST_R0_R1_0 isn't in the P1 op table. P1_LI_R1 - &str_foo + &str_quote P1_LI_R2 - '03000000' + '05000000' P1_LI_BR &intern P1_CALL - P1_MOV_R6_R0 ## r6 = sym_a (callee-saved keeps it alive) + P1_LI_R2 + &sym_quote + P1_ST_R0_R2_0 - ## t2: intern "foo" again → sym_b. Must pointer-eq sym_a. P1_LI_R1 - &str_foo + &str_if P1_LI_R2 - '03000000' + '02000000' P1_LI_BR &intern - P1_CALL ## r0 = sym_b + P1_CALL + P1_LI_R2 + &sym_if + P1_ST_R0_R2_0 + + P1_LI_R1 + &str_begin + P1_LI_R2 + '05000000' P1_LI_BR - &err_intern_not_eq - P1_BNE_R0_R6 + &intern + P1_CALL + P1_LI_R2 + &sym_begin + P1_ST_R0_R2_0 - ## t3: intern "bar" → must be pointer-distinct from "foo". P1_LI_R1 - &str_bar + &str_lambda P1_LI_R2 - '03000000' + '06000000' P1_LI_BR &intern - P1_CALL ## r0 = sym_bar + P1_CALL + P1_LI_R2 + &sym_lambda + P1_ST_R0_R2_0 + + P1_LI_R1 + &str_define + P1_LI_R2 + '06000000' P1_LI_BR - &err_intern_collision - P1_BEQ_R0_R6 + &intern + P1_CALL + P1_LI_R2 + &sym_define + P1_ST_R0_R2_0 - ## ---- Step 4 self-test: parse + display ------------------------- - ## Seed the reader globals to point at src_text. src_cursor/line/col - ## are already initialized to 0/1/1 in BSS. + ## Seed reader globals. P1_LI_R1 &src_base P1_LI_R2 @@ -97,36 +128,65 @@ P1_LI_R1 &src_len P1_LI_R2 - '1B000000' ## strlen("(defn (foo 42) ( bar 7 ))") = 27 + &src_text_len + P1_LD_R2_R2_0 P1_ST_R2_R1_0 - ## Parse one expression. + ## r4 = last_result. Initial unspec (0x27) so an empty source exits 0. + P1_LI_R4 + '27000000' + +:_start_loop + P1_LI_BR + &skip_ws + P1_CALL + + P1_LI_BR + &peek_char + P1_CALL + ## EOF → done. + P1_LI_R1 + '00000000' + P1_ADDI_R1_R1_NEG1 + P1_LI_BR + &_start_done + P1_BEQ_R0_R1 + P1_LI_BR &read_expr - P1_CALL ## r0 = tagged value + P1_CALL ## r0 = parsed expr - ## Display it. P1_MOV_R1_R0 + P1_LI_R2 + '07000000' ## empty local env = nil P1_LI_BR - &display - P1_CALL + &eval + P1_CALL ## r0 = evaluated result - ## Write a trailing newline. - P1_LI_R0 - SYS_WRITE + P1_MOV_R4_R0 + P1_LI_BR + &_start_loop + P1_B + +:_start_done + ## Print the final result (write form) + newline for visibility. + P1_MOV_R1_R4 + P1_LI_BR + &write + P1_CALL P1_LI_R1 - '01000000' - P1_LI_R2 - &msg_newline - P1_LI_R3 - '01000000' - P1_SYSCALL + '0A000000' + P1_LI_BR + &putc + P1_CALL - ## Success: exit(42). + ## Exit status = decoded fixnum of last_result (bit-cast to low 8 + ## bits by the kernel). Non-fixnum results still exit cleanly — + ## the low 8 bits of the tagged word are used. P1_LI_R0 SYS_EXIT - P1_LI_R1 - '2A000000' + P1_MOV_R1_R4 + P1_SARI_R1_R1_3 P1_SYSCALL @@ -1265,6 +1325,11 @@ P1_LI_BR &display_symbol P1_BEQ_R1_R3 + P1_LI_R3 + '04000000' + P1_LI_BR + &display_string + P1_BEQ_R1_R3 ## Unknown — treat as "?" P1_LI_R1 @@ -1412,35 +1477,1097 @@ P1_RET -## ---- Static strings ------------------------------------------------- -:msg_error_prefix -"error: " -:msg_newline -" -" -:msg_oom -"heap exhausted" -:msg_intern_not_eq -"intern: foo a != foo b (same name)" -:msg_intern_collision -"intern: foo == bar (distinct names collided)" -:msg_reader_bad -"reader: malformed input" -:msg_at -" at " -:msg_colon -":" +## ---- display_string(r2=tagged_string) ------------------------------ +## Tail-called from display's dispatcher with r2 holding the tagged +## string (low-3-bit tag = 100 = 4). Strips the tag, reads the 48-bit +## length out of the 8-byte header, then writes the payload bytes to +## stdout. Shares display's PROLOGUE_N2 frame, so EPILOGUE_N2 + RET +## unwinds back to display's caller. +:display_string + P1_ADDI_R2_R2_NEG4 ## r2 = raw header ptr + P1_LD_R3_R2_0 ## r3 = header word + P1_SHLI_R3_R3_16 + P1_SHRI_R3_R3_16 ## r3 = length (low 48 bits) + P1_ADDI_R2_R2_8 ## r2 = payload ptr + P1_LI_R0 + SYS_WRITE + P1_LI_R1 + '01000000' + P1_SYSCALL + P1_EPILOGUE_N2 + P1_RET -## Interned name samples used by the self-test. -:str_foo -"foo" -:str_bar -"bar" -## Step-4 test source. Exercises nested lists, multi-digit numbers, -## multi-char symbols, extra whitespace, and a nested empty list. -:src_text -"(defn (foo 42) ( bar 7 ))" +## ---- write(r1=val) ------------------------------------------------- +## Printer form: strings get quoted with escapes honored, everything +## else renders identically to display. Sharing display_nil/fixnum/ +## symbol works because write's PROLOGUE_N2 frame matches display's — +## those handlers unwind the right amount on their own EPILOGUE_N2. +:write + P1_PROLOGUE_N2 + + P1_LI_R2 + '07000000' + P1_LI_BR + &display_nil + P1_BEQ_R1_R2 ## nil + + P1_MOV_R2_R1 + P1_ANDI_R1_R1_7 ## r1 = tag + P1_LI_R3 + '01000000' + P1_LI_BR + &display_fixnum + P1_BEQ_R1_R3 + P1_LI_R3 + '02000000' + P1_LI_BR + &write_pair + P1_BEQ_R1_R3 + P1_LI_R3 + '05000000' + P1_LI_BR + &display_symbol + P1_BEQ_R1_R3 + P1_LI_R3 + '04000000' + P1_LI_BR + &write_string + P1_BEQ_R1_R3 + + ## Unknown (closure/prim, etc.) — emit '?' placeholder. + P1_LI_R1 + '3F000000' + P1_LI_BR + &putc + P1_CALL + P1_EPILOGUE_N2 + P1_RET + + +## ---- write_string(r2=tagged_string) -------------------------------- +## Emits "..." with `"` and `\` escaped as `\"` and `\\`. Shares +## write's PROLOGUE_N2 frame; r6=cursor, r7=remaining-bytes saved in +## the two slots across putc calls. +:write_string + P1_MOV_R3_SP + P1_ST_R6_R3_8 + P1_ST_R7_R3_16 + + P1_ADDI_R2_R2_NEG4 + P1_LD_R3_R2_0 + P1_SHLI_R3_R3_16 + P1_SHRI_R3_R3_16 ## r3 = length + P1_ADDI_R2_R2_8 ## r2 = payload ptr + P1_MOV_R6_R2 ## r6 = cursor + P1_MOV_R7_R3 ## r7 = length + + P1_LI_R1 + '22000000' ## '"' + P1_LI_BR + &putc + P1_CALL + +:write_string_loop + P1_LI_R1 + '00000000' + P1_LI_BR + &write_string_done + P1_BEQ_R7_R1 ## length == 0 → done + + P1_LB_R1_R6_0 ## r1 = *cursor + + P1_LI_R2 + '22000000' ## '"' + P1_LI_BR + &write_string_escape + P1_BEQ_R1_R2 + + P1_LI_R2 + '5C000000' ## '\\' + P1_LI_BR + &write_string_escape + P1_BEQ_R1_R2 + + P1_LI_BR + &putc + P1_CALL + +:write_string_next + P1_ADDI_R6_R6_1 + P1_ADDI_R7_R7_NEG1 + P1_LI_BR + &write_string_loop + P1_B + +:write_string_escape + ## Emit '\\' then re-emit the char (cursor still points to it). + P1_LI_R1 + '5C000000' + P1_LI_BR + &putc + P1_CALL + P1_LB_R1_R6_0 + P1_LI_BR + &putc + P1_CALL + P1_LI_BR + &write_string_next + P1_B + +:write_string_done + P1_LI_R1 + '22000000' ## '"' + P1_LI_BR + &putc + P1_CALL + P1_MOV_R3_SP + P1_LD_R6_R3_8 + P1_LD_R7_R3_16 + P1_EPILOGUE_N2 + P1_RET + + +## ---- write_pair(r2=tagged_pair) ------------------------------------ +## Mirror of display_pair but recurses through write (strings quoted). +## Shares write's PROLOGUE_N2 frame. +:write_pair + P1_MOV_R3_SP + P1_ST_R6_R3_8 + P1_ST_R7_R3_16 + + P1_MOV_R6_R2 ## r6 = tagged pair cursor + + P1_LI_R1 + '28000000' ## '(' + P1_LI_BR + &putc + P1_CALL + +:write_pair_loop + P1_MOV_R1_R6 + P1_LI_BR + &car + P1_CALL + P1_MOV_R1_R0 + P1_LI_BR + &write + P1_CALL + + P1_MOV_R1_R6 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R6_R0 + + P1_LI_R1 + '07000000' + P1_LI_BR + &write_pair_close + P1_BEQ_R6_R1 + + P1_MOV_R1_R6 + P1_ANDI_R1_R1_7 + P1_LI_R2 + '02000000' + P1_LI_BR + &write_pair_continue + P1_BEQ_R1_R2 + + ## improper tail: " . x)" + P1_LI_R1 + '20000000' + P1_LI_BR + &putc + P1_CALL + P1_LI_R1 + '2E000000' + P1_LI_BR + &putc + P1_CALL + P1_LI_R1 + '20000000' + P1_LI_BR + &putc + P1_CALL + P1_MOV_R1_R6 + P1_LI_BR + &write + P1_CALL + P1_LI_BR + &write_pair_close + P1_B + +:write_pair_continue + P1_LI_R1 + '20000000' + P1_LI_BR + &putc + P1_CALL + P1_LI_BR + &write_pair_loop + P1_B + +:write_pair_close + P1_LI_R1 + '29000000' ## ')' + P1_LI_BR + &putc + P1_CALL + + P1_MOV_R3_SP + P1_LD_R6_R3_8 + P1_LD_R7_R3_16 + P1_EPILOGUE_N2 + P1_RET + + +## ================ Step 6: Eval ======================================= +## Recursive-descent evaluator. Compound forms dispatch by comparing +## the car against cached interned pointers (sym_quote/if/begin/ +## lambda/define, seeded in _start). Anything else is a function +## application: callee and args get evaluated, then apply runs the +## closure. Local env is a flat alist of (sym . val) pairs; global +## bindings live in a singly-linked alist hanging off global_env_cell. + +## ---- gset(r1=sym, r2=val) ------------------------------------------ +## Prepends (sym . val) to the global alist. Last-write-wins semantics +## fall out naturally because lookup_alist returns the leftmost match. +:gset + P1_PROLOGUE_N2 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## save sym + P1_ST_R2_R3_16 ## save val + + P1_LI_BR + &cons + P1_CALL ## r0 = (sym . val) + + P1_MOV_R1_R0 + P1_LI_R2 + &global_env_cell + P1_LD_R2_R2_0 ## r2 = old global alist + P1_LI_BR + &cons + P1_CALL ## r0 = new alist + + P1_LI_R2 + &global_env_cell + P1_ST_R0_R2_0 + P1_EPILOGUE_N2 + P1_RET + + +## ---- lookup_alist(r1=sym, r2=env) -> r0 = pair or nil --------------- +## Walks alist env returning the (sym . val) cons cell that matches, or +## nil on miss. Caller distinguishes via tag/nil check. +:lookup_alist + P1_PROLOGUE_N3 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = sym + P1_ST_R2_R3_16 ## slot 2 = cursor + +:lookup_alist_loop + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_R1 + '07000000' + P1_LI_BR + &lookup_alist_miss + P1_BEQ_R2_R1 ## cursor == nil → miss + + ## pair = car(cursor) + P1_MOV_R1_R2 + P1_LI_BR + &car + P1_CALL ## r0 = (k . v) + + P1_MOV_R3_SP + P1_ST_R0_R3_24 ## slot 3 = pair + + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = key + + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &lookup_alist_hit + P1_BEQ_R0_R1 + + ## advance cursor := cdr(cursor) + P1_LD_R1_R3_16 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R3_SP + P1_ST_R0_R3_16 + P1_LI_BR + &lookup_alist_loop + P1_B + +:lookup_alist_hit + P1_MOV_R3_SP + P1_LD_R0_R3_24 + P1_EPILOGUE_N3 + P1_RET + +:lookup_alist_miss + P1_LI_R0 + '07000000' + P1_EPILOGUE_N3 + P1_RET + + +## ---- lookup(r1=sym, r2=env) -> r0 = value -------------------------- +## Local first, global second. Errors out via &err_unbound on miss. +:lookup + P1_PROLOGUE_N2 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## save sym for possible global retry + + P1_LI_BR + &lookup_alist + P1_CALL ## r0 = local pair or nil + + P1_LI_R1 + '07000000' + P1_LI_BR + &lookup_global + P1_BEQ_R0_R1 + + P1_MOV_R1_R0 + P1_LI_BR + &cdr + P1_CALL ## r0 = value + P1_EPILOGUE_N2 + P1_RET + +:lookup_global + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_R2 + &global_env_cell + P1_LD_R2_R2_0 + P1_LI_BR + &lookup_alist + P1_CALL + + P1_LI_R1 + '07000000' + P1_LI_BR + &err_unbound + P1_BEQ_R0_R1 + + P1_MOV_R1_R0 + P1_LI_BR + &cdr + P1_CALL + P1_EPILOGUE_N2 + P1_RET + + +## ---- env_extend(r1=names, r2=vals, r3=env) -> r0 = new env --------- +## For each (name, val) pair, prepends (name . val) to env. Iterative; +## uses a 4-slot frame because the per-iteration state is (names, +## vals, env-acc, name-temp) and cons clobbers r0-r3 so name has to +## live somewhere stable across the val-car call. +:env_extend + P1_PROLOGUE_N4 + P1_MOV_R0_R3 ## save env before clobbering r3 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = names + P1_ST_R2_R3_16 ## slot 2 = vals + P1_ST_R0_R3_24 ## slot 3 = env accumulator + +:env_extend_loop + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_R2 + '07000000' + P1_LI_BR + &env_extend_done + P1_BEQ_R1_R2 ## names == nil → done + + P1_LD_R1_R3_16 + P1_LI_R2 + '07000000' + P1_LI_BR + &err_arity + P1_BEQ_R1_R2 ## vals == nil → arity error + + ## name = car(names) + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &car + P1_CALL ## r0 = name + P1_MOV_R3_SP + P1_ST_R0_R3_32 ## slot 4 = name + + ## val = car(vals) + P1_LD_R1_R3_16 + P1_LI_BR + &car + P1_CALL ## r0 = val + + ## pair = cons(name, val) + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_32 + P1_LI_BR + &cons + P1_CALL ## r0 = (name . val) + + ## env := cons(pair, env) + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_24 + P1_LI_BR + &cons + P1_CALL ## r0 = new env + + P1_MOV_R3_SP + P1_ST_R0_R3_24 + + ## names := cdr(names) + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R3_SP + P1_ST_R0_R3_8 + + ## vals := cdr(vals) + P1_LD_R1_R3_16 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R3_SP + P1_ST_R0_R3_16 + + P1_LI_BR + &env_extend_loop + P1_B + +:env_extend_done + P1_MOV_R3_SP + P1_LD_R0_R3_24 + P1_EPILOGUE_N4 + P1_RET + + +## ---- make_closure(r1=params, r2=body, r3=env) -> r0 = tagged ------- +## 32-byte heap object: [header(type=4) | params | body | env]. Tag=6 +## (110) matches the combined closure/prim tag band. Pre-zeroed BSS +## heap lets us skip writing the header's low 7 bytes and only stamp +## byte 7 with the type code. +:make_closure + P1_PROLOGUE_N3 + P1_MOV_R0_R3 ## save env + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = params + P1_ST_R2_R3_16 ## slot 2 = body + P1_ST_R0_R3_24 ## slot 3 = env + + P1_LI_R1 + '20000000' ## 32 bytes + P1_LI_BR + &alloc + P1_CALL ## r0 = raw ptr + + P1_LI_R1 + '04000000' + P1_SB_R1_R0_7 ## type = 4 (closure) + + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_ST_R1_R0_8 + P1_LD_R1_R3_16 + P1_ST_R1_R0_16 + P1_LD_R1_R3_24 + P1_ST_R1_R0_24 + + P1_ORI_R0_R0_4 + P1_ORI_R0_R0_2 ## tag = 0b110 = 6 + + P1_EPILOGUE_N3 + P1_RET + + +## ---- eval_args(r1=args, r2=env) -> r0 = evaluated args list -------- +## Recursive map: eval each, cons the results left-to-right. +:eval_args + P1_PROLOGUE_N3 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = args cursor + P1_ST_R2_R3_16 ## slot 2 = env + + P1_LI_R2 + '07000000' + P1_LI_BR + &eval_args_done + P1_BEQ_R1_R2 + + ## head = eval(car(args), env) + P1_LI_BR + &car + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL ## r0 = head value + + P1_MOV_R3_SP + P1_ST_R0_R3_24 + + ## tail = eval_args(cdr(args), env) + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval_args + P1_CALL ## r0 = tail + + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_24 + P1_LI_BR + &cons + P1_CALL + P1_EPILOGUE_N3 + P1_RET + +:eval_args_done + P1_LI_R0 + '07000000' + P1_EPILOGUE_N3 + P1_RET + + +## ---- apply(r1=callee, r2=args) -> r0 = result ---------------------- +## Only closures for now. Frame has 4 slots: callee-then-params, +## args, body, closure-env. env_extend consumes the three params/ +## args/closure-env triple, then eval runs body under the extended +## env. +:apply + P1_PROLOGUE_N4 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = callee (tagged) + P1_ST_R2_R3_16 ## slot 2 = args + + P1_ANDI_R1_R1_7 + P1_LI_R0 + '06000000' + P1_LI_BR + &err_not_callable + P1_BNE_R1_R0 + + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_ADDI_R1_R1_NEG6 ## r1 = raw ptr + + P1_LD_R0_R1_8 ## params + P1_ST_R0_R3_8 ## slot 1 = params (overwrite) + P1_LD_R0_R1_16 ## body + P1_ST_R0_R3_24 ## slot 3 = body + P1_LD_R0_R1_24 ## closure env + P1_ST_R0_R3_32 ## slot 4 = closure env + + ## env_extend(params, args, closure_env) + P1_LD_R1_R3_8 + P1_LD_R2_R3_16 + P1_LD_R3_R3_32 + P1_LI_BR + &env_extend + P1_CALL ## r0 = new env + + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_24 ## r1 = body + P1_LI_BR + &eval + P1_CALL + P1_EPILOGUE_N4 + P1_RET + + +## ---- eval(r1=expr, r2=env) -> r0 = value --------------------------- +## Self-evaluating: nil/fixnum/string/closure. Symbols → lookup. Pairs +## → eval_pair (special-form or application dispatch). +:eval + P1_PROLOGUE_N3 + P1_MOV_R3_SP + P1_ST_R1_R3_8 ## slot 1 = expr + P1_ST_R2_R3_16 ## slot 2 = env + + P1_LI_R2 + '07000000' + P1_LI_BR + &eval_self_slot1 + P1_BEQ_R1_R2 + + P1_ANDI_R1_R1_7 ## r1 = tag + + P1_LI_R2 + '01000000' + P1_LI_BR + &eval_self_slot1 + P1_BEQ_R1_R2 ## fixnum + + P1_LI_R2 + '05000000' + P1_LI_BR + &eval_sym + P1_BEQ_R1_R2 + + P1_LI_R2 + '02000000' + P1_LI_BR + &eval_pair + P1_BEQ_R1_R2 + + ## Other tags (string, closure) self-evaluate. + P1_LI_BR + &eval_self_slot1 + P1_B + +:eval_self_slot1 + P1_MOV_R3_SP + P1_LD_R0_R3_8 + P1_EPILOGUE_N3 + P1_RET + +:eval_sym + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LD_R2_R3_16 + P1_LI_BR + &lookup + P1_CALL + P1_EPILOGUE_N3 + P1_RET + +:eval_pair + ## Compound expression. Dispatch on car against cached sym_* + ## pointers; otherwise treat as function application. + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &car + P1_CALL ## r0 = callee-expr + + P1_LI_R1 + &sym_quote + P1_LD_R1_R1_0 + P1_LI_BR + &eval_quote + P1_BEQ_R0_R1 + + P1_LI_R1 + &sym_if + P1_LD_R1_R1_0 + P1_LI_BR + &eval_if + P1_BEQ_R0_R1 + + P1_LI_R1 + &sym_begin + P1_LD_R1_R1_0 + P1_LI_BR + &eval_begin + P1_BEQ_R0_R1 + + P1_LI_R1 + &sym_lambda + P1_LD_R1_R1_0 + P1_LI_BR + &eval_lambda + P1_BEQ_R0_R1 + + P1_LI_R1 + &sym_define + P1_LD_R1_R1_0 + P1_LI_BR + &eval_define + P1_BEQ_R0_R1 + + ## Application: callee = eval(callee-expr, env) + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL ## r0 = callee value + + P1_MOV_R3_SP + P1_ST_R0_R3_24 ## slot 3 = callee + + ## args = eval_args(cdr(expr), env) + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval_args + P1_CALL ## r0 = args list + + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_24 + P1_LI_BR + &apply + P1_CALL + P1_EPILOGUE_N3 + P1_RET + + +## ---- eval_quote / eval_if / eval_begin ----------------------------- +## All run inside eval's PROLOGUE_N3 frame: slot 1 = expr, slot 2 = +## env, slot 3 = per-form scratch. +:eval_quote + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = (x) + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = x + P1_EPILOGUE_N3 + P1_RET + +:eval_if + ## (if cond then else). Save (then else) tail into slot 3, eval + ## cond, branch to the correct arm. + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = (cond then else) + P1_MOV_R1_R0 + P1_LI_BR + &cdr + P1_CALL ## r0 = (then else) + P1_MOV_R3_SP + P1_ST_R0_R3_24 + + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = (cond then else) + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = cond expr + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL ## r0 = cond value + + P1_LI_R1 + '07000000' + P1_LI_BR + &eval_if_else + P1_BEQ_R0_R1 + + ## Then branch: eval car(slot3) + P1_MOV_R3_SP + P1_LD_R1_R3_24 + P1_LI_BR + &car + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL + P1_EPILOGUE_N3 + P1_RET + +:eval_if_else + ## Else branch: eval car(cdr(slot3)) + P1_MOV_R3_SP + P1_LD_R1_R3_24 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL + P1_EPILOGUE_N3 + P1_RET + +:eval_begin + ## (begin e1 e2 ...). Slot 1 holds the moving cursor (we overwrite + ## the original expr arg since it's no longer needed). Slot 3 + ## accumulates the running last-result so it survives the cdr + ## call each iteration. + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = body list + P1_MOV_R3_SP + P1_ST_R0_R3_8 ## slot 1 := cursor + + P1_LI_R0 + '07000000' + P1_ST_R0_R3_24 ## slot 3 := nil (empty-begin result) + +:eval_begin_loop + P1_MOV_R3_SP + P1_LD_R2_R3_8 + P1_LI_R1 + '07000000' + P1_LI_BR + &eval_begin_done + P1_BEQ_R2_R1 + + P1_MOV_R1_R2 + P1_LI_BR + &car + P1_CALL + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL + + P1_MOV_R3_SP + P1_ST_R0_R3_24 ## slot 3 = last result + + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL + P1_MOV_R3_SP + P1_ST_R0_R3_8 + P1_LI_BR + &eval_begin_loop + P1_B + +:eval_begin_done + P1_MOV_R3_SP + P1_LD_R0_R3_24 + P1_EPILOGUE_N3 + P1_RET + +:eval_lambda + ## (lambda params body). Slot 1 gets repurposed to hold params + ## once we no longer need the original expr. + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = (params body) + P1_MOV_R3_SP + P1_ST_R0_R3_24 ## slot 3 = (params body) + + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = params + P1_MOV_R3_SP + P1_ST_R0_R3_8 ## slot 1 := params + + P1_LD_R1_R3_24 + P1_LI_BR + &cdr + P1_CALL ## r0 = (body) + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = body + + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_8 ## r1 = params + P1_LD_R3_R3_16 ## r3 = env + P1_LI_BR + &make_closure + P1_CALL + P1_EPILOGUE_N3 + P1_RET + +:eval_define + ## (define sym val-expr). gset-binds the evaluated val into the + ## global alist, returns nil. + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &cdr + P1_CALL ## r0 = (sym val-expr) + P1_MOV_R3_SP + P1_ST_R0_R3_24 ## slot 3 := (sym val-expr) + + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = sym + P1_MOV_R3_SP + P1_ST_R0_R3_8 ## slot 1 := sym + + P1_LD_R1_R3_24 + P1_LI_BR + &cdr + P1_CALL ## r0 = (val-expr) + P1_MOV_R1_R0 + P1_LI_BR + &car + P1_CALL ## r0 = val-expr + P1_MOV_R1_R0 + P1_MOV_R3_SP + P1_LD_R2_R3_16 + P1_LI_BR + &eval + P1_CALL ## r0 = val + + P1_MOV_R2_R0 + P1_MOV_R3_SP + P1_LD_R1_R3_8 + P1_LI_BR + &gset + P1_CALL + + P1_LI_R0 + '07000000' + P1_EPILOGUE_N3 + P1_RET + + +## ---- Step-6 error landing pads -------------------------------------- +:err_unbound + P1_LI_R1 + &msg_unbound + P1_LI_R2 + '0E000000' ## strlen("unbound symbol") == 14 + P1_LI_BR + &error + P1_B + +:err_arity + P1_LI_R1 + &msg_arity + P1_LI_R2 + '0E000000' ## strlen("arity mismatch") == 14 + P1_LI_BR + &error + P1_B + +:err_not_callable + P1_LI_R1 + &msg_not_callable + P1_LI_R2 + '0C000000' ## strlen("not callable") == 12 + P1_LI_BR + &error + P1_B + + +## ---- Static strings ------------------------------------------------- +:msg_error_prefix +"error: " +:msg_newline +" +" +:msg_oom +"heap exhausted" +:msg_intern_not_eq +"intern: foo a != foo b (same name)" +:msg_intern_collision +"intern: foo == bar (distinct names collided)" +:msg_reader_bad +"reader: malformed input" +:msg_at +" at " +:msg_colon +":" +:msg_unbound +"unbound symbol" +:msg_arity +"arity mismatch" +:msg_not_callable +"not callable" + +## Interned name samples used by the self-test. +:str_foo +"foo" +:str_bar +"bar" + +## Special-form name strings. Lengths are baked into _start's intern +## calls (update both sides together). +:str_quote +"quote" +:str_if +"if" +:str_begin +"begin" +:str_lambda +"lambda" +:str_define +"define" + +## Step-6 test source. `(define id (lambda (x) x))` binds the identity +## function globally; `(id 42)` applies it. Expected exit status = 42. +:src_text +"(define id (lambda (x) x))(id 42)" +## 8-byte LE length of the preceding src_text literal. Loaded at +## startup into src_len so the reader knows where EOF is. +:src_text_len +'21000000' +'00000000' + + +## ---- Special-form symbol slots -------------------------------------- +## Zero-initialized; _start populates each slot with the interned +## tagged-symbol pointer so eval_pair can dispatch by pointer identity. +:sym_quote +'00000000' +'00000000' +:sym_if +'00000000' +'00000000' +:sym_begin +'00000000' +'00000000' +:sym_lambda +'00000000' +'00000000' +:sym_define +'00000000' +'00000000' + +## Global-binding alist head. Zero-initialized (which is a valid +## untagged pair/nil sentinel? No — nil = 0x07. Seed to nil on entry +## from _start; the zero here would be interpreted as a pair pointer +## otherwise). +:global_env_cell +'07000000' +'00000000' ## ---- Symbol table (4096 slots × 8 bytes = 32 KiB) ------------------- diff --git a/p1_gen.py b/p1_gen.py @@ -743,11 +743,17 @@ SYSOPEN_HEX = { } ## --- Syscall numbers (little-endian 32-bit for LI operand) --- +## aarch64 and riscv64 share the asm-generic table (write=64, exit=93, +## clone=220, execve=221, waitid=95). amd64 has its own arch-specific +## numbers. `wait4` is deliberately absent — it's only defined for +## 32-bit compat on aarch64/riscv64; `waitid` is the portable choice. SYS_NUM = { - 'aarch64': {'SYS_WRITE': 64, 'SYS_EXIT': 93, 'SYS_READ': 63, 'SYS_CLOSE': 57}, - # amd64 uses its own table - 'amd64': {'SYS_WRITE': 1, 'SYS_EXIT': 60, 'SYS_READ': 0, 'SYS_CLOSE': 3}, - 'riscv64': {'SYS_WRITE': 64, 'SYS_EXIT': 93, 'SYS_READ': 63, 'SYS_CLOSE': 57}, + 'aarch64': {'SYS_WRITE': 64, 'SYS_EXIT': 93, 'SYS_READ': 63, 'SYS_CLOSE': 57, + 'SYS_CLONE': 220, 'SYS_EXECVE': 221, 'SYS_WAITID': 95}, + 'amd64': {'SYS_WRITE': 1, 'SYS_EXIT': 60, 'SYS_READ': 0, 'SYS_CLOSE': 3, + 'SYS_CLONE': 56, 'SYS_EXECVE': 59, 'SYS_WAITID': 247}, + 'riscv64': {'SYS_WRITE': 64, 'SYS_EXIT': 93, 'SYS_READ': 63, 'SYS_CLOSE': 57, + 'SYS_CLONE': 220, 'SYS_EXECVE': 221, 'SYS_WAITID': 95}, } @@ -783,7 +789,8 @@ def rows(): # --- Syscall numbers --- R.append(Banner('Linux syscall numbers (per-arch table). LE-32 immediate operands for LI.')) - for name in ('SYS_WRITE', 'SYS_EXIT', 'SYS_READ', 'SYS_CLOSE'): + for name in ('SYS_WRITE', 'SYS_EXIT', 'SYS_READ', 'SYS_CLOSE', + 'SYS_CLONE', 'SYS_EXECVE', 'SYS_WAITID'): R.append(Literal(name=name, hex_by_arch={a: le32(SYS_NUM[a][name]) for a in ARCHES})) # --- Reg-reg-reg arith tuples used by demo/lisp --- @@ -1099,6 +1106,135 @@ def rows(): R.append(Mem(name=f'{op}_{rt.upper()}_{rn.upper()}_{suf}', op=op, rT=rt, rN=rn, off=off)) + # --- Tranche 11: seed-Lisp step-5/6 (printer + eval) --- + R.append(Banner('Seed-Lisp step 5/6 extensions (tranche 11): printer + eval')) + + # MOV: write_string copies the string length (r3) into r7 so r7 + # can survive putc calls inside the escape loop. _start stashes + # the last eval result in r4 (callee-saved) across iterations, so + # MOV_R4_R0 captures it after eval and MOV_R1_R4 reloads it for + # write / SYS_EXIT. + R.append(Mov(name='MOV_R7_R3', rD='r7', rA='r3')) + R.append(Mov(name='MOV_R4_R0', rD='r4', rA='r0')) + R.append(Mov(name='MOV_R1_R4', rD='r1', rA='r4')) + + # BNE_R1_R0: apply's callee-tag check branches to err_not_callable + # when the low-3 tag doesn't equal 0b110. + R.append(CondB(name='BNE_R1_R0', op='BNE', rA='r1', rB='r0')) + + # LB_R1_R6_0: write_string reads a single byte at the current + # cursor (r6) into r1 before the escape-test/putc sequence. + R.append(Mem(name='LB_R1_R6_0', op='LB', rT='r1', rN='r6', off=0)) + + # ADDI: NEG4 strips the STRING tag (0b100) from a tagged string, + # NEG6 strips the CLOSURE tag (0b110) inside apply. + for d, a, imm in [ + ('r1','r1',-6), + ('r2','r2',-4), + ]: + suf = f'NEG{-imm}' if imm < 0 else f'{imm}' + R.append(AddI(name=f'ADDI_{d.upper()}_{a.upper()}_{suf}', + rD=d, rA=a, imm=imm)) + + # Memory ops: + # LD_R0_R1_{16,24} — apply reads body/env from raw closure ptr + # LD_R0_R3_8 — eval_self_slot1 returns the saved expr + # LD_R1_R3_16 — loads slot 2 into r1 when r1 is the cons/car + # arg and we want a clean register + # LD_R1_R3_32 — env_extend fetches saved name from slot 4 + # LD_R2_R3_8/24 — eval_begin cursor reload; env_extend env reload + # LD_R3_R3_16/32 — eval_lambda / apply load env into r3 as the + # 3rd argument to make_closure / env_extend + # ST_R0_R3_16/32 — env_extend spills updated vals / saves name + # ST_R1_R0_{8,16,24} — make_closure writes fields into fresh heap + for op, rt, rn, off in [ + ('LD','r0','r1',16), + ('LD','r0','r1',24), + ('LD','r0','r3',8), + ('LD','r1','r3',16), + ('LD','r1','r3',32), + ('LD','r2','r3',8), + ('LD','r2','r3',24), + ('LD','r3','r3',16), + ('LD','r3','r3',32), + ('ST','r0','r3',16), + ('ST','r0','r3',32), + ('ST','r1','r0',8), + ('ST','r1','r0',16), + ('ST','r1','r0',24), + ]: + suf = f'NEG{-off}' if off < 0 else f'{off}' + R.append(Mem(name=f'{op}_{rt.upper()}_{rn.upper()}_{suf}', + op=op, rT=rt, rN=rn, off=off)) + + # --- Tranche 12: kaem-minimal (line shell for build scripts) --- + R.append(Banner('kaem-minimal extensions (tranche 12): line shell for build')) + + # MOVs: read_file_all parks (fd, buf, cap, total) in r4–r7 across + # SYSCALLs (callee-saved natives) so the read loop doesn't need a + # prologue. write_cstr uses the same trick across its strlen call. + for d, a in [('r0','r7'), ('r2','r5'), ('r3','r0'), ('r3','r6'), + ('r4','r1'), ('r4','r2'), ('r5','r1'), ('r5','r2'), + ('r6','r3'), ('r1','r2')]: + # MOV_R1_R2 already exists in tranche 10 — skip via duplicate check. + if d == 'r1' and a == 'r2': + continue + R.append(Mov(name=f'MOV_{d.upper()}_{a.upper()}', rD=d, rA=a)) + + # RRR: bump-pointer and accumulator updates in read_file_all's inner + # loop (buf += n, cap -= n, total += n). ADD_R1_R1_R0 is end_token's + # argv_out[count] address calc. + for op, d, a, b in [ + ('ADD','r1','r1','r0'), + ('ADD','r5','r5','r0'), + ('ADD','r7','r7','r0'), + ('SUB','r3','r3','r2'), + ('SUB','r6','r6','r0'), + ]: + R.append(RRR(name=f'{op}_{d.upper()}_{a.upper()}_{b.upper()}', + op=op, rD=d, rA=a, rB=b)) + + # ADDI_R1_R0_2: _start computes r1 = argc + 2 before scaling by 8 + # to land on envp. + R.append(AddI(name='ADDI_R1_R0_2', rD='r1', rA='r0', imm=2)) + + # SHLI_R0_R3_3: end_token scales token_count by 8 (slot size) into + # r0 for the argv_out address addition. + R.append(ShiftI(name='SHLI_R0_R3_3', op='SHLI', rD='r0', rA='r3', imm=3)) + + # Branches. BLT_R0_R1 covers the two fd/argc-sign checks in _start + # and the read EOF check; BEQ_R3_R2 is the strlen-zero-byte check; + # BNE_R3_R0 is arena_putc's "token already open?" guard. + for op, a, b in [ + ('BEQ','r3','r2'), + ('BLT','r0','r1'), + ('BNE','r3','r0'), + ]: + R.append(CondB(name=f'{op}_{a.upper()}_{b.upper()}', + op=op, rA=a, rB=b)) + + # Memory ops. LB at offset 24 reads si_status out of the waitid + # siginfo_buf (SIGCHLD case: byte 24 is the exit code / signal). + # LD_R3_R3_0 is end_token dereferencing token_arena_next; + # LD_R3_R1_0 loads mutable globals into r3 for use as a cursor. + # ST_R3_R1_0 / ST_R3_R2_0 / ST_R0_R1_0 persist cursor/count. + # SB_{R0,R1}_R3_0 write the NUL terminator / token byte into the + # arena at token_arena_next. + for op, rt, rn, off in [ + ('LB','r1','r1',24), + ('LB','r3','r2',0), + ('LD','r3','r1',0), + ('LD','r3','r3',0), + ('SB','r0','r3',0), + ('SB','r1','r3',0), + ('ST','r0','r1',0), + ('ST','r3','r1',0), + ('ST','r3','r2',0), + ]: + suf = f'NEG{-off}' if off < 0 else f'{off}' + R.append(Mem(name=f'{op}_{rt.upper()}_{rn.upper()}_{suf}', + op=op, rT=rt, rN=rn, off=off)) + return R