boot2

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

scheme1.P1pp (19854B)


      1 # scheme1.P1pp -- Phase 1 minimal Scheme interpreter on P1.
      2 #
      3 # The full target is described in docs/LISP-C.md and docs/LISP.md. This
      4 # file is the spine: enough infrastructure to read a single source file,
      5 # parse one s-expression, and evaluate it via tag-dispatched eval/apply
      6 # with a single primitive (`sys-exit`). Every later piece (more
      7 # primitives, special forms, closures, pmatch, records, prelude, repl
      8 # loop) hooks onto these anchors without restructuring.
      9 #
     10 # What's wired up now:
     11 #   - Tag layout per LISP-C.md: fixnums, pairs, symbols, headered objects,
     12 #     immediate singletons. mkimm composes IMM constants at M1pp time.
     13 #   - Bump heap allocator (cons + alloc_hdr) over a BSS-past-ELF_end arena.
     14 #   - Linear-scan symbol intern table; entries store (name_ptr, name_len,
     15 #     global_val) borrowed directly from readbuf.
     16 #   - Reader: '(', ')', fixnums (decimal, optional leading `-`), bare
     17 #     symbols. No strings/hex/chars/quote/dotted yet, no `;` comments.
     18 #   - eval: tag dispatch -> self-eval / symbol lookup / pair application.
     19 #   - apply: HDR.PRIM dispatch only; closures/specials come later.
     20 #   - One primitive: sys-exit. Exits with the raw fixnum.
     21 #
     22 # Build chain (P1pp.P1pp = libp1pp must be in the catm sequence):
     23 #   catm P1-<arch>.M1pp P1.M1pp P1pp.P1pp scheme1/scheme1.P1pp \
     24 #     | m1pp -> M0 -> hex2 -> ELF
     25 #
     26 # Memory model: the ELF's ph_memsz is 8 MB (boot2 default), so all
     27 # zero-initialized arenas live past :ELF_end and cost zero file bytes.
     28 # p1_main writes their absolute addresses into pointer slots once at
     29 # startup; every later access goes through one extra load.
     30 
     31 # =========================================================================
     32 # Constants
     33 # =========================================================================
     34 
     35 %enum TAG { FIXNUM PAIR SYM HEAP IMM }
     36 %enum IMM { FALSE TRUE NIL UNSPEC UNBOUND }
     37 %enum HDR { BV CLOSURE PRIM TD REC }
     38 
     39 # imm_val(idx) -> integer-expression for the tagged immediate at IMM index
     40 # `idx`. Used both at %li sites (loaded into a register) and at $() emission
     41 # sites (baked into a static word).
     42 %macro imm_val(idx)
     43 (| (<< idx 3) %TAG.IMM)
     44 %endm
     45 
     46 # Layout helpers. %struct stride is 8 bytes per field.
     47 %struct PAIR    { car cdr }                          # .SIZE = 16
     48 %struct SYMENT  { name_ptr name_len global_val pad } # .SIZE = 32
     49 %struct PRIM    { hdr entry_w }                       # .SIZE = 16
     50 
     51 # BSS arena offsets from :ELF_end. Each arena is 64 KiB; the three are
     52 # packed back-to-back below ELF_end + 192 KiB. p1_main's startup loop
     53 # materializes &ELF_end + OFF_X into the matching pointer slot. The
     54 # offsets are emitted directly in bss_init_tbl via $().
     55 
     56 %macro SYMTAB_CAP_SLOTS()
     57 1024
     58 %endm
     59 
     60 %macro READBUF_CAP_BYTES()
     61 65536
     62 %endm
     63 
     64 # =========================================================================
     65 # Tag idioms
     66 # =========================================================================
     67 
     68 %macro tagof(rd, rs)
     69 %andi(rd, rs, 7)
     70 %endm
     71 
     72 %macro mkfix(rd, rs)
     73 %shli(rd, rs, 3)
     74 %endm
     75 
     76 %macro untag_fix(rd, rs)
     77 %sari(rd, rs, 3)
     78 %endm
     79 
     80 %macro untag_sym(rd, rs)
     81 %sari(rd, rs, 3)
     82 %endm
     83 
     84 %macro car(rd, rs)
     85 %ld(rd, rs, -1)
     86 %endm
     87 
     88 %macro cdr(rd, rs)
     89 %ld(rd, rs, 7)
     90 %endm
     91 
     92 %macro hdr_type(rd, rs)
     93 %lb(rd, rs, -3)
     94 %endm
     95 
     96 # =========================================================================
     97 # Scheme1-local helpers
     98 # =========================================================================
     99 
    100 # Load the byte at readbuf_buf[off_reg] into rd. Clobbers rd. `rd` must
    101 # be the destination register; the macro reuses it as a scratch pointer
    102 # during the la / ld / add chain before the final lb writes the byte.
    103 %macro readbuf_byte(rd, off_reg)
    104 %la(rd, &readbuf_buf_ptr)
    105 %ld(rd, rd, 0)
    106 %add(rd, rd, off_reg)
    107 %lb(rd, rd, 0)
    108 %endm
    109 
    110 # Branch to `target` if `ch_reg` holds an ASCII whitespace byte (space,
    111 # tab, LF, CR). `scratch` is clobbered.
    112 %macro is_ws_branch(scratch, ch_reg, target)
    113 %addi(scratch, ch_reg, -32)
    114 %beqz(scratch, target)
    115 %addi(scratch, ch_reg, -9)
    116 %beqz(scratch, target)
    117 %addi(scratch, ch_reg, -10)
    118 %beqz(scratch, target)
    119 %addi(scratch, ch_reg, -13)
    120 %beqz(scratch, target)
    121 %endm
    122 
    123 # Compute &symtab_buf + idx_reg * SYMENT.SIZE into rd. `scratch` is
    124 # clobbered.
    125 %macro symtab_entry(rd, idx_reg, scratch)
    126 %la(rd, &symtab_buf_ptr)
    127 %ld(rd, rd, 0)
    128 %shli(scratch, idx_reg, 5)
    129 %add(rd, rd, scratch)
    130 %endm
    131 
    132 # Print msg_label, exit with `code`. Never returns.
    133 %macro die(msg, code)
    134 %la(a0, & ## msg)
    135 %call(&print_cstr)
    136 %li(a0, code)
    137 %call(&sys_exit)
    138 %endm
    139 
    140 # =========================================================================
    141 # p1_main -- runtime spine
    142 # =========================================================================
    143 #
    144 # Frame layout (16 bytes):
    145 #   +0  saved argv
    146 
    147 %fn(p1_main, 16, {
    148     %st(a1, sp, 0)
    149 
    150     %li(t0, 2)
    151     %bltu(a0, t0, &::usage)
    152 
    153     # Initialize BSS pointer slots from ELF_end + OFF_*. Same idiom as
    154     # M1pp.P1: a tiny init table walked once.
    155     %la(t0, &ELF_end)
    156     %la(t1, &bss_init_tbl)
    157     %la(t2, &bss_init_tbl_end)
    158     ::bss_loop
    159     %beq(t1, t2, &::bss_done)
    160     %ld(a0, t1, 0)
    161     %ld(a2, t1, 8)
    162     %add(a2, a2, t0)
    163     %st(a2, a0, 0)
    164     %addi(t1, t1, 16)
    165     %b(&::bss_loop)
    166     ::bss_done
    167 
    168     # heap_next = &heap_buf, rounded up to 8-byte alignment. The BSS arena
    169     # starts at &ELF_end + OFF_heap, but &ELF_end's alignment depends on
    170     # the data section above it; cons assumes 8-byte-aligned heap_next so
    171     # every pair pointer's low 3 bits are exactly the PAIR tag.
    172     %la(t0, &heap_buf_ptr)
    173     %ld(t0, t0, 0)
    174     %alignup(t0, t0, 8, t1)
    175     %la(t1, &heap_next)
    176     %st(t0, t1, 0)
    177 
    178     # Bind built-in primitives.
    179     %call(&register_primitives)
    180 
    181     # argv[1] is the source path (NUL-terminated cstr from the kernel).
    182     %ld(a1, sp, 0)
    183     %ld(a0, a1, 8)
    184     %call(&load_source)
    185 
    186     # parse_one returns one s-expression in a0.
    187     %call(&parse_one)
    188 
    189     # eval(expr, NIL).
    190     %li(a1, %imm_val(%IMM.NIL))
    191     %call(&eval)
    192 
    193     # If the form did not sys-exit (e.g. evaluated to a fixnum), drop
    194     # cleanly with status 0. Useful while wiring up more of the runtime.
    195     %li(a0, 0)
    196     %eret
    197 
    198     ::usage
    199     %la(a0, &msg_usage)
    200     %call(&print_cstr)
    201     %li(a0, 2)
    202 })
    203 
    204 # =========================================================================
    205 # Source loading -- argv[1] -> readbuf, length stored in readbuf_len
    206 # =========================================================================
    207 
    208 %fn(load_source, 0, {
    209     %la(t0, &readbuf_buf_ptr)
    210     %ld(a1, t0, 0)
    211     %li(a2, %READBUF_CAP_BYTES)
    212     %call(&read_file)
    213     %bltz(a0, &::fail)
    214 
    215     %la(t0, &readbuf_len)
    216     %st(a0, t0, 0)
    217     %li(a0, 0)
    218     %eret
    219 
    220     ::fail
    221     %die(msg_load_fail, 3)
    222 })
    223 
    224 # =========================================================================
    225 # Heap: cons (leaf) and alloc_hdr (leaf)
    226 # =========================================================================
    227 #
    228 # Both are call-free leaves: bump heap_next, write fields, return tagged
    229 # pointer. Heap exhaustion crashes via a deliberate read past the arena
    230 # end -- runtime check arrives when error reporting does (LISP-C.md
    231 # milestone 3).
    232 
    233 # cons(car=a0, cdr=a1) -> tagged pair (a0)
    234 :cons
    235     %la(t2, &heap_next)
    236     %ld(t0, t2, 0)
    237     %st(a0, t0, 0)
    238     %st(a1, t0, 8)
    239     %addi(t1, t0, 16)
    240     %st(t1, t2, 0)
    241     %addi(a0, t0, 1)
    242     %ret
    243 
    244 # alloc_hdr(bytes=a0, hdr_word=a1) -> tagged heap obj (a0)
    245 # Rounds bytes up to a multiple of 8 and writes hdr_word at offset 0.
    246 :alloc_hdr
    247     %alignup(a0, a0, 8, t0)
    248     %la(t2, &heap_next)
    249     %ld(t0, t2, 0)
    250     %add(t1, t0, a0)
    251     %st(t1, t2, 0)
    252     %st(a1, t0, 0)
    253     %addi(a0, t0, 3)
    254     %ret
    255 
    256 # =========================================================================
    257 # Symbol intern -- linear scan, append on miss
    258 # =========================================================================
    259 #
    260 # Frame: 32 bytes
    261 #   +0  name_ptr      (input)
    262 #   +8  name_len      (input)
    263 #   +16 idx           (loop counter / found index)
    264 #   +24 entry_ptr     (spilled across memcmp)
    265 
    266 %fn(intern, 32, {
    267     %st(a0, sp, 0)
    268     %st(a1, sp, 8)
    269 
    270     %li(t0, 0)
    271     %st(t0, sp, 16)
    272 
    273     ::scan
    274     # idx >= count? -> append
    275     %ld(t0, sp, 16)
    276     %la(t1, &symtab_count)
    277     %ld(t1, t1, 0)
    278     %bltu(t0, t1, &::probe)
    279     %b(&::append)
    280 
    281     ::probe
    282     %symtab_entry(t1, t0, t2)
    283     %st(t1, sp, 24)
    284 
    285     # entry.name_len == name_len ?
    286     %ld(t2, t1, 8)
    287     %ld(a2, sp, 8)
    288     %bne(t2, a2, &::next)
    289 
    290     # memcmp(entry.name_ptr, name_ptr, len)
    291     %ld(a0, t1, 0)
    292     %ld(a1, sp, 0)
    293     %ld(a2, sp, 8)
    294     %call(&memcmp)
    295     %beqz(a0, &::found)
    296 
    297     ::next
    298     %ld(t0, sp, 16)
    299     %addi(t0, t0, 1)
    300     %st(t0, sp, 16)
    301     %b(&::scan)
    302 
    303     ::append
    304     # Bounds check; on overflow exit 5 with a message.
    305     %ld(t0, sp, 16)
    306     %li(t1, %SYMTAB_CAP_SLOTS)
    307     %bltu(t0, t1, &::append_ok)
    308     %die(msg_symtab_full, 5)
    309 
    310     ::append_ok
    311     %symtab_entry(t1, t0, t2)
    312     %ld(a0, sp, 0)
    313     %st(a0, t1, 0)
    314     %ld(a0, sp, 8)
    315     %st(a0, t1, 8)
    316     %li(a0, %imm_val(%IMM.UNBOUND))
    317     %st(a0, t1, 16)
    318     %li(a0, 0)
    319     %st(a0, t1, 24)
    320 
    321     # symtab_count = idx + 1
    322     %addi(a0, t0, 1)
    323     %la(t2, &symtab_count)
    324     %st(a0, t2, 0)
    325 
    326     # fall through with idx in t0 = sp[16]
    327 
    328     ::found
    329     %ld(t0, sp, 16)
    330     %shli(a0, t0, 3)
    331     %ori(a0, a0, %TAG.SYM)
    332 })
    333 
    334 # Lookup by sym_idx (untagged, in a0). Returns symtab[idx].global_val in a0.
    335 # Leaf.
    336 :sym_global
    337     %la(t0, &symtab_buf_ptr)
    338     %ld(t0, t0, 0)
    339     %shli(t1, a0, 5)
    340     %add(t0, t0, t1)
    341     %ld(a0, t0, 16)
    342     %ret
    343 
    344 # sym_set_global(idx=a0, val=a1). Leaf.
    345 :sym_set_global
    346     %la(t0, &symtab_buf_ptr)
    347     %ld(t0, t0, 0)
    348     %shli(t1, a0, 5)
    349     %add(t0, t0, t1)
    350     %st(a1, t0, 16)
    351     %ret
    352 
    353 # =========================================================================
    354 # Reader -- parse_one over readbuf with a single byte cursor
    355 # =========================================================================
    356 #
    357 # Cursor lives in &readbuf_pos; readbuf_len holds the slurped byte count.
    358 # The reader is called recursively from parse_list, so every state goes
    359 # through frame slots, not s-registers.
    360 
    361 # Skip whitespace (ASCII 32, 9, 10, 13). Leaf.
    362 :skip_ws
    363 %scope skip_ws
    364     %la(t2, &readbuf_pos)
    365     %ld(t0, t2, 0)
    366     %la(t1, &readbuf_len)
    367     %ld(t1, t1, 0)
    368     ::loop
    369     %beq(t0, t1, &::done)
    370     %readbuf_byte(a0, t0)
    371     %is_ws_branch(a1, a0, &::step)
    372     %b(&::done)
    373     ::step
    374     %addi(t0, t0, 1)
    375     %b(&::loop)
    376     ::done
    377     %st(t0, t2, 0)
    378     %ret
    379 %endscope
    380 
    381 # parse_one() -> tagged value in a0
    382 %fn(parse_one, 0, {
    383     %call(&skip_ws)
    384 
    385     %la(t0, &readbuf_pos)
    386     %ld(t0, t0, 0)
    387     %la(t1, &readbuf_len)
    388     %ld(t1, t1, 0)
    389     %beq(t0, t1, &::eof)
    390 
    391     %readbuf_byte(a0, t0)
    392 
    393     %addi(a1, a0, -40)
    394     %beqz(a1, &::lparen)
    395     %addi(a1, a0, -41)
    396     %beqz(a1, &::rparen)
    397 
    398     %call(&parse_atom)
    399     %eret
    400 
    401     ::lparen
    402     # Consume '(' and read items until ')'.
    403     %la(t0, &readbuf_pos)
    404     %ld(t1, t0, 0)
    405     %addi(t1, t1, 1)
    406     %st(t1, t0, 0)
    407     %call(&parse_list)
    408     %eret
    409 
    410     ::rparen
    411     %die(msg_unexp_rparen, 6)
    412 
    413     ::eof
    414     %die(msg_unexp_eof, 6)
    415 })
    416 
    417 # parse_list() -> tagged list value in a0. Cursor sits past '(' on entry;
    418 # returns once ')' is consumed.
    419 #
    420 # Frame: 16 bytes
    421 #   +0  head (NIL until first item)
    422 #   +8  tail (most recent cons; set-cdr! target)
    423 %fn(parse_list, 16, {
    424     %li(t0, %imm_val(%IMM.NIL))
    425     %st(t0, sp, 0)
    426     %st(t0, sp, 8)
    427 
    428     ::loop
    429     %call(&skip_ws)
    430     %la(t0, &readbuf_pos)
    431     %ld(t0, t0, 0)
    432     %la(t1, &readbuf_len)
    433     %ld(t1, t1, 0)
    434     %beq(t0, t1, &::eof)
    435 
    436     %readbuf_byte(a0, t0)
    437     %addi(a1, a0, -41)
    438     %beqz(a1, &::close)
    439 
    440     # Not ')': parse one item, append.
    441     %call(&parse_one)
    442     %li(a1, %imm_val(%IMM.NIL))
    443     %call(&cons)
    444 
    445     # If head is NIL, both head and tail = new cons; else set-cdr! tail = new.
    446     %ld(t0, sp, 0)
    447     %li(t1, %imm_val(%IMM.NIL))
    448     %bne(t0, t1, &::link)
    449     %st(a0, sp, 0)
    450     %st(a0, sp, 8)
    451     %b(&::loop)
    452 
    453     ::link
    454     %ld(t0, sp, 8)
    455     # set-cdr! tail = a0  -> store a0 at [tail + 7] (raw + 8)
    456     %st(a0, t0, 7)
    457     %st(a0, sp, 8)
    458     %b(&::loop)
    459 
    460     ::close
    461     # Consume ')' and return head.
    462     %la(t0, &readbuf_pos)
    463     %ld(t1, t0, 0)
    464     %addi(t1, t1, 1)
    465     %st(t1, t0, 0)
    466     %ld(a0, sp, 0)
    467     %eret
    468 
    469     ::eof
    470     %die(msg_unterm_list, 6)
    471 })
    472 
    473 # parse_atom() -> tagged value (fixnum or symbol) in a0.
    474 # Reads until whitespace or paren or EOF, then dispatches by first byte.
    475 #
    476 # Frame: 16 bytes
    477 #   +0  start cursor (byte offset)
    478 #   +8  end cursor   (byte offset)
    479 %fn(parse_atom, 16, {
    480     %la(t0, &readbuf_pos)
    481     %ld(t1, t0, 0)
    482     %st(t1, sp, 0)
    483 
    484     %la(t2, &readbuf_len)
    485     %ld(t2, t2, 0)
    486 
    487     ::scan
    488     %beq(t1, t2, &::end)
    489     %readbuf_byte(a0, t1)
    490 
    491     %is_ws_branch(a1, a0, &::end)
    492     # paren?
    493     %addi(a1, a0, -40)
    494     %beqz(a1, &::end)
    495     %addi(a1, a0, -41)
    496     %beqz(a1, &::end)
    497 
    498     %addi(t1, t1, 1)
    499     %b(&::scan)
    500 
    501     ::end
    502     %st(t1, sp, 8)
    503     %la(t0, &readbuf_pos)
    504     %st(t1, t0, 0)
    505 
    506     # Dispatch on the first byte.
    507     %ld(t0, sp, 0)
    508     %la(a0, &readbuf_buf_ptr)
    509     %ld(a0, a0, 0)
    510     %add(a0, a0, t0)
    511     %lb(t1, a0, 0)
    512 
    513     # '0'..'9' -> int
    514     %addi(a1, t1, -48)
    515     %li(a2, 10)
    516     %bltu(a1, a2, &::is_int)
    517     # '-' followed by digit -> int
    518     %addi(a1, t1, -45)
    519     %bnez(a1, &::is_sym)
    520     # there must be at least one more byte for it to be a number
    521     %ld(t2, sp, 8)
    522     %addi(t0, t0, 1)
    523     %beq(t0, t2, &::is_sym)
    524     %readbuf_byte(a0, t0)
    525     %addi(a1, a0, -48)
    526     %bltu(a1, a2, &::is_int)
    527     # fall through to is_sym
    528 
    529     ::is_sym
    530     %ld(a0, sp, 0)
    531     %la(t0, &readbuf_buf_ptr)
    532     %ld(t0, t0, 0)
    533     %add(a0, t0, a0)
    534     %ld(t1, sp, 8)
    535     %ld(t2, sp, 0)
    536     %sub(a1, t1, t2)
    537     %call(&intern)
    538     %eret
    539 
    540     ::is_int
    541     %ld(a0, sp, 0)
    542     %ld(a1, sp, 8)
    543     %call(&parse_int)
    544 })
    545 
    546 # parse_int(start_off=a0, end_off=a1) -> tagged fixnum in a0. Leaf.
    547 :parse_int
    548 %scope parse_int
    549     %la(t0, &readbuf_buf_ptr)
    550     %ld(t0, t0, 0)
    551     %add(t1, t0, a1)        ; t1 = end pointer = base + end_off
    552     %add(t0, t0, a0)        ; t0 = start pointer = base + start_off
    553 
    554     %li(a2, 0)              ; a2 = "is negative" flag (0 = positive)
    555     %lb(a3, t0, 0)
    556     %addi(a3, a3, -45)
    557     %bnez(a3, &::loop)
    558     %li(a2, 1)
    559     %addi(t0, t0, 1)
    560 
    561     ::loop
    562     %li(a0, 0)
    563     %li(t2, 10)
    564     ::step
    565     %beq(t0, t1, &::done)
    566     %lb(a3, t0, 0)
    567     %addi(a3, a3, -48)
    568     %mul(a0, a0, t2)
    569     %add(a0, a0, a3)
    570     %addi(t0, t0, 1)
    571     %b(&::step)
    572 
    573     ::done
    574     %beqz(a2, &::tag)
    575     %li(t2, 0)
    576     %sub(a0, t2, a0)
    577     ::tag
    578     %mkfix(a0, a0)
    579     %ret
    580 %endscope
    581 
    582 # =========================================================================
    583 # eval / apply
    584 # =========================================================================
    585 #
    586 # eval is the only place that touches tag bits at runtime; the table
    587 # below is a flat compare cascade for now (5 tags). When special-form
    588 # dispatch is wired up the SYM/PAIR paths split further per LISP-C.md
    589 # §Eval.
    590 
    591 # eval(expr=a0, env=a1) -> value (a0)
    592 #
    593 # Frame: 32 bytes
    594 #   +0  expr
    595 #   +8  env
    596 #   +16 fn (head value, while args are being evaluated)
    597 %fn(eval, 32, {
    598     %st(a0, sp, 0)
    599     %st(a1, sp, 8)
    600 
    601     %tagof(t0, a0)
    602     %li(t1, %TAG.PAIR)
    603     %beq(t0, t1, &::pair)
    604     %li(t1, %TAG.SYM)
    605     %beq(t0, t1, &::sym)
    606     # FIXNUM, HEAP, IMM all self-evaluate.
    607     %eret
    608 
    609     ::sym
    610     %untag_sym(a0, a0)
    611     %call(&sym_global)
    612     %li(t0, %imm_val(%IMM.UNBOUND))
    613     %beq(a0, t0, &::unbound)
    614     %eret
    615 
    616     ::unbound
    617     %die(msg_unbound, 7)
    618 
    619     ::pair
    620     # head = eval(car(expr), env)
    621     %ld(a0, sp, 0)
    622     %car(a0, a0)
    623     %ld(a1, sp, 8)
    624     %call(&eval)
    625     %st(a0, sp, 16)
    626 
    627     # args = eval_args(cdr(expr), env)
    628     %ld(a0, sp, 0)
    629     %cdr(a0, a0)
    630     %ld(a1, sp, 8)
    631     %call(&eval_args)
    632 
    633     # apply(fn, args)
    634     %mov(a1, a0)
    635     %ld(a0, sp, 16)
    636     %call(&apply)
    637 })
    638 
    639 # eval_args(args=a0, env=a1) -> evaluated args list (cons-built).
    640 # Recursion depth = arg count, so very long arg lists could blow the
    641 # stack. Iterative tail-build is a future tightening.
    642 %fn(eval_args, 24, {
    643     %li(t0, %imm_val(%IMM.NIL))
    644     %beq(a0, t0, &::nil)
    645 
    646     %st(a0, sp, 0)
    647     %st(a1, sp, 8)
    648 
    649     # val = eval(car(args), env)
    650     %car(a0, a0)
    651     %call(&eval)
    652     %st(a0, sp, 16)
    653 
    654     # rest = eval_args(cdr(args), env)
    655     %ld(a0, sp, 0)
    656     %cdr(a0, a0)
    657     %ld(a1, sp, 8)
    658     %call(&eval_args)
    659 
    660     # cons(val, rest)
    661     %mov(a1, a0)
    662     %ld(a0, sp, 16)
    663     %call(&cons)
    664     %eret
    665 
    666     ::nil
    667     %li(a0, %imm_val(%IMM.NIL))
    668 })
    669 
    670 # apply(fn=a0, args=a1) -> result (a0)
    671 #
    672 # Frame: 16 bytes
    673 #   +0  args
    674 %fn(apply, 16, {
    675     %st(a1, sp, 0)
    676 
    677     # Only HEAP-tagged values can be applicable. PRIM is the only header
    678     # type wired up here.
    679     %tagof(t0, a0)
    680     %li(t1, %TAG.HEAP)
    681     %bne(t0, t1, &::not_proc)
    682 
    683     %hdr_type(t0, a0)
    684     %li(t1, %HDR.PRIM)
    685     %beq(t0, t1, &::prim)
    686 
    687     ::not_proc
    688     %die(msg_not_proc, 8)
    689 
    690     ::prim
    691     %ld(t0, a0, 5)         ; t0 = entry word (offset = -3 + 8)
    692     %ld(a0, sp, 0)         ; args list -> a0
    693     %callr(t0)
    694 })
    695 
    696 # =========================================================================
    697 # Primitives
    698 # =========================================================================
    699 #
    700 # Each primitive sits behind a 16-byte heap object literal in the data
    701 # section: [hdr_word, entry_word]. The tagged value is &obj + 3.
    702 # register_primitives interns the surface name and writes the tagged
    703 # pointer into the symbol's global slot.
    704 
    705 %fn(register_primitives, 0, {
    706     %la(a0, &name_sys_exit)
    707     %li(a1, 8)
    708     %call(&intern)
    709     %untag_sym(a0, a0)          ; idx
    710     %la(a1, &prim_sys_exit)
    711     %addi(a1, a1, 3)            ; tag HEAP
    712     %call(&sym_set_global)
    713 })
    714 
    715 # prim_sys_exit_entry(args=a0). Args is a one-element list whose car is
    716 # the exit code as a tagged fixnum. Untag and tail-jump to libp1pp's
    717 # sys_exit (a %b, not a %call -- this is a leaf with no frame, and
    718 # sys_exit doesn't return anyway).
    719 :prim_sys_exit_entry
    720     %car(a0, a0)            ; car = fixnum
    721     %untag_fix(a0, a0)
    722     %b(&sys_exit)
    723 
    724 # =========================================================================
    725 # Read-only data
    726 # =========================================================================
    727 
    728 # Primitive object literals (16 bytes each).
    729 :prim_sys_exit
    730 $(%HDR.PRIM) &prim_sys_exit_entry %(0)
    731 
    732 # Surface names. Length is hard-coded at the call site; no NUL needed
    733 # because intern takes (ptr, len). Aligned padding via "\0" bytes is
    734 # fine -- M0 emits ASCII verbatim.
    735 :name_sys_exit "sys-exit"
    736 
    737 :msg_usage          "scheme1: usage: scheme1 SOURCE.scm" '0a' '00'
    738 :msg_load_fail      "scheme1: failed to read source" '0a' '00'
    739 :msg_symtab_full    "scheme1: symbol table full" '0a' '00'
    740 :msg_unexp_rparen   "scheme1: unexpected ')'" '0a' '00'
    741 :msg_unexp_eof      "scheme1: unexpected EOF in form" '0a' '00'
    742 :msg_unterm_list    "scheme1: unterminated list" '0a' '00'
    743 :msg_unbound        "scheme1: unbound variable" '0a' '00'
    744 :msg_not_proc       "scheme1: not a procedure" '0a' '00'
    745 
    746 # =========================================================================
    747 # BSS pointer-init table
    748 # =========================================================================
    749 #
    750 # Each entry: 8-byte slot pointer (4-byte label ref + 4 bytes pad) +
    751 # 8-byte offset constant. p1_main walks this once at startup.
    752 :bss_init_tbl
    753 &readbuf_buf_ptr %(0) $(0)
    754 &heap_buf_ptr    %(0) $(0x10000)
    755 &symtab_buf_ptr  %(0) $(0x20000)
    756 :bss_init_tbl_end
    757 
    758 # =========================================================================
    759 # Scalar BSS (file-resident, zero-initialized)
    760 # =========================================================================
    761 
    762 # heap_next: bump pointer; written once by p1_main, then by cons/alloc_hdr.
    763 :heap_next        $(0)
    764 
    765 # Source-buffer cursor and slurped length.
    766 :readbuf_pos      $(0)
    767 :readbuf_len      $(0)
    768 
    769 # Symbol table count (number of entries used).
    770 :symtab_count     $(0)
    771 
    772 # Pointer slots for the past-:ELF_end arenas.
    773 :readbuf_buf_ptr  $(0)
    774 :heap_buf_ptr     $(0)
    775 :symtab_buf_ptr   $(0)
    776 
    777 :ELF_end