commit 4b16290a81d50db583b30d19f710fb515bc51465
parent d1669e299cb2cdd93ca6cb11bc70b22e29b9c915
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 25 Apr 2026 09:49:38 -0700
Add scheme1 embedded prelude
p1_main now calls eval_prelude before load_source. eval_prelude memcpy's
the embedded prelude bytes into readbuf and runs the standard read-eval
loop. The prelude defines list, length, reverse, append, list-ref, map,
for-each in plain Scheme.
Two correctness fixes were required to make this work:
1. intern now copies each new name into a stable heap buffer (via
alloc_bytes + memcpy) before storing it in the symtab entry.
parse_atom passes pointers into readbuf_buf; load_source overwrites
readbuf when it slurps the user file, so symtab entries created from
the prelude would otherwise have stale name_ptr's and be unreachable
on subsequent intern lookups.
2. load_source resets readbuf_pos to 0 in addition to writing
readbuf_len. Without that the user-file read loop continues from
wherever the prelude left off in the buffer.
skip_ws now treats NUL as whitespace because M0 auto-NUL-terminates
each "..." literal in the prelude block; that yields one stray NUL
between consecutive lines that the parser would otherwise treat as
junk.
Diffstat:
3 files changed, 96 insertions(+), 4 deletions(-)
diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp
@@ -220,6 +220,9 @@
%call(&intern_special_forms)
%call(®ister_primitives)
+ # Evaluate the embedded prelude before the user source.
+ %call(&eval_prelude)
+
# argv[1] is the source path (NUL-terminated cstr from the kernel).
%ld(a1, sp, 0)
%ld(a0, a1, 8)
@@ -263,12 +266,49 @@
%la(t0, &readbuf_len)
%st(a0, t0, 0)
%li(a0, 0)
+ %la(t0, &readbuf_pos)
+ %st(a0, t0, 0)
%eret
::fail
%die(msg_load_fail, 3)
})
+# eval_prelude -- copy the embedded prelude bytes into readbuf, run the
+# read-eval loop until the prelude is exhausted, then return so
+# load_source can later refill readbuf with the user file.
+%fn(eval_prelude, 0, {
+ %la(a1, &prelude_src)
+ %la(a2, &prelude_src_end)
+ %sub(a2, a2, a1)
+ %la(t0, &readbuf_buf_ptr)
+ %ld(a0, t0, 0)
+ %call(&memcpy)
+
+ %la(t0, &prelude_src)
+ %la(t1, &prelude_src_end)
+ %sub(t0, t1, t0)
+ %la(t1, &readbuf_len)
+ %st(t0, t1, 0)
+ %li(t0, 0)
+ %la(t1, &readbuf_pos)
+ %st(t0, t1, 0)
+
+ ::loop
+ %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)
+ %li(a1, %imm_val(%IMM.NIL))
+ %call(&eval)
+ %b(&::loop)
+
+ ::done
+})
+
# =========================================================================
# Heap: cons (leaf) and alloc_hdr (leaf)
# =========================================================================
@@ -356,9 +396,18 @@
%die(msg_symtab_full, 5)
::append_ok
+ # Copy the name into a stable heap buffer. The caller-provided ptr
+ # may live in readbuf_buf (parse_atom), which gets overwritten when
+ # the next source is loaded; symtab entries must outlive that.
+ %ld(a0, sp, 8)
+ %call(&alloc_bytes)
+ %ld(a1, sp, 0)
+ %ld(a2, sp, 8)
+ %call(&memcpy) ; returns dst in a0 = stable copy
+
+ %ld(t0, sp, 16)
%symtab_entry(t1, t0, t2)
- %ld(a0, sp, 0)
- %st(a0, t1, 0)
+ %st(a0, t1, 0) ; entry.name_ptr = stable copy
%ld(a0, sp, 8)
%st(a0, t1, 8)
%li(a0, %imm_val(%IMM.UNBOUND))
@@ -406,7 +455,9 @@
# The reader is called recursively from parse_list, so every state goes
# through frame slots, not s-registers.
-# Skip whitespace (ASCII 32, 9, 10, 13) and `;`-to-LF comments. Leaf.
+# Skip whitespace (ASCII 32, 9, 10, 13), NUL bytes (M0 auto-terminates
+# string literals, so the embedded prelude has NULs we want to ignore),
+# and `;`-to-LF comments. Leaf.
:skip_ws
%scope skip_ws
%la(t2, &readbuf_pos)
@@ -417,7 +468,8 @@
%beq(t0, t1, &::done)
%readbuf_byte(a0, t0)
%is_ws_branch(a1, a0, &::step)
- %addi(a1, a0, -59) ; ';'
+ %beqz(a0, &::step) ; NUL
+ %addi(a1, a0, -59) ; ';'
%beqz(a1, &::comment)
%b(&::done)
::comment
@@ -2936,6 +2988,32 @@
&name_eof_objectq %(0) $(11) &prim_eof_objectq_entry %(0)
:prim_table_end
+# Embedded Scheme prelude. Length = prelude_src_end - prelude_src,
+# computed at startup. Each form is parsed and evaluated under the
+# global env before the user file runs, so the user's source can
+# rely on these helpers without re-defining them.
+:prelude_src
+"(define (list . xs) xs)" '0a'
+"(define (length xs)" '0a'
+" (let loop ((xs xs) (n 0))" '0a'
+" (if (null? xs) n (loop (cdr xs) (+ n 1)))))" '0a'
+"(define (reverse xs)" '0a'
+" (let loop ((xs xs) (acc (quote ())))" '0a'
+" (if (null? xs) acc (loop (cdr xs) (cons (car xs) acc)))))" '0a'
+"(define (append-pair a b)" '0a'
+" (if (null? a) b (cons (car a) (append-pair (cdr a) b))))" '0a'
+"(define (append . lists)" '0a'
+" (cond ((null? lists) (quote ()))" '0a'
+" ((null? (cdr lists)) (car lists))" '0a'
+" (else (append-pair (car lists) (apply append (cdr lists))))))" '0a'
+"(define (list-ref xs n)" '0a'
+" (if (= n 0) (car xs) (list-ref (cdr xs) (- n 1))))" '0a'
+"(define (map f xs)" '0a'
+" (if (null? xs) (quote ()) (cons (f (car xs)) (map f (cdr xs)))))" '0a'
+"(define (for-each f xs)" '0a'
+" (if (null? xs) (quote ()) (begin (f (car xs)) (for-each f (cdr xs)))))" '0a'
+:prelude_src_end
+
:msg_usage "scheme1: usage: scheme1 SOURCE.scm" '0a' '00'
:msg_load_fail "scheme1: failed to read source" '0a' '00'
:msg_symtab_full "scheme1: symbol table full" '0a' '00'
diff --git a/tests/scheme1/43-prelude.expected-exit b/tests/scheme1/43-prelude.expected-exit
@@ -0,0 +1 @@
+19
diff --git a/tests/scheme1/43-prelude.scm b/tests/scheme1/43-prelude.scm
@@ -0,0 +1,13 @@
+; Verify the full embedded prelude: list, length, reverse, append,
+; list-ref, map, for-each are all in scope before user code.
+(define xs (list 7 8 9))
+(define ys (append xs (list 10))) ; (7 8 9 10)
+(define rev (reverse ys)) ; (10 9 8 7)
+(define mapped (map (lambda (n) (- n 5)) ys)) ; (2 3 4 5)
+; for-each is side-effecting; just verify it runs without error by
+; consuming a list with a no-op lambda.
+(for-each (lambda (x) x) ys)
+(sys-exit (+ (length ys)
+ (+ (list-ref rev 0)
+ (list-ref mapped 3))))
+; length=4, list-ref rev 0 = 10, list-ref mapped 3 = 5. 4+10+5 = 19