boot2

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

commit 60313fcd116e77f3f0016bb3cafe27c8c5c9cdb9
parent cf5f560c7bb3255588cc29a42027e9c9b4bfaa05
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 25 Apr 2026 06:48:39 -0700

Add scheme1 lambda, closures, and alist environments

eval's ::sym path now walks an alist env -- ((sym . val) . rest) -- and
falls through to sym_global only on a NIL chain, so lambda parameters
resolve before global bindings. The new `lambda` special form allocates
a 32-byte CLOSURE [hdr][params][body][env] and captures the current
env pointer directly (no free-var analysis). apply gains an
HDR.CLOSURE arm that spills body, runs bind_params to prepend
(param . arg) pairs onto the captured env, and tail-jumps eval_body.
eval_body evaluates body forms left-to-right with the final form in
tail position, so closure-tail-calls inherit the %tail invariant.
bind_params is fixed-arity only; the `.`-tail variadic case is a
follow-up. Tests cover single-param, multi-param env-walk, and a
nested closure that captures the outer binding.

Diffstat:
Mscheme1/scheme1.P1pp | 169++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atests/scheme1/03-lambda-call.expected-exit | 1+
Atests/scheme1/03-lambda-call.scm | 1+
Atests/scheme1/04-lambda-multi.expected-exit | 1+
Atests/scheme1/04-lambda-multi.scm | 1+
Atests/scheme1/05-lambda-capture.expected-exit | 1+
Atests/scheme1/05-lambda-capture.scm | 1+
7 files changed, 173 insertions(+), 2 deletions(-)

diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp @@ -47,6 +47,7 @@ %struct PAIR { car cdr } # .SIZE = 16 %struct SYMENT { name_ptr name_len global_val pad } # .SIZE = 32 %struct PRIM { hdr entry_w } # .SIZE = 16 +%struct CLOSURE { hdr params body env } # .SIZE = 32 # BSS arena offsets from :ELF_end. Each arena is 64 KiB; the three are # packed back-to-back below ELF_end + 192 KiB. p1_main's startup loop @@ -633,6 +634,23 @@ %eret ::sym + # Walk the env alist. Each cell is ((sym . val) . rest). On hit, + # return cdr(binding); on NIL, fall back to the symbol's global slot. + # a0 still holds the tagged sym; a1 still holds env. + ::env_walk + %li(t0, %imm_val(%IMM.NIL)) + %beq(a1, t0, &::env_miss) + %car(t1, a1) ; t1 = (sym . val) + %car(t2, t1) ; t2 = sym in binding + %beq(t2, a0, &::env_hit) + %cdr(a1, a1) + %b(&::env_walk) + + ::env_hit + %cdr(a0, t1) + %eret + + ::env_miss %untag_sym(a0, a0) %call(&sym_global) %li(t0, %imm_val(%IMM.UNBOUND)) @@ -651,6 +669,9 @@ %la(t1, &sym_if) %ld(t1, t1, 0) %beq(t0, t1, &::do_if) + %la(t1, &sym_lambda) + %ld(t1, t1, 0) + %beq(t0, t1, &::do_lambda) # head = eval(car(expr), env) %ld(a0, sp, 0) @@ -675,6 +696,12 @@ %cdr(a0, a0) %ld(a1, sp, 8) %tail(&eval_if) + + ::do_lambda + %ld(a0, sp, 0) + %cdr(a0, a0) + %ld(a1, sp, 8) + %tail(&eval_lambda) }) # eval_args(args=a0, env=a1) -> evaluated args list (cons-built). @@ -711,11 +738,11 @@ # # Frame: 16 bytes # +0 args +# +8 body (saved across bind_params for the closure path) %fn(apply, 16, { %st(a1, sp, 0) - # Only HEAP-tagged values can be applicable. PRIM is the only header - # type wired up here. + # Only HEAP-tagged values can be applicable. %tagof(t0, a0) %li(t1, %TAG.HEAP) %bne(t0, t1, &::not_proc) @@ -723,6 +750,8 @@ %hdr_type(t0, a0) %li(t1, %HDR.PRIM) %beq(t0, t1, &::prim) + %li(t1, %HDR.CLOSURE) + %beq(t0, t1, &::closure) ::not_proc %die(msg_not_proc, 8) @@ -731,6 +760,24 @@ %ld(t0, a0, 5) ; t0 = entry word (offset = -3 + 8) %ld(a0, sp, 0) ; args list -> a0 %tailr(t0) + + ::closure + # Closure layout (HEAP-tagged): [hdr][params][body][env] + # field offsets from tagged ptr: params=5, body=13, env=21. + %ld(t1, a0, 13) ; body (must survive bind_params) + %st(t1, sp, 8) + %ld(t2, a0, 21) ; captured env + %ld(t0, a0, 5) ; params + + %mov(a0, t0) ; bind_params(params, args, env) + %ld(a1, sp, 0) + %mov(a2, t2) + %call(&bind_params) + + # eval_body(body, new_env) -- tail call + %mov(a1, a0) + %ld(a0, sp, 8) + %tail(&eval_body) }) # ========================================================================= @@ -749,6 +796,12 @@ %call(&intern) %la(t0, &sym_if) %st(a0, t0, 0) + + %la(a0, &name_lambda) + %li(a1, 6) + %call(&intern) + %la(t0, &sym_lambda) + %st(a0, t0, 0) }) # eval_if(rest=a0, env=a1) -> value (a0). `rest` is (test then else). @@ -785,6 +838,116 @@ %tail(&eval) }) +# eval_lambda(rest=a0, env=a1) -> closure (a0). +# rest is (params . body). Allocates a 32-byte CLOSURE on the heap +# and stores params, body, and the captured env directly. +# +# Frame: 24 bytes +# +0 rest +# +8 env +# +16 closure ptr (HEAP-tagged) +%fn(eval_lambda, 24, { + %st(a0, sp, 0) + %st(a1, sp, 8) + + %li(a0, 32) + %li(a1, %HDR.CLOSURE) + %call(&alloc_hdr) + %st(a0, sp, 16) + + # closure[params] = car(rest) + %ld(t0, sp, 0) + %car(t1, t0) + %ld(t0, sp, 16) + %st(t1, t0, 5) + + # closure[body] = cdr(rest) + %ld(t1, sp, 0) + %cdr(t1, t1) + %st(t1, t0, 13) + + # closure[env] = captured env + %ld(t1, sp, 8) + %st(t1, t0, 21) + + %ld(a0, sp, 16) +}) + +# bind_params(params=a0, args=a1, env=a2) -> extended env (a0). +# Walks params and args in lockstep, prepending (param . arg) to env. +# Fixed-arity only for now; variadic `.`-tail is a follow-up. +# +# Frame: 24 bytes +# +0 params (advanced each iteration) +# +8 args (advanced each iteration) +# +16 env (extended each iteration) +%fn(bind_params, 24, { + %st(a0, sp, 0) + %st(a1, sp, 8) + %st(a2, sp, 16) + + ::loop + %ld(t0, sp, 0) + %li(t1, %imm_val(%IMM.NIL)) + %beq(t0, t1, &::done) + + # binding = cons(car(params), car(args)) + %ld(t0, sp, 0) + %car(a0, t0) + %ld(t0, sp, 8) + %car(a1, t0) + %call(&cons) + + # env = cons(binding, env) + %ld(a1, sp, 16) + %call(&cons) + %st(a0, sp, 16) + + # advance params and args + %ld(t0, sp, 0) + %cdr(t0, t0) + %st(t0, sp, 0) + %ld(t0, sp, 8) + %cdr(t0, t0) + %st(t0, sp, 8) + %b(&::loop) + + ::done + %ld(a0, sp, 16) +}) + +# eval_body(body=a0, env=a1) -> value of last form (a0). +# Evaluates each non-last form for effect; tail-evaluates the last so +# that closures used in tail position do not grow the host stack. +# +# Frame: 16 bytes +# +0 body +# +8 env +%fn(eval_body, 16, { + ::loop + %st(a0, sp, 0) + %st(a1, sp, 8) + + # If cdr(body) is NIL, body's car is the last form. + %cdr(t0, a0) + %li(t1, %imm_val(%IMM.NIL)) + %beq(t0, t1, &::last) + + # Non-last form: eval and discard, advance. + %car(a0, a0) + %call(&eval) + %ld(a0, sp, 0) + %cdr(a0, a0) + %ld(a1, sp, 8) + %b(&::loop) + + ::last + %ld(a0, sp, 0) + %car(a0, a0) + %ld(a1, sp, 8) + %tail(&eval) +}) + # ========================================================================= # Primitives # ========================================================================= @@ -839,6 +1002,7 @@ # because intern takes (ptr, len). Aligned padding via "\0" bytes is # fine -- M0 emits ASCII verbatim. :name_if "if" +:name_lambda "lambda" :name_sys_exit "sys-exit" :msg_usage "scheme1: usage: scheme1 SOURCE.scm" '0a' '00' @@ -880,6 +1044,7 @@ # Cached tagged-symbol values for special forms (filled by # intern_special_forms at startup). :sym_if $(0) +:sym_lambda $(0) # Pointer slots for the past-:ELF_end arenas. :readbuf_buf_ptr $(0) diff --git a/tests/scheme1/03-lambda-call.expected-exit b/tests/scheme1/03-lambda-call.expected-exit @@ -0,0 +1 @@ +42 diff --git a/tests/scheme1/03-lambda-call.scm b/tests/scheme1/03-lambda-call.scm @@ -0,0 +1 @@ +((lambda (x) (sys-exit x)) 42) diff --git a/tests/scheme1/04-lambda-multi.expected-exit b/tests/scheme1/04-lambda-multi.expected-exit @@ -0,0 +1 @@ +42 diff --git a/tests/scheme1/04-lambda-multi.scm b/tests/scheme1/04-lambda-multi.scm @@ -0,0 +1 @@ +((lambda (a b) (sys-exit b)) 1 42) diff --git a/tests/scheme1/05-lambda-capture.expected-exit b/tests/scheme1/05-lambda-capture.expected-exit @@ -0,0 +1 @@ +42 diff --git a/tests/scheme1/05-lambda-capture.scm b/tests/scheme1/05-lambda-capture.scm @@ -0,0 +1 @@ +((lambda (x) ((lambda (y) (sys-exit x)) 99)) 42)