boot2

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

commit 37f3b7510b05967ff92516f2ba46c7e9a5d70bf1
parent 0da31f6ffbb178470961fc0de5a9cfe634c508e3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 25 Apr 2026 08:14:31 -0700

Add scheme1 top-level define and read-eval loop

p1_main now iterates skip_ws + parse_one + eval until EOF, so multi-form
scripts work. eval_define handles both `(define name value)` and the
`(define (f a b) body...)` lambda-sugar by building (params . body) and
delegating to eval_lambda. The new value or closure is stored in the
symbol's global slot via sym_set_global; the form returns UNSPEC.

Diffstat:
Mscheme1/scheme1.P1pp | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Atests/scheme1/09-define-value.expected-exit | 1+
Atests/scheme1/09-define-value.scm | 3+++
Atests/scheme1/10-define-fn.expected-exit | 1+
Atests/scheme1/10-define-fn.scm | 4++++
Atests/scheme1/11-define-mutual.expected-exit | 1+
Atests/scheme1/11-define-mutual.scm | 6++++++
7 files changed, 101 insertions(+), 5 deletions(-)

diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp @@ -185,15 +185,21 @@ %ld(a0, a1, 8) %call(&load_source) - # parse_one returns one s-expression in a0. + # Top-level read-eval loop. Each iteration: skip ws+comments, stop + # at EOF, otherwise parse one form and eval it under NIL. + ::repl + %call(&skip_ws) + %la(t0, &readbuf_pos) + %ld(t0, t0, 0) + %la(t1, &readbuf_len) + %ld(t1, t1, 0) + %beq(t0, t1, &::done) %call(&parse_one) - - # eval(expr, NIL). %li(a1, %imm_val(%IMM.NIL)) %call(&eval) + %b(&::repl) - # If the form did not sys-exit (e.g. evaluated to a fixnum), drop - # cleanly with status 0. Useful while wiring up more of the runtime. + ::done %li(a0, 0) %eret @@ -744,6 +750,9 @@ %la(t1, &sym_lambda) %ld(t1, t1, 0) %beq(t0, t1, &::do_lambda) + %la(t1, &sym_define) + %ld(t1, t1, 0) + %beq(t0, t1, &::do_define) # head = eval(car(expr), env) %ld(a0, sp, 0) @@ -781,6 +790,12 @@ %cdr(a0, a0) %ld(a1, sp, 8) %tail(&eval_lambda) + + ::do_define + %ld(a0, sp, 0) + %cdr(a0, a0) + %ld(a1, sp, 8) + %tail(&eval_define) }) # eval_args(args=a0, env=a1) -> evaluated args list (cons-built). @@ -887,6 +902,12 @@ %call(&intern) %la(t0, &sym_lambda) %st(a0, t0, 0) + + %la(a0, &name_define) + %li(a1, 6) + %call(&intern) + %la(t0, &sym_define) + %st(a0, t0, 0) }) # eval_if(rest=a0, env=a1) -> value (a0). `rest` is (test then else). @@ -958,6 +979,63 @@ %ld(a0, sp, 16) }) +# eval_define(rest=a0, env=a1) -> UNSPEC (a0). +# Top-level only. Two surface forms: +# (define <sym> <expr>) ; head of rest is a SYM +# (define (<sym> . <params>) . body) ; head of rest is a PAIR; sugar for +# (define <sym> (lambda <params> . body)) +# Inner `define` is the responsibility of eval_body and never reaches here. +# +# Frame: 16 bytes +# +0 rest +# +8 env +%fn(eval_define, 16, { + %st(a0, sp, 0) + %st(a1, sp, 8) + + # If car(rest) is a pair, this is the lambda-sugar form. + %car(t0, a0) + %tagof(t1, t0) + %li(t2, %TAG.PAIR) + %beq(t1, t2, &::sugar) + + # Plain define: value = eval(car(cdr(rest)), env) + %ld(t0, sp, 0) + %cdr(a0, t0) + %car(a0, a0) + %ld(a1, sp, 8) + %call(&eval) + + %ld(t0, sp, 0) + %car(t0, t0) + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + %li(a0, %imm_val(%IMM.UNSPEC)) + %eret + + ::sugar + # rest = ((name . params) . body); build (params . body) for eval_lambda. + %ld(t0, sp, 0) + %car(t0, t0) + %cdr(a0, t0) ; params + %ld(t0, sp, 0) + %cdr(a1, t0) ; body + %call(&cons) + %ld(a1, sp, 8) + %call(&eval_lambda) + + %ld(t0, sp, 0) + %car(t0, t0) + %car(t0, t0) ; name + %untag_sym(t0, t0) + %mov(a1, a0) + %mov(a0, t0) + %call(&sym_set_global) + %li(a0, %imm_val(%IMM.UNSPEC)) +}) + # 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. @@ -1089,6 +1167,7 @@ :name_quote "quote" :name_if "if" :name_lambda "lambda" +:name_define "define" :name_sys_exit "sys-exit" :msg_usage "scheme1: usage: scheme1 SOURCE.scm" '0a' '00' @@ -1132,6 +1211,7 @@ :sym_quote $(0) :sym_if $(0) :sym_lambda $(0) +:sym_define $(0) # Pointer slots for the past-:ELF_end arenas. :readbuf_buf_ptr $(0) diff --git a/tests/scheme1/09-define-value.expected-exit b/tests/scheme1/09-define-value.expected-exit @@ -0,0 +1 @@ +42 diff --git a/tests/scheme1/09-define-value.scm b/tests/scheme1/09-define-value.scm @@ -0,0 +1,3 @@ +; Plain define: (define name value). +(define x 42) +(sys-exit x) diff --git a/tests/scheme1/10-define-fn.expected-exit b/tests/scheme1/10-define-fn.expected-exit @@ -0,0 +1 @@ +55 diff --git a/tests/scheme1/10-define-fn.scm b/tests/scheme1/10-define-fn.scm @@ -0,0 +1,4 @@ +; define-fn sugar: (define (f x y) body). +(define (id x) x) +(define (k x y) x) +(sys-exit (id (k 55 99))) diff --git a/tests/scheme1/11-define-mutual.expected-exit b/tests/scheme1/11-define-mutual.expected-exit @@ -0,0 +1 @@ +31 diff --git a/tests/scheme1/11-define-mutual.scm b/tests/scheme1/11-define-mutual.scm @@ -0,0 +1,6 @@ +; Forward references at top level: `a` calls `b` defined later. Works +; because env lookup falls through to symtab[idx].global_val at call +; time, not closure-creation time. +(define (a x) (b x)) +(define (b x) x) +(sys-exit (a 31))