boot2

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

commit 4b41d120da4d603fd18c7a860f4acdf5af3acfe2
parent 8e6c12f7c9592d655124146d115a456b531a6b9a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 21 Apr 2026 17:26:38 -0700

lisp.M1 steps 11-12: reader shorthand + set!/let/letrec/cond/quasiquote

Step 11 (reader): quote/quasiquote/unquote/unquote-splicing shorthand,
#\char literals (incl. space/newline/tab), #(...) vector literals,
improper dotted lists, 0x hex and negative fixnum literals.

Step 12 (eval): set! (local-first, global-fallback), let/let*/letrec
with two-pass pre-bind for mutual recursion, cond with else,
quasiquote/unquote/unquote-splicing, and inner-define rewriting into
letrec-shape at lambda-body scope.

Tests cover distinct code paths: local vs. global set!, mutually
recursive letrec (even?/odd?), and a recursive inner define that
requires true letrec semantics.

Diffstat:
Msrc/lisp.M1 | 1568++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Atests/lisp/20-quote.expected | 1+
Atests/lisp/20-quote.scm | 11+++++++++++
Atests/lisp/21-neg-hex.expected | 1+
Atests/lisp/21-neg-hex.scm | 7+++++++
Atests/lisp/22-char.expected | 1+
Atests/lisp/22-char.scm | 8++++++++
Atests/lisp/23-vector.expected | 1+
Atests/lisp/23-vector.scm | 8++++++++
Atests/lisp/24-dotted.expected | 1+
Atests/lisp/24-dotted.scm | 7+++++++
Atests/lisp/25-set.expected | 1+
Atests/lisp/25-set.scm | 8++++++++
Atests/lisp/26-let.expected | 1+
Atests/lisp/26-let.scm | 15+++++++++++++++
Atests/lisp/27-cond.expected | 1+
Atests/lisp/27-cond.scm | 10++++++++++
Atests/lisp/28-quasi.expected | 1+
Atests/lisp/28-quasi.scm | 6++++++
Atests/lisp/29-innerdef.expected | 1+
Atests/lisp/29-innerdef.scm | 17+++++++++++++++++
21 files changed, 1552 insertions(+), 123 deletions(-)

diff --git a/src/lisp.M1 b/src/lisp.M1 @@ -111,9 +111,9 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ## Publish src_base/src_len so the reader can walk the source, ## and park (src_buf, bytes_read) in callee-saved r6/r7 so the - ## tuple survives primitive registration + prelude eval down to - ## the final eval_source CALL below. Capture r0 into r7 NOW — - ## sys_close below will overwrite r0 with its own return value. + ## tuple survives primitive registration down to the eval_source + ## CALL below. Capture r0 into r7 NOW — sys_close below will + ## overwrite r0 with its own return value. mov_r7,r0 ## r7 = bytes_read (captured pre-close) li_r6 &src_buf ## r6 = &src_buf li_r1 &src_len @@ -174,6 +174,69 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_r2 &sym_define st_r0,r2,0 + li_r1 &str_quasiquote + li_r2 %10 + li_br &intern + call + li_r2 &sym_quasiquote + st_r0,r2,0 + + li_r1 &str_unquote + li_r2 %7 + li_br &intern + call + li_r2 &sym_unquote + st_r0,r2,0 + + li_r1 &str_unquote_splicing + li_r2 %16 + li_br &intern + call + li_r2 &sym_unquote_splicing + st_r0,r2,0 + + li_r1 &str_set + li_r2 %4 + li_br &intern + call + li_r2 &sym_set + st_r0,r2,0 + + li_r1 &str_let + li_r2 %3 + li_br &intern + call + li_r2 &sym_let + st_r0,r2,0 + + li_r1 &str_letstar + li_r2 %4 + li_br &intern + call + li_r2 &sym_letstar + st_r0,r2,0 + + li_r1 &str_letrec + li_r2 %6 + li_br &intern + call + li_r2 &sym_letrec + st_r0,r2,0 + + li_r1 &str_cond + li_r2 %4 + li_br &intern + call + li_r2 &sym_cond + st_r0,r2,0 + + li_r1 &str_else + li_r2 %4 + li_br &intern + call + li_r2 &sym_else + st_r0,r2,0 + ## Register primitives (LISP.md step 10b). Walk prim_table with ## r4 = cursor, r5 = saved sym across make_primitive. Entry is ## 40 bytes; zero name pointer ends the loop. @@ -203,15 +266,9 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &_start_reg_prim_loop b :_start_reg_prim_done - ## Evaluate the embedded Lisp prelude (map/filter/fold) so user - ## scripts see those bindings. - li_r1 &prelude_src - li_r2 &prelude_src_end - sub_r2,r2,r1 - li_br &eval_source - call - - ## Evaluate the script read from argv[1]. + ## Evaluate the script read from argv[1]. The Makefile cats + ## src/prelude.scm ahead of the user script before invoking us, + ## so map/filter/fold are in scope by the time user code runs. mov_r1,r6 mov_r2,r7 li_br &eval_source @@ -972,6 +1029,28 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ret +## ---- peek2_char() -> r0 = char at cursor+1 (0..255) or -1 on EOF ---- +## Used by the reader to distinguish `,` vs `,@`, `-` vs `-<digit>`, and +## `0` vs `0x`/`0X` without consuming input. +:peek2_char + li_r1 &src_cursor + ld_r1,r1,0 + addi_r1,r1,1 ## r1 = cursor + 1 + li_r2 &src_len + ld_r2,r2,0 + li_br &peek2_char_inb + blt_r1,r2 + li_r0 %0 + addi_r0,r0,neg1 + ret +:peek2_char_inb + li_r2 &src_base + ld_r2,r2,0 + add_r2,r2,r1 + lb_r0,r2,0 + ret + + ## ---- advance_char() — consume current char, track line/col -------- :advance_char prologue @@ -1021,13 +1100,6 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &skip_ws_done beq_r0,r1 - ## NUL (0x00) — treated as whitespace so adjacent M1 "..." chunks - ## (which each emit a trailing NUL) concatenate cleanly in the - ## embedded prelude. - li_r1 %0 - li_br &skip_ws_eat - beq_r0,r1 - ## ' ' (0x20) li_r1 %32 li_br &skip_ws_eat @@ -1103,9 +1175,10 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ## ---- read_number() -> r0 = tagged fixnum ---------------------------- -## Reads a non-negative decimal integer. Digits are consumed while -## peek_char is in [0-9]. Uses r6 = accumulator across iterations -## (saved via PROLOGUE_N1 slot 1). +## Reads a non-negative decimal integer, or a hex integer with `0x`/`0X` +## prefix. Digits are consumed while peek_char is in [0-9] (or [0-9a-fA-F] +## after the hex prefix). Uses r6 = accumulator across iterations (saved +## via PROLOGUE_N1 slot 1). :read_number prologue mov_r3,sp @@ -1113,6 +1186,52 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_r6 %0 ## r6 = 0 (accumulator) + ## Detect `0x` / `0X` hex prefix — only triggers if the very first + ## byte is '0' and the next is 'x' or 'X'. Anything else falls + ## through to the decimal digit loop (including bare `0` or `07`). + li_br &peek_char + call + li_r1 %48 ## '0' + li_br &read_number_loop + bne_r0,r1 + li_br &peek2_char + call + li_r1 %120 ## 'x' + li_br &read_number_hex_start + beq_r0,r1 + li_r1 %88 ## 'X' + li_br &read_number_hex_start + beq_r0,r1 + li_br &read_number_loop + b + +:read_number_hex_start + li_br &advance_char + call ## eat '0' + li_br &advance_char + call ## eat 'x' / 'X' + +:read_number_hex_loop + li_br &peek_char + call + mov_r1,r0 + li_br &hex_digit_val + call ## r0 = digit (0..15) or -1 + li_r1 %0 + addi_r1,r1,neg1 + li_br &read_number_done + beq_r0,r1 + + ## r6 = r6 * 16 + digit. ADDI_IMMS lacks 4-shift, so SHLI 2 twice. + shli_r6,r6,2 + shli_r6,r6,2 + add_r6,r6,r0 + + li_br &advance_char + call + li_br &read_number_hex_loop + b + :read_number_loop li_br &peek_char call ## r0 = char (or -1) @@ -1151,6 +1270,47 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ret +## ---- hex_digit_val(r1=c) -> r0 = digit in [0..15] or -1 ------------ +## Classifies one byte as a hex digit. Range-checks via BLT then does +## an arithmetic subtract: digits subtract '0', A-F subtract 55, a-f +## subtract 87. Non-hex chars (including EOF=-1) return -1. +:hex_digit_val + li_r2 %48 ## '0' + li_br &hex_dv_none + blt_r1,r2 + li_r2 %58 ## ':' (one past '9') + li_br &hex_dv_dec + blt_r1,r2 + li_r2 %65 ## 'A' + li_br &hex_dv_none + blt_r1,r2 + li_r2 %71 ## 'G' (one past 'F') + li_br &hex_dv_upper + blt_r1,r2 + li_r2 %97 ## 'a' + li_br &hex_dv_none + blt_r1,r2 + li_r2 %103 ## 'g' (one past 'f') + li_br &hex_dv_lower + blt_r1,r2 +:hex_dv_none + li_r0 %0 + addi_r0,r0,neg1 + ret +:hex_dv_dec + mov_r0,r1 + addi_r0,r0,neg48 + ret +:hex_dv_upper + li_r0 %55 ## 'A' - 10 = 55 + sub_r0,r1,r0 + ret +:hex_dv_lower + li_r0 %87 ## 'a' - 10 = 87 + sub_r0,r1,r0 + ret + + ## ---- is_delim(c) -> r0 = 0 or 1 ------------------------------------ ## Returns 1 if c is a token delimiter: whitespace, '(', ')', ';', or EOF. :is_delim @@ -1372,9 +1532,13 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &err_reader_bad beq_r0,r1 - ## Read head element, then tail, then cons them. - st_r6,sp,8 ## save outer r6 (if any) + ## '.' followed by a delimiter → dotted tail. `.<alpha>` still + ## parses as a regular symbol (fall-through to read_expr). + li_r1 %46 ## '.' + li_br &read_list_maybe_dotted + beq_r0,r1 + ## Read head element, then tail, then cons them. li_br &read_expr call ## r0 = head @@ -1389,9 +1553,50 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &cons call - ## Restore r6 (we clobbered the slot when storing head; outer r6 is gone. - ## In this test we never rely on r6 across read_list invocations, so - ## leave it; slot is scratch). + epilogue + ret + +:read_list_maybe_dotted + li_br &peek2_char + call + mov_r1,r0 + li_br &is_delim + call + li_r1 %1 + li_br &read_list_dotted + beq_r0,r1 + + ## Bare `.` followed by non-delim → fall back to regular item path. + li_br &read_expr + call + st_r0,sp,8 + li_br &read_list + call + ld_r1,sp,8 + mov_r2,r0 + li_br &cons + call + epilogue + ret + +:read_list_dotted + li_br &advance_char + call ## eat '.' + li_br &skip_ws + call + li_br &read_expr + call ## r0 = dotted tail expr + st_r0,sp,8 + li_br &skip_ws + call + li_br &peek_char + call + li_r1 %41 ## ')' + li_br &err_reader_bad + bne_r0,r1 + li_br &advance_char + call ## eat ')' + ld_r0,sp,8 epilogue ret @@ -1430,11 +1635,32 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &read_expr_string beq_r0,r1 - ## '#' → hash-prefix literal (#t, #f for now). + ## '#' → hash-prefix literal (#t, #f, #\char, #(...)). li_r1 %35 li_br &read_expr_hash beq_r0,r1 + ## '\'' → (quote x) + li_r1 %39 + li_br &read_expr_quote + beq_r0,r1 + + ## '`' → (quasiquote x) + li_r1 %96 + li_br &read_expr_quasiquote + beq_r0,r1 + + ## ',' → (unquote x) or (unquote-splicing x) if followed by '@' + li_r1 %44 + li_br &read_expr_unquote + beq_r0,r1 + + ## '-' followed by a digit → negative fixnum literal. Plain '-' + ## still reads as a symbol (the subtraction primitive). + li_r1 %45 + li_br &read_expr_maybe_neg + beq_r0,r1 + ## digit → number. mov_r1,r0 li_br &is_digit @@ -1469,8 +1695,8 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' epilogue ret -## Hash-prefix literals: `#t` → 0x0F, `#f` → 0x17. Inherits read_expr's -## PROLOGUE (no new frame). Extended syntax (#\char, #(…)) is step 11. +## Hash-prefix literals: `#t`/`#f` singletons, `#\char` ASCII fixnum, +## `#(...)` vector literal. Inherits read_expr's PROLOGUE. :read_expr_hash li_br &advance_char call ## eat '#' @@ -1483,6 +1709,12 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_r1 %102 ## 'f' li_br &read_expr_hash_f beq_r0,r1 + li_r1 %92 ## '\\' + li_br &read_expr_hash_char + beq_r0,r1 + li_r1 %40 ## '(' + li_br &read_expr_hash_vector + beq_r0,r1 li_br &err_reader_bad b @@ -1501,65 +1733,398 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' epilogue ret +:read_expr_hash_char + li_br &read_char_literal + call + epilogue + ret -## ---- Display -------------------------------------------------------- +:read_expr_hash_vector + li_br &read_vector_literal + call + epilogue + ret -## ---- putc(r1=char) — write one byte to fd 1 ------------------------ -:putc_buf %0 %0 -:putc - prologue - li_r2 &putc_buf - sb_r1,r2,0 ## *putc_buf = low byte of r1 - li_r0 sys_write - li_r1 %1 - ## r2 = &putc_buf already - li_r3 %1 - syscall +## ---- read_expr_quote / quasiquote / unquote / maybe_neg ------------- +## All inherit read_expr's PROLOGUE; each wraps the downstream value and +## ret/epilogue out. +:read_expr_quote + li_br &advance_char + call ## eat '\'' + li_r1 &sym_quote + ld_r1,r1,0 + li_br &read_quoted_wrap + call epilogue ret +:read_expr_quasiquote + li_br &advance_char + call ## eat '`' + li_r1 &sym_quasiquote + ld_r1,r1,0 + li_br &read_quoted_wrap + call + epilogue + ret -## ---- display_uint(r1=u64, r2=fd) — decimal, no sign -------------- -## Writes digits to `digit_buf` right-to-left, then SYS_WRITEs the -## filled range to fd. 24-byte buffer covers any 61-bit value. -## fd is spilled into slot 3 because the digit loop reuses r2 as the -## divisor '10' constant — keeping fd in a reg would require saving -## another callee-saved, which costs the same. -:digit_buf '000000000000000000000000000000000000000000000000' -:digit_buf_end %0 +:read_expr_unquote + li_br &advance_char + call ## eat ',' + li_br &peek_char + call + li_r1 %64 ## '@' + li_br &read_expr_unquote_splicing + beq_r0,r1 + li_r1 &sym_unquote + ld_r1,r1,0 + li_br &read_quoted_wrap + call + epilogue + ret -:display_uint - prologue_n3 - st_r6,sp,8 - st_r7,sp,16 - st_r2,sp,24 ## save fd +:read_expr_unquote_splicing + li_br &advance_char + call ## eat '@' + li_r1 &sym_unquote_splicing + ld_r1,r1,0 + li_br &read_quoted_wrap + call + epilogue + ret - li_r6 &digit_buf_end ## r6 = end-of-buffer cursor (moves left) - mov_r7,r1 ## r7 = value (mutated) +:read_expr_maybe_neg + li_br &peek2_char + call + mov_r1,r0 + li_br &is_digit + call + li_r1 %1 + li_br &read_expr_negnum + beq_r0,r1 + ## Not a digit after '-' → treat as a regular symbol (the '-' is + ## the sub primitive, or a longer ident like `-ident`). + li_br &read_symbol + call + epilogue + ret - ## Special-case zero. - li_br &du_loop - bnez_r7 ## if value != 0, loop; else write '0' - addi_r6,r6,neg1 - li_r1 %48 ## '0' - sb_r1,r6,0 - li_br &du_write - b +:read_expr_negnum + li_br &advance_char + call ## eat '-' + li_br &read_number + call ## r0 = tagged positive number + sari_r0,r0,3 ## decode + li_r1 %0 + sub_r0,r1,r0 ## r0 = -value + shli_r0,r0,3 + ori_r0,r0,1 ## retag + epilogue + ret -:du_loop - li_br &du_write - beqz_r7 ## value == 0 → done digit gen - ## digit = value % 10. - mov_r1,r7 - li_r2 %10 - rem_r1,r1,r2 ## r1 = value % 10 - addi_r1,r1,48 ## r1 += '0' - addi_r6,r6,neg1 - sb_r1,r6,0 ## *--r6 = digit +## ---- read_quoted_wrap(r1=tagged_sym) -> r0 = (sym expr) ------------- +## Reads one expression and returns (sym . (expr . nil)). Shared by the +## four reader shorthand forms (quote/quasi/unquote/unquote-splicing). +:read_quoted_wrap + prologue + st_r1,sp,8 ## save tagged sym + li_br &read_expr + call ## r0 = expr + mov_r1,r0 + li_r2 NIL + li_br &cons + call ## r0 = (expr . nil) + mov_r2,r0 + ld_r1,sp,8 + li_br &cons ## r0 = (sym . (expr . nil)) + tail - ## value = value / 10. + +## ---- read_char_literal() -> r0 = tagged fixnum (ASCII) ------------- +## `\` already at cursor. Consumes `\`, then one or more chars. If the +## first char is followed by a delimiter, that char is the result. If +## more chars follow, the full span is matched against `space`, `newline`, +## or `tab`; anything else errors out. +:read_char_literal + prologue_n4 + li_br &advance_char + call ## eat '\\' + + li_r1 &src_cursor + ld_r0,r1,0 + st_r0,sp,8 ## slot 1 = start cursor + + li_br &peek_char + call ## r0 = first char (must not be EOF) + li_r1 %0 + addi_r1,r1,neg1 + li_br &err_reader_bad + beq_r0,r1 + + st_r0,sp,16 ## slot 2 = first char + li_br &advance_char + call ## eat first char + + li_br &peek_char + call + mov_r1,r0 + li_br &is_delim + call + li_r1 %1 + li_br &rcl_single + beq_r0,r1 + +:rcl_name_loop + li_br &peek_char + call + mov_r1,r0 + li_br &is_delim + call + li_r1 %1 + li_br &rcl_compare + beq_r0,r1 + li_br &advance_char + call + li_br &rcl_name_loop + b + +:rcl_compare + ## r2 = len (cursor - start), r0 = ptr (base + start). + li_r1 &src_cursor + ld_r2,r1,0 + ld_r1,sp,8 + sub_r2,r2,r1 + li_r0 &src_base + ld_r0,r0,0 + add_r0,r0,r1 + li_r1 %5 + li_br &rcl_cmp5 + beq_r2,r1 + li_r1 %7 + li_br &rcl_cmp7 + beq_r2,r1 + li_r1 %3 + li_br &rcl_cmp3 + beq_r2,r1 + li_br &err_reader_bad + b + +:rcl_cmp5 + ## "space" → 32 + lb_r1,r0,0 + li_r2 %115 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %112 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %97 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %99 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %101 + li_br &err_reader_bad + bne_r1,r2 + li_r0 %32 + shli_r0,r0,3 + ori_r0,r0,1 + epilogue_n4 + ret + +:rcl_cmp7 + ## "newline" → 10 + lb_r1,r0,0 + li_r2 %110 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %101 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %119 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %108 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %105 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %110 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %101 + li_br &err_reader_bad + bne_r1,r2 + li_r0 %10 + shli_r0,r0,3 + ori_r0,r0,1 + epilogue_n4 + ret + +:rcl_cmp3 + ## "tab" → 9 + lb_r1,r0,0 + li_r2 %116 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %97 + li_br &err_reader_bad + bne_r1,r2 + addi_r0,r0,1 + lb_r1,r0,0 + li_r2 %98 + li_br &err_reader_bad + bne_r1,r2 + li_r0 %9 + shli_r0,r0,3 + ori_r0,r0,1 + epilogue_n4 + ret + +:rcl_single + ld_r0,sp,16 + shli_r0,r0,3 + ori_r0,r0,1 + epilogue_n4 + ret + + +## ---- read_vector_literal() -> r0 = tagged vector ------------------- +## `#(` detected; cursor at `(`. Read the body as a list, then convert +## element-by-element into a freshly-allocated vector. +:read_vector_literal + prologue_n3 + li_br &advance_char + call ## eat '(' + li_br &read_list + call ## r0 = tagged list + st_r0,sp,8 ## slot 1 = list head + + mov_r1,r0 + li_r2 %0 ## r2 = count +:rvl_count + li_r0 NIL + li_br &rvl_alloc + beq_r1,r0 + addi_r0,r1,neg2 + ld_r1,r0,8 + addi_r2,r2,1 + li_br &rvl_count + b + +:rvl_alloc + mov_r1,r2 + li_r2 NIL + li_br &make_vector + call ## r0 = tagged vector + st_r0,sp,16 ## slot 2 = vec + + addi_r3,r0,neg3 + addi_r3,r3,8 ## r3 = payload cursor + ld_r1,sp,8 + +:rvl_fill + li_r0 NIL + li_br &rvl_done + beq_r1,r0 + addi_r0,r1,neg2 + ld_r2,r0,0 + ld_r1,r0,8 + st_r2,r3,0 + addi_r3,r3,8 + li_br &rvl_fill + b + +:rvl_done + ld_r0,sp,16 + epilogue_n3 + ret + + +## ---- Display -------------------------------------------------------- + +## ---- putc(r1=char) — write one byte to fd 1 ------------------------ +:putc_buf %0 %0 + +:putc + prologue + li_r2 &putc_buf + sb_r1,r2,0 ## *putc_buf = low byte of r1 + li_r0 sys_write + li_r1 %1 + ## r2 = &putc_buf already + li_r3 %1 + syscall + epilogue + ret + + +## ---- display_uint(r1=u64, r2=fd) — decimal, no sign -------------- +## Writes digits to `digit_buf` right-to-left, then SYS_WRITEs the +## filled range to fd. 24-byte buffer covers any 61-bit value. +## fd is spilled into slot 3 because the digit loop reuses r2 as the +## divisor '10' constant — keeping fd in a reg would require saving +## another callee-saved, which costs the same. +:digit_buf '000000000000000000000000000000000000000000000000' +:digit_buf_end %0 + +:display_uint + prologue_n3 + st_r6,sp,8 + st_r7,sp,16 + st_r2,sp,24 ## save fd + + li_r6 &digit_buf_end ## r6 = end-of-buffer cursor (moves left) + mov_r7,r1 ## r7 = value (mutated) + + ## Special-case zero. + li_br &du_loop + bnez_r7 ## if value != 0, loop; else write '0' + addi_r6,r6,neg1 + li_r1 %48 ## '0' + sb_r1,r6,0 + li_br &du_write + b + +:du_loop + li_br &du_write + beqz_r7 ## value == 0 → done digit gen + + ## digit = value % 10. + mov_r1,r7 + li_r2 %10 + rem_r1,r1,r2 ## r1 = value % 10 + addi_r1,r1,48 ## r1 += '0' + addi_r6,r6,neg1 + sb_r1,r6,0 ## *--r6 = digit + + ## value = value / 10. mov_r1,r7 li_r2 %10 div_r1,r1,r2 @@ -4137,6 +4702,36 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' li_br &eval_define beq_r0,r1 + li_r1 &sym_set + ld_r1,r1,0 + li_br &eval_set + beq_r0,r1 + + li_r1 &sym_let + ld_r1,r1,0 + li_br &eval_let + beq_r0,r1 + + li_r1 &sym_letstar + ld_r1,r1,0 + li_br &eval_letstar + beq_r0,r1 + + li_r1 &sym_letrec + ld_r1,r1,0 + li_br &eval_letrec + beq_r0,r1 + + li_r1 &sym_cond + ld_r1,r1,0 + li_br &eval_cond + beq_r0,r1 + + li_r1 &sym_quasiquote + ld_r1,r1,0 + li_br &eval_quasiquote + beq_r0,r1 + ## Application: callee = eval(callee-expr, env) mov_r1,r0 ld_r2,sp,16 @@ -4278,12 +4873,13 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ret :eval_lambda - ## (lambda params body). Slot 1 gets repurposed to hold params - ## once we no longer need the original expr. + ## (lambda params body1 body2 ...). Collect body-list, rewrite any + ## leading (define …) forms into a letrec (step 12e), then stash + ## the resulting single expression as the closure's body. ld_r1,sp,8 li_br &cdr - call ## r0 = (params body) - st_r0,sp,24 ## slot 3 = (params body) + call ## r0 = (params body1 body2 ...) + st_r0,sp,24 ## slot 3 = after-head mov_r1,r0 li_br &car @@ -4292,15 +4888,15 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ld_r1,sp,24 li_br &cdr - call ## r0 = (body) + call ## r0 = body list mov_r1,r0 - li_br &car - call ## r0 = body + li_br &rewrite_lambda_body + call ## r0 = rewritten single expr mov_r2,r0 ld_r1,sp,8 ## r1 = params ld_r3,sp,16 ## r3 = env - li_br &make_closure ## tail: no Scheme-visible work after + li_br &make_closure tail_n3 :eval_define @@ -4337,34 +4933,752 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ret -## ---- Step-6 error landing pads -------------------------------------- -:err_unbound - li_r1 &msg_unbound - li_r2 %14 ## strlen("unbound symbol") == 14 - li_br &error - b +## ---- eval_set (step 12a) ------------------------------------------- +## (set! sym val-expr). Evaluates val-expr, then mutates the first +## binding of sym found in local→global search order. +:eval_set + ld_r1,sp,8 + li_br &cdr + call ## r0 = (sym val-expr) + mov_r1,r0 + li_br &car + call ## r0 = sym + st_r0,sp,24 ## slot 3 = sym -:err_arity - li_r1 &msg_arity - li_r2 %14 ## strlen("arity mismatch") == 14 - li_br &error - b + ld_r1,sp,8 + li_br &cdr + call + mov_r1,r0 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = val-expr + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + call ## r0 = val -:err_not_callable - li_r1 &msg_not_callable - li_r2 %12 ## strlen("not callable") == 12 - li_br &error - b + mov_r3,r0 + ld_r1,sp,24 + ld_r2,sp,16 + li_br &set_binding + tail_n3 -:err_too_many_args - li_r1 &msg_too_many_args - li_r2 %21 ## strlen("primitive argc > 32") == 21 - li_br &error - b -:err_bad_prim - li_r1 &msg_bad_prim - li_r2 %20 ## strlen("unknown primitive id") == 20 +## ---- set_binding(r1=sym, r2=env, r3=val) -> r0 = unspec ------------- +## Looks up the first (sym . v) cell in env (local alist first, then +## global) and mutates the cdr to val. Errors out on unbound sym. +:set_binding + prologue_n3 + st_r3,sp,24 ## slot 3 = val + st_r1,sp,8 ## slot 1 = sym (for global fallback) + + li_br &lookup_alist + call ## r0 = (sym . v) pair or nil + li_r1 NIL + li_br &set_binding_global + beq_r0,r1 + + ld_r1,sp,24 + addi_r0,r0,neg2 ## raw pair ptr + st_r1,r0,8 ## mutate cdr + li_r0 UNSPEC + epilogue_n3 + ret + +:set_binding_global + ld_r1,sp,8 + li_r2 &global_env_cell + ld_r2,r2,0 + li_br &lookup_alist + call + li_r1 NIL + li_br &err_unbound + beq_r0,r1 + + ld_r1,sp,24 + addi_r0,r0,neg2 + st_r1,r0,8 + li_r0 UNSPEC + epilogue_n3 + ret + + +## ---- eval_let (step 12b) ------------------------------------------- +## (let ((n1 e1) …) body…). Each RHS is evaluated in the *outer* env; +## then body evaluates in env extended with all new bindings. +:eval_let + ld_r1,sp,8 + li_br &cdr + call ## r0 = (bindings body…) + st_r0,sp,24 ## slot 3 = after-head + + mov_r1,r0 + li_br &car + call ## r0 = bindings + mov_r1,r0 + ld_r2,sp,16 + li_br &build_let_env + call ## r0 = new env + st_r0,sp,16 ## slot 2 = new env + + ld_r1,sp,24 + li_br &cdr + call ## r0 = body list + mov_r2,r0 + li_r1 &sym_begin + ld_r1,r1,0 + li_br &cons + call ## r0 = (begin . body) + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + tail_n3 + + +## ---- eval_letstar -------------------------------------------------- +## (let* ((n1 e1) …) body…). Each RHS is evaluated in the env built +## from previous bindings, then body evaluates in the final env. +:eval_letstar + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,24 + + mov_r1,r0 + li_br &car + call + mov_r1,r0 + ld_r2,sp,16 + li_br &build_letstar_env + call + st_r0,sp,16 + + ld_r1,sp,24 + li_br &cdr + call + mov_r2,r0 + li_r1 &sym_begin + ld_r1,r1,0 + li_br &cons + call + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + tail_n3 + + +## ---- eval_letrec --------------------------------------------------- +## (letrec ((n1 e1) …) body…). Pre-binds each name to UNSPEC, then +## walks the binding list a second time evaluating each RHS in the +## fully pre-bound env and mutating the binding cell. +:eval_letrec + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,24 + + mov_r1,r0 + li_br &car + call + mov_r1,r0 + ld_r2,sp,16 + li_br &build_letrec_env + call + st_r0,sp,16 + + ld_r1,sp,24 + li_br &cdr + call + mov_r2,r0 + li_r1 &sym_begin + ld_r1,r1,0 + li_br &cons + call + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + tail_n3 + + +## ---- eval_cond (step 12c) ------------------------------------------ +## (cond (test body…) … (else body…)). Walks clauses in order, evaluates +## each test; on truthy test or literal `else`, evaluates body as a begin. +## Returns unspec if no clause fires. +:eval_cond + ld_r1,sp,8 + li_br &cdr + call ## r0 = clauses + st_r0,sp,8 ## slot 1 = cursor + +:eval_cond_loop + ld_r1,sp,8 + li_r0 NIL + li_br &eval_cond_done + beq_r1,r0 + + li_br &car + call ## r0 = clause + st_r0,sp,24 ## slot 3 = clause + + mov_r1,r0 + li_br &car + call ## r0 = test + + li_r1 &sym_else + ld_r1,r1,0 + li_br &eval_cond_body + beq_r0,r1 + + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + call ## r0 = test value + + li_r1 FALSE + li_br &eval_cond_next + beq_r0,r1 + +:eval_cond_body + ld_r1,sp,24 + li_br &cdr + call ## r0 = body list + mov_r2,r0 + li_r1 &sym_begin + ld_r1,r1,0 + li_br &cons + call + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + tail_n3 + +:eval_cond_next + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &eval_cond_loop + b + +:eval_cond_done + li_r0 UNSPEC + epilogue_n3 + ret + + +## ---- eval_quasiquote (step 12d) ------------------------------------- +## (quasiquote template). Delegates to quasi_expand which walks the +## template recursively, handling `,x` (unquote) and `,@x` +## (unquote-splicing) forms. +:eval_quasiquote + ld_r1,sp,8 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = template + mov_r1,r0 + ld_r2,sp,16 + li_br &quasi_expand + tail_n3 + + +## ---- build_let_env(r1=bindings, r2=outer_env) -> r0 = new_env ------ +## Builds a fresh alist extending outer_env. Each RHS expression is +## evaluated in the *outer* env — the let semantics. +:build_let_env + prologue_n4 + st_r1,sp,8 ## slot 1 = cursor + st_r2,sp,16 ## slot 2 = outer env + st_r2,sp,24 ## slot 3 = new env acc + +:ble_loop + ld_r1,sp,8 + li_r0 NIL + li_br &ble_done + beq_r1,r0 + + li_br &car + call ## r0 = (name expr) + st_r0,sp,32 ## slot 4 = (name expr) + + mov_r1,r0 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = expr + + mov_r1,r0 + ld_r2,sp,16 ## outer env + li_br &eval + call ## r0 = val + + mov_r2,r0 + ld_r1,sp,32 + addi_r1,r1,neg2 + ld_r1,r1,0 ## r1 = name + li_br &cons + call ## r0 = (name . val) + + mov_r1,r0 + ld_r2,sp,24 + li_br &cons + call ## r0 = extended env + st_r0,sp,24 + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &ble_loop + b + +:ble_done + ld_r0,sp,24 + epilogue_n4 + ret + + +## ---- build_letstar_env(r1=bindings, r2=outer_env) -> r0 = new_env -- +## Like build_let_env, but each RHS is evaluated in the env accumulated +## so far (not the fixed outer env). +:build_letstar_env + prologue_n4 + st_r1,sp,8 + st_r2,sp,24 ## slot 3 = current env + +:bls_loop + ld_r1,sp,8 + li_r0 NIL + li_br &bls_done + beq_r1,r0 + + li_br &car + call + st_r0,sp,32 + + mov_r1,r0 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = expr + + mov_r1,r0 + ld_r2,sp,24 ## current env + li_br &eval + call ## r0 = val + + mov_r2,r0 + ld_r1,sp,32 + addi_r1,r1,neg2 + ld_r1,r1,0 + li_br &cons + call + + mov_r1,r0 + ld_r2,sp,24 + li_br &cons + call + st_r0,sp,24 + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &bls_loop + b + +:bls_done + ld_r0,sp,24 + epilogue_n4 + ret + + +## ---- build_letrec_env(r1=bindings, r2=outer_env) -> r0 = new_env -- +## Two-pass: pass 1 seeds each name with UNSPEC; pass 2 evaluates each +## RHS in the fully pre-bound env and mutates the binding's cdr via +## set_binding. Supports mutual recursion. +:build_letrec_env + prologue_n4 + st_r1,sp,8 ## slot 1 = cursor + st_r1,sp,16 ## slot 2 = original bindings (for pass 2) + st_r2,sp,24 ## slot 3 = new env + +:blr_pre_loop + ld_r1,sp,8 + li_r0 NIL + li_br &blr_pre_done + beq_r1,r0 + + li_br &car + call ## r0 = (name expr) + mov_r1,r0 + li_br &car + call ## r0 = name + + mov_r1,r0 + li_r2 UNSPEC + li_br &cons + call ## r0 = (name . UNSPEC) + + mov_r1,r0 + ld_r2,sp,24 + li_br &cons + call + st_r0,sp,24 + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &blr_pre_loop + b + +:blr_pre_done + ld_r1,sp,16 + st_r1,sp,8 ## reset cursor + +:blr_eval_loop + ld_r1,sp,8 + li_r0 NIL + li_br &blr_eval_done + beq_r1,r0 + + li_br &car + call + st_r0,sp,32 ## slot 4 = (name expr) + + mov_r1,r0 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = expr + + mov_r1,r0 + ld_r2,sp,24 + li_br &eval + call ## r0 = val + + mov_r3,r0 + ld_r1,sp,32 + addi_r1,r1,neg2 + ld_r1,r1,0 ## r1 = name + ld_r2,sp,24 + li_br &set_binding + call + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &blr_eval_loop + b + +:blr_eval_done + ld_r0,sp,24 + epilogue_n4 + ret + + +## ---- rewrite_lambda_body(r1=body_list) -> r0 = single body expr ---- +## Collects leading (define name val) forms from a lambda body and +## rewrites the body into (letrec ((n1 v1) …) body-tail…) if any are +## present. Otherwise returns either the single body form or a +## (begin . body) wrapper. +:rewrite_lambda_body + prologue_n4 + st_r1,sp,8 ## slot 1 = cursor + li_r0 NIL + st_r0,sp,16 ## slot 2 = reversed bindings acc + +:rlb_loop + ld_r1,sp,8 + li_r0 NIL + li_br &rlb_done + beq_r1,r0 + + li_br &car + call ## r0 = body form + mov_r1,r0 + andi_r0,r0,7 + li_r2 TAG_PAIR + li_br &rlb_done + bne_r0,r2 + + st_r1,sp,24 ## slot 3 = form + li_br &car + call ## r0 = head + li_r1 &sym_define + ld_r1,r1,0 + li_br &rlb_done + bne_r0,r1 + + ld_r1,sp,24 + li_br &cdr + call ## r0 = (name val) + + mov_r1,r0 + ld_r2,sp,16 + li_br &cons + call + st_r0,sp,16 + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &rlb_loop + b + +:rlb_done + ld_r1,sp,16 + li_r0 NIL + li_br &rlb_no_defs + beq_r1,r0 + + ld_r1,sp,16 + li_br &list_reverse + call + st_r0,sp,16 + + ld_r1,sp,16 + ld_r2,sp,8 + li_br &cons + call ## r0 = (bindings . body-tail) + mov_r2,r0 + li_r1 &sym_letrec + ld_r1,r1,0 + li_br &cons + tail_n4 + +:rlb_no_defs + ld_r1,sp,8 + li_br &cdr + call + li_r1 NIL + li_br &rlb_single + beq_r0,r1 + + ld_r2,sp,8 + li_r1 &sym_begin + ld_r1,r1,0 + li_br &cons + tail_n4 + +:rlb_single + ld_r1,sp,8 + li_br &car + tail_n4 + + +## ---- list_reverse(r1=list) -> r0 = reversed list -------------------- +## Simple tail-consing reverse; used by rewrite_lambda_body to restore +## source-order bindings. +:list_reverse + prologue_n2 + st_r1,sp,8 + li_r0 NIL + st_r0,sp,16 + +:lrv_loop + ld_r1,sp,8 + li_r0 NIL + li_br &lrv_done + beq_r1,r0 + + li_br &car + call + mov_r1,r0 + ld_r2,sp,16 + li_br &cons + call + st_r0,sp,16 + + ld_r1,sp,8 + li_br &cdr + call + st_r0,sp,8 + li_br &lrv_loop + b + +:lrv_done + ld_r0,sp,16 + epilogue_n2 + ret + + +## ---- quasi_expand(r1=template, r2=env) -> r0 = value --------------- +## Top-level quasiquote walker. Non-pair templates self-evaluate. A pair +## whose car is `unquote` evaluates the first element of its cdr. +## Otherwise the pair is walked as a list via quasi_list. +:quasi_expand + prologue_n3 + st_r1,sp,8 + st_r2,sp,16 + + mov_r0,r1 + andi_r0,r0,7 + li_r2 TAG_PAIR + li_br &qe_pair + beq_r0,r2 + ld_r0,sp,8 + epilogue_n3 + ret + +:qe_pair + ld_r1,sp,8 + li_br &car + call + li_r1 &sym_unquote + ld_r1,r1,0 + li_br &qe_unquote + beq_r0,r1 + + ld_r1,sp,8 + ld_r2,sp,16 + li_br &quasi_list + tail_n3 + +:qe_unquote + ld_r1,sp,8 + li_br &cdr + call + mov_r1,r0 + li_br &car + call + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + tail_n3 + + +## ---- quasi_list(r1=list, r2=env) -> r0 = expanded list -------------- +## Recursively expands each element. If an element is (unquote-splicing X), +## evaluates X and appends the resulting list into the output; else it +## recursively calls quasi_expand on the element. +:quasi_list + prologue_n4 + st_r1,sp,8 ## slot 1 = cursor + st_r2,sp,16 ## slot 2 = env + + li_r0 NIL + li_br &ql_nil + beq_r1,r0 + + mov_r0,r1 + andi_r0,r0,7 + li_r2 TAG_PAIR + li_br &ql_nonpair + bne_r0,r2 + + li_br &car + call ## r0 = head + st_r0,sp,24 ## slot 3 = head + + mov_r1,r0 + andi_r0,r0,7 + li_r2 TAG_PAIR + li_br &ql_head_reg + bne_r0,r2 + + ld_r1,sp,24 + li_br &car + call ## r0 = car(head) + li_r1 &sym_unquote_splicing + ld_r1,r1,0 + li_br &ql_splice + beq_r0,r1 + +:ql_head_reg + ld_r1,sp,24 + ld_r2,sp,16 + li_br &quasi_expand + call + st_r0,sp,24 ## slot 3 = expanded head + + ld_r1,sp,8 + li_br &cdr + call + mov_r1,r0 + ld_r2,sp,16 + li_br &quasi_list + call + + mov_r2,r0 + ld_r1,sp,24 + li_br &cons + tail_n4 + +:ql_splice + ld_r1,sp,24 + li_br &cdr + call + mov_r1,r0 + li_br &car + call ## r0 = X (the spliced expression) + mov_r1,r0 + ld_r2,sp,16 + li_br &eval + call ## r0 = evaluated list + st_r0,sp,24 + + ld_r1,sp,8 + li_br &cdr + call + mov_r1,r0 + ld_r2,sp,16 + li_br &quasi_list + call + + mov_r2,r0 + ld_r1,sp,24 + li_br &append_one + tail_n4 + +:ql_nil + li_r0 NIL + epilogue_n4 + ret + +:ql_nonpair + ld_r0,sp,8 + epilogue_n4 + ret + + +## ---- Step-6 error landing pads -------------------------------------- +:err_unbound + li_r1 &msg_unbound + li_r2 %14 ## strlen("unbound symbol") == 14 + li_br &error + b + +:err_arity + li_r1 &msg_arity + li_r2 %14 ## strlen("arity mismatch") == 14 + li_br &error + b + +:err_not_callable + li_r1 &msg_not_callable + li_r2 %12 ## strlen("not callable") == 12 + li_br &error + b + +:err_too_many_args + li_r1 &msg_too_many_args + li_r2 %21 ## strlen("primitive argc > 32") == 21 + li_br &error + b + +:err_bad_prim + li_r1 &msg_bad_prim + li_r2 %20 ## strlen("unknown primitive id") == 20 li_br &error b @@ -4407,6 +5721,15 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' :str_begin "begin" :str_lambda "lambda" :str_define "define" +:str_quasiquote "quasiquote" +:str_unquote "unquote" +:str_unquote_splicing "unquote-splicing" +:str_set "set!" +:str_let "let" +:str_letstar "let*" +:str_letrec "letrec" +:str_cond "cond" +:str_else "else" ## Primitive name strings (step 10b). The registration table below ## holds (ptr, len, code_id, type, arity) for each. _start walks the @@ -4477,16 +5800,6 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' :str_prim_equal "equal?" :str_prim_apply "apply" -:prelude_src -"(define map (lambda (f xs) (if (null? xs) (quote ()) " -"(cons (f (car xs)) (map f (cdr xs))))))" -"(define filter (lambda (p xs) (if (null? xs) (quote ()) " -"(if (p (car xs)) (cons (car xs) (filter p (cdr xs))) " -"(filter p (cdr xs))))))" -"(define fold (lambda (f acc xs) (if (null? xs) acc " -"(fold f (f acc (car xs)) (cdr xs)))))" -:prelude_src_end - ## Registration table. 40-byte records: ptr(8) + len(8) + code_id(8) + ## type(8) + arity(8). End-sentinel = zero name pointer. _start iterates @@ -4863,11 +6176,20 @@ DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000' ## ---- 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 %0 %0 -:sym_if %0 %0 -:sym_begin %0 %0 -:sym_lambda %0 %0 -:sym_define %0 %0 +:sym_quote %0 %0 +:sym_if %0 %0 +:sym_begin %0 %0 +:sym_lambda %0 %0 +:sym_define %0 %0 +:sym_quasiquote %0 %0 +:sym_unquote %0 %0 +:sym_unquote_splicing %0 %0 +:sym_set %0 %0 +:sym_let %0 %0 +:sym_letstar %0 %0 +:sym_letrec %0 %0 +:sym_cond %0 %0 +:sym_else %0 %0 ## Global-binding alist head. Zero-initialized (which is a valid ## untagged pair/nil sentinel? No — nil = 0x07. Seed to nil on entry diff --git a/tests/lisp/20-quote.expected b/tests/lisp/20-quote.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/20-quote.scm b/tests/lisp/20-quote.scm @@ -0,0 +1,11 @@ +;; Step 11a: quote shorthand. 'x → (quote x); same for the atomic, +;; symbol, and list cases. +(if (= (quote 42) 42) + (if (eq? (quote foo) (quote foo)) + (if (= (car (quote (1 2 3))) 1) + (if (= (length (quote (1 2 3))) 3) + (if (= '42 42) + (if (eq? 'foo 'foo) + (if (= (car '(1 2 3)) 1) + (if (= (length '(1 2 3)) 3) + 42 0) 0) 0) 0) 0) 0) 0) 0) diff --git a/tests/lisp/21-neg-hex.expected b/tests/lisp/21-neg-hex.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/21-neg-hex.scm b/tests/lisp/21-neg-hex.scm @@ -0,0 +1,7 @@ +;; Step 11b: negative decimal and 0x hex fixnum literals. +(if (< -5 0) + (if (= (+ -3 5) 2) + (if (= 0xFF 255) + (if (= 0xa 10) + (if (= -0x10 -16) + 42 0) 0) 0) 0) 0) diff --git a/tests/lisp/22-char.expected b/tests/lisp/22-char.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/22-char.scm b/tests/lisp/22-char.scm @@ -0,0 +1,8 @@ +;; Step 11c: character literals #\a, #\space, #\newline, #\tab. +(if (= #\a 97) + (if (= #\Z 90) + (if (= #\space 32) + (if (= #\newline 10) + (if (= #\tab 9) + (if (= #\0 48) + 42 0) 0) 0) 0) 0) 0) diff --git a/tests/lisp/23-vector.expected b/tests/lisp/23-vector.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/23-vector.scm b/tests/lisp/23-vector.scm @@ -0,0 +1,8 @@ +;; Step 11d: vector literals #(...). Reader builds a vector of the +;; constituent values (no element-level evaluation — literals only). +(if (= (vector-ref #(10 20 30) 0) 10) + (if (= (vector-ref #(10 20 30) 1) 20) + (if (= (vector-ref #(10 20 30) 2) 30) + (if (= (vector-length #(1 2 3 4)) 4) + (if (= (vector-length #()) 0) + 42 0) 0) 0) 0) 0) diff --git a/tests/lisp/24-dotted.expected b/tests/lisp/24-dotted.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/24-dotted.scm b/tests/lisp/24-dotted.scm @@ -0,0 +1,7 @@ +;; Step 11e: improper (dotted) list tail. (a b . c) is +;; (cons a (cons b c)). +(if (= (car '(1 . 2)) 1) + (if (= (cdr '(1 . 2)) 2) + (if (= (car (cdr '(1 2 . 3))) 2) + (if (= (cdr (cdr '(1 2 . 3))) 3) + 42 0) 0) 0) 0) diff --git a/tests/lisp/25-set.expected b/tests/lisp/25-set.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/25-set.scm b/tests/lisp/25-set.scm @@ -0,0 +1,8 @@ +;; Step 12a: set!. Mutates an existing binding. Exercises both the +;; global path and the local (let-bound) path — set_binding is +;; local-first with global fallback, so the two paths are distinct. +(define x 10) +(set! x 30) +(if (= x 30) + (if (= (let ((y 1)) (set! y 12) y) 12) + 42 0) 0) diff --git a/tests/lisp/26-let.expected b/tests/lisp/26-let.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/26-let.scm b/tests/lisp/26-let.scm @@ -0,0 +1,15 @@ +;; Step 12b: let / let* / letrec. +;; letrec must pre-bind all names so mutually recursive bindings can +;; see each other — this is the key behavior that distinguishes it +;; from let*. +(if (= (let ((x 5) (y 7)) (+ x y)) 12) + (if (= (let* ((x 5) (y (+ x 3))) y) 8) + (if (= (letrec ((f (lambda (n) + (if (< n 2) 1 (* n (f (- n 1))))))) + (f 5)) 120) + (if (= (letrec ((even? (lambda (n) + (if (= n 0) 1 (odd? (- n 1))))) + (odd? (lambda (n) + (if (= n 0) 0 (even? (- n 1)))))) + (+ (even? 4) (odd? 7))) 2) + 42 0) 0) 0) 0) diff --git a/tests/lisp/27-cond.expected b/tests/lisp/27-cond.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/27-cond.scm b/tests/lisp/27-cond.scm @@ -0,0 +1,10 @@ +;; Step 12c: cond. +(define classify + (lambda (n) + (cond ((< n 0) 'neg) + ((= n 0) 'zero) + (else 'pos)))) +(if (eq? (classify -5) 'neg) + (if (eq? (classify 0) 'zero) + (if (eq? (classify 7) 'pos) + 42 0) 0) 0) diff --git a/tests/lisp/28-quasi.expected b/tests/lisp/28-quasi.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/28-quasi.scm b/tests/lisp/28-quasi.scm @@ -0,0 +1,6 @@ +;; Step 12d: quasiquote / unquote / unquote-splicing. +(define x 5) +(if (= (car (cdr `(1 ,x 3))) 5) + (if (= (length `(1 ,@(list 2 3) 4)) 4) + (if (= (car (cdr (cdr `(1 ,@(list 2 3) 4)))) 3) + 42 0) 0) 0) diff --git a/tests/lisp/29-innerdef.expected b/tests/lisp/29-innerdef.expected @@ -0,0 +1 @@ +42 diff --git a/tests/lisp/29-innerdef.scm b/tests/lisp/29-innerdef.scm @@ -0,0 +1,17 @@ +;; Step 12e: inner (define …) inside a lambda body is rewritten into +;; letrec-shape so the definitions share scope with the trailing body. +;; A recursive inner define actually requires letrec semantics — plain +;; let* would leave `fact` unbound in its own RHS. +(define f + (lambda (x) + (define a 1) + (define b 2) + (+ x a b))) +(define g + (lambda (n) + (define fact + (lambda (k) (if (< k 2) 1 (* k (fact (- k 1)))))) + (fact n))) +(if (= (f 10) 13) + (if (= (g 5) 120) + 42 0) 0)