boot2

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

scheme1.P1pp (233265B)


      1 # scheme1.P1pp -- Phase 1 minimal Scheme interpreter on P1.
      2 #
      3 # Build chain:
      4 #   catm P1-<arch>.M1pp P1.M1pp P1pp.P1pp scheme1/scheme1.P1pp \
      5 #     | m1pp -> hex2pp -> ELF
      6 #
      7 # Run chain:
      8 #   catm scheme1/prelude.scm prog.scm | scheme1
      9 
     10 # =========================================================================
     11 # Constants
     12 # =========================================================================
     13 
     14 %enum TAG { FIXNUM PAIR SYM HEAP IMM CHAR }
     15 %enum IMM { FALSE TRUE NIL UNSPEC UNBOUND EOF }
     16 %enum HDR { BV STRING CLOSURE PRIM TD REC MV HASH HASHDATA ENV }
     17 %enum GCKIND { FREE PAIR HEAP RAW }
     18 
     19 # Each managed block begins with two padded 8-byte slots. The first word is
     20 #   (total_block_bytes << 8) | kind | mark
     21 # and the second is an intrusive link, reused by the free list and mark
     22 # worklist.  Payloads retain their historical layouts and tags.
     23 %macro GC_HEADER_BYTES() 16 %endm
     24 %macro GC_MARK_BIT() 128 %endm
     25 %macro GC_KIND_MASK() 7 %endm
     26 
     27 # imm_val(idx) -> integer-expression for the tagged immediate at IMM index
     28 # `idx`. Used both at %li sites (loaded into a register) and at $() emission
     29 # sites (baked into a static word).
     30 %macro imm_val(idx) (| (<< idx 3) %TAG.IMM) %endm
     31 
     32 # Layout helpers. %struct stride is 8 bytes per field.
     33 %struct PAIR    { car cdr }                          # .SIZE = 16
     34 %struct SYMENT  { name_ptr name_len global_val pad } # .SIZE = 32
     35 %struct PRIM    { hdr entry_w data }                  # .SIZE = 24
     36 %struct CLOSURE { hdr params body env }              # .SIZE = 32
     37 %struct TD      { hdr name nfields fields }           # .SIZE = 32
     38 %struct BV      { hdr data }                          # .SIZE = 16
     39 %struct REC     { hdr td }                            # .SIZE = 16 (header)
     40 %struct HASH    { hdr data }                          # .SIZE = 16
     41 %struct HASHDATA { hdr count used }                   # .SIZE = 24 (header)
     42 %struct ENV     { hdr sym value parent bloom }        # .SIZE = 40
     43 # Records are variable width: header + td slot + N field slots.
     44 # Hash data is variable width: header + count + used + N 24-byte slots.
     45 # A slot is (stored-hash, key, value); 0 means empty and 1 is a tombstone.
     46 
     47 # BSS arenas anchored past :ELF_end. readbuf is 1 MiB (sized to fit
     48 # the catm'd cc compiler source incl. prelude), followed by the symbol
     49 # table, the exact-root frame stack, and one 256 MiB managed heap.
     50 
     51 %macro SYMTAB_CAP_SLOTS() 8192 %endm
     52 %macro READBUF_CAP_BYTES() 1048576 %endm
     53 # A block header stores byte size above eight flag bits. P1-32 therefore
     54 # caps an individual block below 16 MiB; P1-64 keeps the original 256 MiB
     55 # heap. The source/read/symbol arenas are unchanged.
     56 %macro HEAP_CAP_BYTES() %p1_scheme_heap_bytes %endm
     57 %macro GC_ROOT_CAP_FRAMES() 8192 %endm
     58 %macro GC_ROOT_FRAME_BYTES() 24 %endm
     59 
     60 # =========================================================================
     61 # Tag idioms
     62 # =========================================================================
     63 
     64 %macro tagof(rd, rs) %andi(rd, rs, 7) %endm
     65 %macro mkfix(rd, rs) %shli(rd, rs, 3) %endm
     66 %macro untag_fix(rd, rs) %sari(rd, rs, 3) %endm
     67 %macro mkchar(rd, rs) %shli(rd, rs, 3) %ori(rd, rd, %TAG.CHAR) %endm
     68 %macro untag_char(rd, rs) %shri(rd, rs, 3) %endm
     69 %macro untag_sym(rd, rs) %sari(rd, rs, 3) %endm
     70 %macro car(rd, rs) %ld(rd, rs, -1) %endm
     71 %macro cdr(rd, rs) %ld(rd, rs, 7) %endm
     72 %macro set_car(rs, pair_tagged) %st(rs, pair_tagged, -1) %endm
     73 %macro set_cdr(rs, pair_tagged) %st(rs, pair_tagged, 7) %endm
     74 %macro hdr_type(rd, rs) %lb(rd, rs, -3) %endm
     75 
     76 # Field access through a tagged HEAP pointer (tag = 3). `field` is a
     77 # constant byte offset from the underlying raw object (e.g. %PRIM.data,
     78 # %CLOSURE.env). Reader is %ld; writer is %heap_st.
     79 %macro heap_ld(rd, rs, field) %ld(rd, rs, (- field 3)) %endm
     80 %macro heap_st(rs, rt, field) %st(rs, rt, (- field 3)) %endm
     81 
     82 # =========================================================================
     83 # Scheme1-local helpers
     84 # =========================================================================
     85 
     86 # Load the byte at readbuf_buf[off_reg] into rd. Clobbers rd. `rd` must
     87 # be the destination register; the macro reuses it as a scratch pointer
     88 # during the la / ld / add chain before the final lb writes the byte.
     89 %macro readbuf_byte(rd, off_reg)
     90     %ld_global(rd, &readbuf_buf_ptr)
     91     %add(rd, rd, off_reg)
     92     %lb(rd, rd, 0)
     93 %endm
     94 
     95 # Increment cursor and store it back. `addr_reg` is the address of
     96 # readbuf_pos as returned by %lda_global (the second output register).
     97 %macro readbuf_advance(pos_reg, addr_reg)
     98     %addi(pos_reg, pos_reg, 1)
     99     %st(pos_reg, addr_reg, 0)
    100 %endm
    101 
    102 # Load readbuf_len into len_reg and branch to target if cursor is at EOF.
    103 %macro readbuf_at_eof(pos_reg, len_reg, target)
    104     %ld_global(len_reg, &readbuf_len)
    105     %beq(pos_reg, len_reg, target)
    106 %endm
    107 
    108 # Branch character equal/not-equal: if (c == expect) / (c != expect) goto target.
    109 # expect passed as -(char_code). scratch is clobbered.
    110 %macro bceq(c, neg_cv, target, scratch)
    111     %addi(scratch, c, neg_cv)
    112     %beqz(scratch, target)
    113 %endm
    114 
    115 %macro bcne(c, neg_cv, target, scratch)
    116     %addi(scratch, c, neg_cv)
    117     %bnez(scratch, target)
    118 %endm
    119 
    120 # Branch immediate equal/not-equal: if (reg == value) / (reg != value) goto target.
    121 # scratch is clobbered.
    122 %macro bieq(reg, value, target, scratch)
    123     %li(scratch, value)
    124     %beq(reg, scratch, target)
    125 %endm
    126 
    127 %macro bine(reg, value, target, scratch)
    128     %li(scratch, value)
    129     %bne(reg, scratch, target)
    130 %endm
    131 
    132 # Branch to `target` if `ch_reg` holds an ASCII whitespace byte (space,
    133 # tab, LF, CR). `scratch` is clobbered.
    134 %macro is_ws_branch(scratch, ch_reg, target)
    135     %bceq(ch_reg, -32, target, scratch)    ; SP
    136     %bceq(ch_reg,  -9, target, scratch)    ; HT
    137     %bceq(ch_reg, -10, target, scratch)    ; LF
    138     %bceq(ch_reg, -13, target, scratch)    ; CR
    139 %endm
    140 
    141 # Branch to `target` if `ch_reg` is an R7RS token delimiter. Some of the
    142 # delimited constructs are deliberately unsupported by micro, but they still
    143 # terminate an adjacent token so the reader rejects them at their own start.
    144 %macro is_token_delimiter_branch(scratch, ch_reg, target)
    145     %is_ws_branch(scratch, ch_reg, target)
    146     %bceq(ch_reg, -40,  target, scratch)    ; '('
    147     %bceq(ch_reg, -41,  target, scratch)    ; ')'
    148     %bceq(ch_reg, -59,  target, scratch)    ; ';'
    149     %bceq(ch_reg, -34,  target, scratch)    ; '"'
    150     %bceq(ch_reg, -124, target, scratch)    ; '|'
    151     %bceq(ch_reg, -39,  target, scratch)    ; '\''
    152     %bceq(ch_reg, -96,  target, scratch)    ; '`'
    153     %bceq(ch_reg, -44,  target, scratch)    ; ','
    154 %endm
    155 
    156 # Branch to `target` if lo_neg <= c < lo_neg+count (unsigned). Both
    157 # scratch and count_scratch are clobbered.
    158 %macro brange(c, lo_neg, count, scratch, count_scratch, target)
    159     %addi(scratch, c, lo_neg)
    160     %li(count_scratch, count)
    161     %bltu(scratch, count_scratch, target)
    162 %endm
    163 
    164 # Compute &symtab_buf + idx_reg * SYMENT.SIZE into rd. `scratch` is
    165 # clobbered.
    166 %macro symtab_entry(rd, idx_reg, scratch)
    167     %ld_global(rd, &symtab_buf_ptr)
    168     %shli(scratch, idx_reg, 5)
    169     %add(rd, rd, scratch)
    170 %endm
    171 
    172 # Build the two-bit lexical-environment filter mask for tagged symbol `sym`.
    173 # Symbol identity is its stable intern-table index, so two inexpensive affine
    174 # projections give deterministic bit positions in one target word. `rd`,
    175 # `scratch0`, and `scratch1` must be distinct; both scratches are clobbered.
    176 %macro env_sym_mask(rd, sym, scratch0, scratch1)
    177     %untag_sym(scratch0, sym)
    178     %andi(rd, scratch0, (- %p1_word_bits 1))
    179     %li(scratch1, 1)
    180     %shl(rd, scratch1, rd)
    181     %li(scratch1, 13)
    182     %mul(scratch0, scratch0, scratch1)
    183     %addi(scratch0, scratch0, 17)
    184     %andi(scratch0, scratch0, (- %p1_word_bits 1))
    185     %li(scratch1, 1)
    186     %shl(scratch0, scratch1, scratch0)
    187     %or(rd, rd, scratch0)
    188 %endm
    189 
    190 # Compute the raw address of hash-data slot `idx_reg`. HASHDATA's three
    191 # header words occupy 24 bytes; each open-addressed slot is another three
    192 # words (stored hash, key, value). `data_reg` is a tagged HEAP pointer.
    193 %macro hash_slot(rd, data_reg, idx_reg, scratch)
    194     %shli(rd, idx_reg, 4)
    195     %shli(scratch, idx_reg, 3)
    196     %add(rd, rd, scratch)
    197     %add(rd, rd, data_reg)
    198     %addi(rd, rd, 21)          ; raw +24 relative to a HEAP-tagged pointer
    199 %endm
    200 
    201 # Print msg_label and abort. Never returns. Routes through runtime_error
    202 # so every error path lands in one place (stderr + exit 1).
    203 %macro die(msg)
    204     %la(a0, & ## msg)
    205     %call(&runtime_error)
    206 %endm
    207 
    208 # Emit an 8-aligned NUL-terminated string.
    209 %macro cstr8(str)
    210     str
    211     00
    212     .align 8
    213 %endm
    214 
    215 # Intern `str` into `slot` and declare its padded string data inline.
    216 # `key` is the label suffix; the data label :name_##key is emitted here.
    217 %macro intern_form(key, str, slot)
    218     %la(a0, &name_ ## key)
    219     %li(a1, (strlen str))
    220     %call(&intern)
    221     %st_global(a0, slot, t0)
    222     %b(&@end)
    223     :name_ ## key
    224     %cstr8(str)
    225     :@end
    226 %endm
    227 
    228 # Special-form dispatch: pointer-compare the head symbol against `slot`'s
    229 # cached value (in t0) and branch to `target` on hit. Caller has already
    230 # loaded head into t0.
    231 %macro dispatch_form(slot, target)
    232     %ld_global(t1, slot)
    233     %beq(t0, t1, target)
    234 %endm
    235 
    236 # Tail-jump from a special-form dispatch label to its handler. Handlers
    237 # uniformly take (rest=cdr(expr), env) -> value; expr lives at sp[0],
    238 # env at sp[8] in eval's frame.
    239 %macro tail_to_handler(handler)
    240     %ld(a0, sp, 0)
    241     %cdr(a0, a0)
    242     %ld(a1, sp, 8)
    243     %gctail(handler)
    244 %endm
    245 
    246 # Branch to `target` if `val` holds the NIL immediate. `scratch` is
    247 # clobbered.
    248 %macro if_nil(scratch, val, target)
    249     %li(scratch, %imm_val(%IMM.NIL))
    250     %beq(val, scratch, target)
    251 %endm
    252 
    253 # Advance a named list-cursor local to its cdr. t0 is the implicit scratch
    254 # register; callers must ensure it's free.
    255 %macro advance_walk(name)
    256     %ldl(t0, name)
    257     %cdr(t0, t0)
    258     %stl(t0, name)
    259 %endm
    260 
    261 # Set a global binding. sym is a tagged symbol, val is the new value.
    262 # Untags sym into the idx ABI position and calls sym_set_global.
    263 %macro set_global(sym, val)
    264     %mov(a1, val)
    265     %untag_sym(a0, sym)
    266     %call(&sym_set_global)
    267 %endm
    268 
    269 # Exact shadow-root frames. Every frame records the portable local base
    270 # plus two bitmaps: tagged Scheme-reference slots and temporarily live raw
    271 # managed-allocation pointers.  Bit N describes native local slot N.  The
    272 # collector dereferences only those described slots; ordinary machine
    273 # locals and the native P1 stack are never scanned.
    274 %macro gc_frame_push(scheme_mask, raw_mask)
    275     %ld_global(t0, &gc_root_next)
    276     %addi(t1, t0, %GC_ROOT_FRAME_BYTES)
    277     %ld_global(t2, &gc_root_end)
    278     %bltu(t2, t1, &@overflow)
    279     # p1_addi sees native sp directly; step over the two-word backend header.
    280     %addi(t2, sp, (* 2 %p1_word_bytes))
    281     %st(t2, t0, 0)
    282     %li(t2, scheme_mask)
    283     %st(t2, t0, 8)
    284     %li(t2, raw_mask)
    285     %st(t2, t0, 16)
    286     %st_global(t1, &gc_root_next, t2)
    287     %b(&@done)
    288     :@overflow
    289     %die(msg_gc_roots_full)
    290     :@done
    291 %endm
    292 
    293 %macro gc_frame_pop()
    294     %ld_global(t0, &gc_root_next)
    295     %addi(t0, t0, (- %GC_ROOT_FRAME_BYTES))
    296     # Clear popped descriptors so debugging an overflow never exposes
    297     # stale native-stack addresses as apparently active slots.
    298     %li(t1, 0)
    299     %st(t1, t0, 0)
    300     %st(t1, t0, 8)
    301     %st(t1, t0, 16)
    302     %st_global(t0, &gc_root_next, t1)
    303 %endm
    304 
    305 %macro gc_frame_clear(frame_size)
    306     %addi(t0, sp, (* 2 %p1_word_bytes))
    307     %li(t1, frame_size)
    308     %li(t2, 0)
    309     :@loop
    310     %beqz(t1, &@done)
    311     %st(t2, t0, 0)
    312     %addi(t0, t0, 8)
    313     %addi(t1, t1, -8)
    314     %b(&@loop)
    315     :@done
    316 %endm
    317 
    318 %macro gceret()
    319     %gc_frame_pop
    320     %eret
    321 %endm
    322 
    323 %macro gctail(target)
    324     %gc_frame_pop
    325     %tail(target)
    326 %endm
    327 
    328 %macro gctailr(target_reg)
    329     %mov(a3, target_reg)
    330     %gc_frame_pop
    331     %tailr(a3)
    332 %endm
    333 
    334 # GC-aware counterpart of P1pp's %fn2.  Functions using this form must
    335 # use %gceret / %gctail / %gctailr for explicit exits; fallthrough runs
    336 # the epilogue emitted here.
    337 %macro gcfn2(name, locals, scheme_mask, raw_mask, body)
    338     %struct name ## _FRAME { locals }
    339     : ## name
    340     .scope
    341     %frame name
    342     %enter(% ## name ## _FRAME.SIZE)
    343     %gc_frame_clear(% ## name ## _FRAME.SIZE)
    344     %gc_frame_push(scheme_mask, raw_mask)
    345     body
    346     %gc_frame_pop
    347     %eret
    348     %endframe
    349     .endscope
    350 %endm
    351 
    352 # car-and-untag-fixnum: rd = car(list) >> 3.
    353 %macro car_fix(rd, list)
    354     %car(rd, list)
    355     %sari(rd, rd, 3)
    356 %endm
    357 
    358 # car-then-load-bytevector-data-pointer: rd = (car(list)).data_ptr.
    359 %macro car_bvdata(rd, list)
    360     %car(rd, list)
    361     %ld(rd, rd, 5)
    362 %endm
    363 
    364 # Positional list-arg extraction. r_n receives the nth element of `list`;
    365 # the last destination register doubles as the in-flight rest cursor
    366 # during extraction (its final value is the last argument).
    367 %macro args2(r0, r1, list)
    368     %car(r0, list)
    369     %cdr(r1, list)
    370     %car(r1, r1)
    371 %endm
    372 
    373 %macro args3(r0, r1, r2, list)
    374     %car(r0, list)
    375     %cdr(r2, list)
    376     %car(r1, r2)
    377     %cdr(r2, r2)
    378     %car(r2, r2)
    379 %endm
    380 
    381 %macro args4(r0, r1, r2, r3, list)
    382     %car(r0, list)
    383     %cdr(r3, list)
    384     %car(r1, r3)
    385     %cdr(r3, r3)
    386     %car(r2, r3)
    387     %cdr(r3, r3)
    388     %car(r3, r3)
    389 %endm
    390 
    391 # =========================================================================
    392 # p1_main -- runtime spine
    393 # =========================================================================
    394 
    395 %fn(p1_main, 0, {
    396     # Stash argc/argv
    397     %st_global(a0, &saved_argc, t0)
    398     %st_global(a1, &saved_argv, t0)
    399 
    400     # if argc < 2 goto usage
    401     %li(t0, 2)
    402     %bltu(a0, t0, &.usage)
    403 
    404     # Initialize
    405     %la(a0, &ELF_end)
    406     %la(a1, &arena_table)
    407     %la(a2, &arena_table_end)
    408     %call(&init_arenas)
    409     %call(&heap_init)
    410     %call(&intern_special_forms)
    411     %call(&register_primitives)
    412     %call(&register_globals)
    413 
    414     # load_source(argv[1])
    415     %ld_global(a0, &saved_argv)
    416     %ld(a0, a0, %p1_word_bytes)
    417     %call(&load_source)
    418 
    419     # read-eval loop
    420     %loop_scoped({
    421         # eof = skip_ws()
    422         %call(&skip_ws)
    423         # if eof break
    424         %if_nez(a0, { %break })
    425         # expr = parse_one()
    426         %call(&parse_one)
    427         # eval(expr, env=nil)
    428         %li(a1, %imm_val(%IMM.NIL))
    429         %call(&eval)
    430     })
    431 
    432     # return 0
    433     %li(a0, 0)
    434     %eret
    435 
    436     :.usage
    437     %la(a0, &msg_usage)
    438     %call(&print_cstr)
    439     %li(a0, 2)
    440 })
    441 
    442 # =========================================================================
    443 # Reader -- parse_one over readbuf with a single byte cursor
    444 # =========================================================================
    445 #
    446 # Cursor lives in &readbuf_pos; readbuf_len holds the slurped byte count.
    447 # The reader is called recursively from parse_list, so every state goes
    448 # through frame slots, not s-registers.
    449 
    450 # Skip whitespace (ASCII 32, 9, 10, 13) and `;`-to-LF comments. Returns
    451 # a0 = 1 if readbuf_pos >= readbuf_len after skipping (caller hit EOF),
    452 # else 0. Leaf.
    453 :skip_ws
    454 .scope
    455     %lda_global(t0, t2, &readbuf_pos)
    456     %ld_global(t1, &readbuf_len)
    457     :.loop
    458         %beq(t0, t1, &.done)
    459         %readbuf_byte(a0, t0)
    460         %is_ws_branch(a1, a0, &.step)
    461         %bceq(a0, -59, &.comment, a1)    ; ';'
    462         %b(&.done)
    463         :.comment
    464         # Consume up to and including the next LF, or to EOF.
    465         %addi(t0, t0, 1)
    466         %beq(t0, t1, &.done)
    467         %readbuf_byte(a0, t0)
    468         %bcne(a0, -10, &.comment, a1)    ; LF
    469         :.step
    470         %addi(t0, t0, 1)
    471         %b(&.loop)
    472     :.done
    473 
    474     %st(t0, t2, 0)
    475     %li(a0, 1)
    476     %beq(t0, t1, &.ret)
    477     %li(a0, 0)
    478     :.ret
    479     %ret
    480 .endscope
    481 
    482 # parse_exact_dec(buf=a0, len=a1) -> (raw value=a0, consumed=a1).
    483 # Accepts an optional leading '-' (callers strip '+').  The accumulator is
    484 # checked before every multiply/add, so even an arbitrarily long token cannot
    485 # wrap the host word and re-enter the tagged exact-integer range.
    486 %fn2(parse_exact_dec, {start cursor end digits accum negative quotient remainder}, {
    487     %stl(a0, start)
    488     %stl(a0, cursor)
    489     %add(t0, a0, a1)
    490     %stl(t0, end)
    491     %li(t0, 0)
    492     %stl(t0, accum)
    493     %stl(t0, negative)
    494 
    495     # Optional minus sign.
    496     %ldl(t0, cursor)
    497     %ldl(t1, end)
    498     %beq(t0, t1, &.after_sign)
    499     %lb(t2, t0, 0)
    500     %addi(t2, t2, -45)
    501     %bnez(t2, &.after_sign)
    502     %li(t2, 1)
    503     %stl(t2, negative)
    504     %addi(t0, t0, 1)
    505     %stl(t0, cursor)
    506 
    507     :.after_sign
    508     %ldl(t0, cursor)
    509     %stl(t0, digits)
    510 
    511     # Magnitude limit is the target word's tagged-fixnum maximum, plus
    512     # one for the negative endpoint.
    513     %li(t0, -1)
    514     %shri(t0, t0, 4)
    515     %ldl(t1, negative)
    516     %beqz(t1, &.have_limit)
    517     %addi(t0, t0, 1)
    518     :.have_limit
    519     %li(t1, 10)
    520     %div(t2, t0, t1)
    521     %stl(t2, quotient)
    522     %rem(t2, t0, t1)
    523     %stl(t2, remainder)
    524 
    525     :.loop
    526     %ldl(t0, cursor)
    527     %ldl(t1, end)
    528     %beq(t0, t1, &.done)
    529     %lb(a0, t0, 0)
    530     %addi(a0, a0, -48)
    531     %bltz(a0, &.done)
    532     %li(t2, 9)
    533     %bltu(t2, a0, &.done)
    534 
    535     # accum * 10 + digit <= limit, checked without overflowing.
    536     %ldl(t0, accum)
    537     %ldl(t1, quotient)
    538     %bltu(t1, t0, &.overflow)
    539     %bne(t0, t1, &.accumulate)
    540     %ldl(t2, remainder)
    541     %bltu(t2, a0, &.overflow)
    542     :.accumulate
    543     %li(t1, 10)
    544     %mul(t0, t0, t1)
    545     %add(t0, t0, a0)
    546     %stl(t0, accum)
    547     %ldl(t0, cursor)
    548     %addi(t0, t0, 1)
    549     %stl(t0, cursor)
    550     %b(&.loop)
    551 
    552     :.done
    553     %ldl(t0, cursor)
    554     %ldl(t1, digits)
    555     %beq(t0, t1, &.no_digits)
    556     %ldl(a0, accum)
    557     %ldl(t1, negative)
    558     %beqz(t1, &.return)
    559     %li(t1, 0)
    560     %sub(a0, t1, a0)
    561     :.return
    562     %ldl(a1, cursor)
    563     %ldl(t0, start)
    564     %sub(a1, a1, t0)
    565     %eret
    566 
    567     :.no_digits
    568     %li(a0, 0)
    569     %li(a1, 0)
    570     %eret
    571 
    572     :.overflow
    573     %die(msg_integer_overflow)
    574 })
    575 
    576 # parse_one() -> tagged value in a0
    577 %fn2(parse_one, {number_len number_neg}, {
    578     %call(&skip_ws)
    579     %bnez(a0, &.eof)
    580 
    581     %ld_global(t0, &readbuf_pos)
    582     %readbuf_byte(a0, t0)
    583 
    584     %bceq(a0, -40, &.lparen, a1)
    585     %bceq(a0, -41, &.rparen, a1)
    586     %bceq(a0, -35, &.hash, a1)
    587     %bceq(a0, -39, &.quote, a1)
    588     %bceq(a0, -44, &.comma, a1)
    589     %bceq(a0, -34, &.string, a1)
    590     %bceq(a0, -96, &.unsupported, a1)    ; backquote
    591     %bceq(a0, -124, &.unsupported, a1)   ; vertical-bar identifier
    592 
    593     %tail(&parse_atom)
    594 
    595     :.lparen
    596     # Consume '(' and read items until ')'.
    597     %lda_global(t1, t0, &readbuf_pos)
    598     %readbuf_advance(t1, t0)
    599     %tail(&parse_list)
    600 
    601     :.rparen
    602     %die(msg_unexp_rparen)
    603 
    604     :.string
    605     # Consume opening '"' and tail to parse_string. parse_string scans
    606     # through the matching '"' (consuming it) and returns a tagged bv.
    607     %lda_global(t1, t0, &readbuf_pos)
    608     %readbuf_advance(t1, t0)
    609     %tail(&parse_string)
    610 
    611     :.hash
    612     # Consume '#' plus its type byte; dispatch on the type byte.
    613     %lda_global(t0, t2, &readbuf_pos)
    614     %addi(t0, t0, 1)
    615     %readbuf_at_eof(t0, t1, &.eof)
    616     %readbuf_byte(a0, t0)
    617     %readbuf_advance(t0, t2)
    618     %bceq(a0, -116, &.true_lit,  a1)    ; 't'
    619     %bceq(a0, -102, &.false_lit, a1)    ; 'f'
    620     %bceq(a0, -120, &.hex_lit,   a1)    ; 'x'
    621     %bceq(a0,  -92, &.char_lit,  a1)    ; '\\'
    622     %bceq(a0, -117, &.u8_lit,    a1)    ; 'u'
    623     %die(msg_bad_hash)
    624 
    625     :.true_lit
    626     %li(a2, %imm_val(%IMM.TRUE))
    627     %b(&.boolean_lit)
    628 
    629     :.false_lit
    630     %li(a2, %imm_val(%IMM.FALSE))
    631 
    632     :.boolean_lit
    633     # A boolean token ends after its one-letter spelling. Without this
    634     # check `#true` would silently become two adjacent top-level datums.
    635     %ld_global(t0, &readbuf_pos)
    636     %ld_global(t1, &readbuf_len)
    637     %beq(t0, t1, &.boolean_done)
    638     %readbuf_byte(t2, t0)
    639     %is_token_delimiter_branch(a3, t2, &.boolean_done)
    640     %die(msg_bad_hash)
    641     :.boolean_done
    642     %mov(a0, a2)
    643     %eret
    644 
    645     :.hex_lit
    646     # t0 sits at the first hex digit; t1 = readbuf_len. Scan to a token
    647     # delimiter, then parse_hex over the slice with an optional sign.
    648     %mov(a3, t0)
    649     :.hex_scan
    650         %beq(t0, t1, &.hex_end)
    651         %readbuf_byte(a0, t0)
    652         %is_token_delimiter_branch(a1, a0, &.hex_end)
    653         %addi(t0, t0, 1)
    654         %b(&.hex_scan)
    655     :.hex_end
    656 
    657     %st_global(t0, &readbuf_pos, t2)
    658     %ld_global(a0, &readbuf_buf_ptr)
    659     %add(a0, a0, a3)
    660     %sub(a1, t0, a3)
    661     %beqz(a1, &.number_bad)
    662     %li(t0, 0)
    663     %stl(t0, number_neg)
    664     %lb(t2, a0, 0)
    665     %addi(t2, t2, -45)              ; '-'
    666     %beqz(t2, &.hex_neg)
    667     %lb(t2, a0, 0)
    668     %addi(t2, t2, -43)              ; '+'
    669     %beqz(t2, &.hex_pos_sign)
    670     %b(&.hex_parse)
    671     :.hex_neg
    672     %li(t0, 1)
    673     %stl(t0, number_neg)
    674     :.hex_pos_sign
    675     %addi(a0, a0, 1)
    676     %addi(a1, a1, -1)
    677     %beqz(a1, &.number_bad)
    678     :.hex_parse
    679     # Strip redundant leading zeroes so a long but representable spelling
    680     # is accepted without allowing parse_hex's machine-word accumulator to
    681     # wrap. At most one target word of significant hex digits remains.
    682     :.hex_trim_zero
    683     %lb(t1, a0, 0)
    684     %addi(t1, t1, -48)
    685     %bnez(t1, &.hex_count_check)
    686     %li(t0, 1)
    687     %beq(a1, t0, &.hex_count_check)
    688     %addi(a0, a0, 1)
    689     %addi(a1, a1, -1)
    690     %b(&.hex_trim_zero)
    691     :.hex_count_check
    692     %li(t0, (+ (/ %p1_word_bits 4) 1))
    693     %bltu(a1, t0, &.hex_parse_go)
    694     %b(&.integer_overflow)
    695     :.hex_parse_go
    696     %stl(a1, number_len)
    697     %call(&parse_hex)
    698     %ldl(t0, number_len)
    699     %bne(a1, t0, &.number_bad)
    700     # Compare the unsigned magnitude before applying the sign. Positive
    701     # Positive values allow the target tagged-fixnum maximum; one extra
    702     # magnitude is valid only for the negative endpoint.
    703     %li(t0, -1)
    704     %shri(t0, t0, 4)
    705     %ldl(t1, number_neg)
    706     %beqz(t1, &.hex_limit_ready)
    707     %addi(t0, t0, 1)
    708     :.hex_limit_ready
    709     %bltu(t0, a0, &.integer_overflow)
    710     %ldl(t0, number_neg)
    711     %beqz(t0, &.hex_tag)
    712     %li(t0, 0)
    713     %sub(a0, t0, a0)
    714     :.hex_tag
    715     %mov(t0, a0)
    716     %mkfix(a0, t0)
    717     %untag_fix(t1, a0)
    718     %bne(t0, t1, &.integer_overflow)
    719     %eret
    720 
    721     :.quote
    722     # Consume the leading '\''; recurse into parse_one for the datum;
    723     # then build (quote <datum>).
    724     %lda_global(t0, t2, &readbuf_pos)
    725     %readbuf_advance(t0, t2)
    726     %call(&parse_one)
    727     %li(a1, %imm_val(%IMM.NIL))
    728     %call(&cons)
    729     %ld_global(t0, &sym_quote)
    730     %mov(a1, a0)
    731     %mov(a0, t0)
    732     %tail(&cons)
    733 
    734     :.comma
    735     # Consume the leading ','; recurse into parse_one for the datum;
    736     # build (unquote <datum>). The comma sugar exists so pmatch
    737     # patterns can be written as `,ident`. Outside pmatch
    738     # `(unquote x)` reaches eval as an application of the unbound
    739     # `unquote` and dies through the standard unbound-variable path.
    740     %lda_global(t0, t2, &readbuf_pos)
    741     %readbuf_advance(t0, t2)
    742     %call(&parse_one)
    743     %li(a1, %imm_val(%IMM.NIL))
    744     %call(&cons)
    745     %ld_global(t0, &sym_unquote)
    746     %mov(a1, a0)
    747     %mov(a0, t0)
    748     %tail(&cons)
    749 
    750     :.char_lit
    751     # Cursor is already past '#\\'; parse_char scans the body and returns
    752     # a disjoint tagged character.
    753     %tail(&parse_char)
    754 
    755     :.u8_lit
    756     # Cursor is past '#u'. Demand '8' then '('; consume both and tail to
    757     # parse_u8_body, which reads the element list and packs it into a bv.
    758     %lda_global(t0, t2, &readbuf_pos)
    759     %readbuf_at_eof(t0, t1, &.u8_bad)
    760     %readbuf_byte(a0, t0)
    761     %bcne(a0, -56, &.u8_bad, a1)    ; '8'
    762     %addi(t0, t0, 1)
    763     %beq(t0, t1, &.u8_bad)
    764     %readbuf_byte(a0, t0)
    765     %bcne(a0, -40, &.u8_bad, a1)    ; '('
    766     %readbuf_advance(t0, t2)
    767     %tail(&parse_u8_body)
    768 
    769     :.u8_bad
    770     %die(msg_bad_hash)
    771 
    772     :.number_bad
    773     %die(msg_bad_number)
    774 
    775     :.integer_overflow
    776     %die(msg_integer_overflow)
    777 
    778     :.unsupported
    779     %die(msg_unsupported_syntax)
    780 
    781     :.eof
    782     %die(msg_unexp_eof)
    783 })
    784 
    785 # parse_list() -> tagged list value in a0. Cursor sits past '(' on entry;
    786 # returns once ')' is consumed.
    787 #
    788 # Locals:
    789 #   head  NIL until first item
    790 #   tail  most recent cons (set-cdr! target)
    791 %gcfn2(parse_list, {head tail}, 3, 0, {
    792     %li(t0, %imm_val(%IMM.NIL))
    793     %stl(t0, head)
    794     %stl(t0, tail)
    795 
    796     :.loop
    797     %call(&skip_ws)
    798     %bnez(a0, &.eof)
    799     %ld_global(t0, &readbuf_pos)
    800     %ld_global(t1, &readbuf_len)
    801 
    802     %readbuf_byte(a0, t0)
    803     %bceq(a0, -41, &.close, a1)
    804 
    805     # Dotted-pair separator: '.' followed by ws/paren/EOF (otherwise the
    806     # '.' is part of an identifier and parse_atom handles it).
    807     %bcne(a0, -46, &.not_dot, a1)    ; '.'
    808     %addi(a2, t0, 1)
    809     %beq(a2, t1, &.do_dot)
    810     %readbuf_byte(a3, a2)
    811     %is_ws_branch(a1, a3, &.do_dot)
    812     %bceq(a3, -40, &.do_dot, a1)
    813     %bceq(a3, -41, &.do_dot, a1)
    814     :.not_dot
    815 
    816     # Not ')': parse one item, append.
    817     %call(&parse_one)
    818     %li(a1, %imm_val(%IMM.NIL))
    819     %call(&cons)
    820 
    821     # If head is NIL, both head and tail = new cons; else set-cdr! tail = new.
    822     %ldl(t0, head)
    823     %bine(t0, %imm_val(%IMM.NIL), &.link, t1)
    824     %stl(a0, head)
    825     %stl(a0, tail)
    826     %b(&.loop)
    827 
    828     :.link
    829     %ldl(t0, tail)
    830     # set-cdr! tail = a0  -> store a0 at [tail + 7] (raw + 8)
    831     %set_cdr(a0, t0)
    832     %stl(a0, tail)
    833     %b(&.loop)
    834 
    835     :.do_dot
    836     # Consume the '.', read one datum, splice it in as the cdr of the
    837     # tail cons. Then expect a closing ')' (with optional ws).
    838     %lda_global(t0, t1, &readbuf_pos)
    839     %readbuf_advance(t0, t1)
    840     %call(&parse_one)
    841     %ldl(t0, tail)
    842     %set_cdr(a0, t0)
    843     %call(&skip_ws)
    844     %bnez(a0, &.eof)
    845     %lda_global(t0, t1, &readbuf_pos)
    846     %readbuf_byte(a0, t0)
    847     %bcne(a0, -41, &.eof, a1)    ; ')'
    848     %readbuf_advance(t0, t1)
    849     %ldl(a0, head)
    850     %gceret
    851 
    852     :.close
    853     # Consume ')' and return head.
    854     %lda_global(t1, t0, &readbuf_pos)
    855     %readbuf_advance(t1, t0)
    856     %ldl(a0, head)
    857     %gceret
    858 
    859     :.eof
    860     %die(msg_unterm_list)
    861 })
    862 
    863 # parse_u8_body() -> tagged HDR.BV in a0. Cursor sits past '#u8(' on
    864 # entry. Reads elements via parse_list; every element must be a fixnum byte
    865 # in 0..255, then packs them into a fresh bytevector.
    866 #
    867 # Locals:
    868 #   list    parsed element list (cursor during fill pass)
    869 #   result  freshly allocated bv
    870 %gcfn2(parse_u8_body, {list result}, 3, 0, {
    871     %call(&parse_list)
    872     %stl(a0, list)
    873 
    874     %call(&list_length)             ; clobbers a0 -> count
    875     %call(&bv_alloc)                ; a0 = bv
    876     %stl(a0, result)
    877 
    878     %heap_ld(t0, a0, %BV.data)
    879     %ldl(a0, list)                  ; list cursor
    880 
    881     :.loop
    882         %if_nil(t1, a0, &.done)
    883         %car(t1, a0)
    884         %tagof(t2, t1)
    885         %bine(t2, %TAG.FIXNUM, &.bad, a1)
    886         %untag_fix(t1, t1)
    887         %bltz(t1, &.bad)
    888         %li(t2, 256)
    889         %bltu(t1, t2, &.store)
    890         %b(&.bad)
    891         :.store
    892         %sb(t1, t0, 0)
    893         %addi(t0, t0, 1)
    894         %cdr(a0, a0)
    895         %b(&.loop)
    896     :.done
    897     %ldl(a0, result)
    898     %gceret
    899 
    900     :.bad
    901     %die(msg_bad_byte)
    902 })
    903 
    904 # is_ident_byte(c=a0) -> a1 (1 if c is a valid identifier byte, else 0).
    905 # Leaf. Allowed bytes are R7RS-Small's identifier set: ASCII letters,
    906 # digits, and the extended chars  ! $ % & * + - . / : < = > ? @ ^ _ ~ .
    907 # Clobbers t0, t1, a1.
    908 :is_ident_byte
    909 .scope
    910     %brange(a0, -48, 10, t0, t1, &.ok)    ; '0'..'9'
    911     %brange(a0, -65, 26, t0, t1, &.ok)    ; 'A'..'Z'
    912     %brange(a0, -97, 26, t0, t1, &.ok)    ; 'a'..'z'
    913 
    914     %bceq(a0,  -33, &.ok, t0)    ; '!'
    915     %bceq(a0,  -36, &.ok, t0)    ; '$'
    916     %bceq(a0,  -37, &.ok, t0)    ; '%'
    917     %bceq(a0,  -38, &.ok, t0)    ; '&'
    918     %bceq(a0,  -42, &.ok, t0)    ; '*'
    919     %bceq(a0,  -43, &.ok, t0)    ; '+'
    920     %bceq(a0,  -45, &.ok, t0)    ; '-'
    921     %bceq(a0,  -46, &.ok, t0)    ; '.'
    922     %bceq(a0,  -47, &.ok, t0)    ; '/'
    923     %bceq(a0,  -58, &.ok, t0)    ; ':'
    924     %bceq(a0,  -60, &.ok, t0)    ; '<'
    925     %bceq(a0,  -61, &.ok, t0)    ; '='
    926     %bceq(a0,  -62, &.ok, t0)    ; '>'
    927     %bceq(a0,  -63, &.ok, t0)    ; '?'
    928     %bceq(a0,  -64, &.ok, t0)    ; '@'
    929     %bceq(a0,  -94, &.ok, t0)    ; '^'
    930     %bceq(a0,  -95, &.ok, t0)    ; '_'
    931     %bceq(a0, -126, &.ok, t0)    ; '~'
    932 
    933     %li(a1, 0)
    934     %ret
    935 
    936     :.ok
    937     %li(a1, 1)
    938     %ret
    939 .endscope
    940 
    941 # parse_atom() -> tagged value (fixnum or symbol) in a0.
    942 # Reads until whitespace or paren or EOF, then dispatches by first byte.
    943 # A token whose first byte is a digit (or sign-then-digit) commits to
    944 # parse_dec and any non-numeric byte aborts; otherwise the token is a
    945 # symbol and every byte is checked against is_ident_byte before intern.
    946 #
    947 # Locals:
    948 #   start  cursor (byte offset)
    949 #   end  cursor   (byte offset)
    950 #   cursor  (scratch slot for the symbol-validation loop)
    951 %fn2(parse_atom, {start end cursor}, {
    952     %lda_global(t1, t0, &readbuf_pos)
    953     %stl(t1, start)
    954 
    955     %ld_global(t2, &readbuf_len)
    956 
    957     :.scan
    958     %beq(t1, t2, &.end)
    959     %readbuf_byte(a0, t1)
    960 
    961     %is_token_delimiter_branch(a1, a0, &.end)
    962 
    963     %addi(t1, t1, 1)
    964     %b(&.scan)
    965 
    966     :.end
    967     %stl(t1, end)
    968     %st(t1, t0, 0)
    969 
    970     # Dispatch on the first byte.
    971     %ldl(t0, start)
    972     %ld_global(a0, &readbuf_buf_ptr)
    973     %add(a0, a0, t0)
    974     %lb(t1, a0, 0)
    975 
    976     # '0'..'9' -> int
    977     %addi(a1, t1, -48)
    978     %li(a2, 10)
    979     %bltu(a1, a2, &.is_int)
    980     # '-' or '+' followed by digit -> int. A lone '+' or '-' falls
    981     # through to is_sym (those tokens stay valid identifiers).
    982     %bceq(t1, -45, &.sign, a1)    ; '-'
    983     %bceq(t1, -43, &.sign, a1)    ; '+'
    984     %b(&.is_sym)
    985     :.sign
    986     %ldl(t2, end)
    987     %addi(t0, t0, 1)
    988     %beq(t0, t2, &.is_sym)
    989     %readbuf_byte(a0, t0)
    990     %addi(a1, a0, -48)
    991     %bltu(a1, a2, &.is_int)
    992     # fall through to is_sym
    993 
    994     :.is_sym
    995     # Validate every byte; abort on the first non-ident byte.
    996     %ldl(t0, start)
    997     %stl(t0, cursor)
    998     :.sym_loop
    999     %ldl(t0, cursor)
   1000     %ldl(t1, end)
   1001     %beq(t0, t1, &.sym_intern)
   1002     %readbuf_byte(a0, t0)
   1003     %call(&is_ident_byte)
   1004     %beqz(a1, &.sym_bad)
   1005     %ldl(t0, cursor)
   1006     %addi(t0, t0, 1)
   1007     %stl(t0, cursor)
   1008     %b(&.sym_loop)
   1009 
   1010     :.sym_bad
   1011     %die(msg_bad_ident)
   1012 
   1013     :.sym_intern
   1014     %ldl(a0, start)
   1015     %ld_global(t0, &readbuf_buf_ptr)
   1016     %add(a0, t0, a0)
   1017     %ldl(t1, end)
   1018     %ldl(t2, start)
   1019     %sub(a1, t1, t2)
   1020     %tail(&intern)
   1021 
   1022     :.is_int
   1023     %ldl(t0, start)
   1024     %ldl(t1, end)
   1025     %ld_global(a0, &readbuf_buf_ptr)
   1026     %add(a0, a0, t0)
   1027     %sub(a1, t1, t0)            ; len = end - start
   1028     # P1pp's parse_dec handles '-' but not '+'; strip '+' here.
   1029     %lb(t2, a0, 0)
   1030     %bcne(t2, -43, &.no_plus, t2)    ; '+'
   1031     %addi(a0, a0, 1)
   1032     %addi(a1, a1, -1)
   1033     :.no_plus
   1034     %stl(a1, cursor)            ; save adjusted len (cursor slot is free on int path)
   1035     %call(&parse_exact_dec)     ; -> (raw_val=a0, consumed=a1)
   1036     %ldl(t0, cursor)
   1037     %bne(a1, t0, &.int_bad)    ; partial parse -> bad
   1038     %mov(t0, a0)
   1039     %mkfix(a0, t0)
   1040     %untag_fix(t1, a0)
   1041     %bne(t0, t1, &.int_overflow)
   1042     %eret
   1043     :.int_bad
   1044     %die(msg_bad_number)
   1045     :.int_overflow
   1046     %die(msg_integer_overflow)
   1047 })
   1048 
   1049 # parse_string() -> tagged string in a0. Cursor sits past the
   1050 # opening '"' (consumed by parse_one). Two-pass: pass 1 walks the body,
   1051 # counting decoded bytes in a0 and locating the closing '"'; pass 2
   1052 # allocates the string and decodes into its data buffer. Each named escape
   1053 # (\n \t \r \\ \") yields one byte; an inline-hex escape \xHEX; (1+
   1054 # hex digits, value 0..255, terminated by ';') also yields one byte.
   1055 #
   1056 # Locals:
   1057 #   start  cursor (first content byte)
   1058 #   end  cursor   (closing '"' position)
   1059 #   bv  string wrapper   (saved across the data fill loop)
   1060 #   spill  slot   (write ptr saved across parse_hex in \x escape)
   1061 %fn2(parse_string, {start end bv spill hex_len}, {
   1062     %ld_global(t1, &readbuf_pos)
   1063     %stl(t1, start)
   1064 
   1065     %ld_global(t2, &readbuf_len)
   1066 
   1067     %li(a0, 0)
   1068     :.scan
   1069         %beq(t1, t2, &.eof)
   1070         %readbuf_byte(a3, t1)
   1071         %bceq(a3, -34, &.scan_done, a1)    ; '"'
   1072         %bceq(a3, -92, &.scan_esc,  a1)    ; '\\'
   1073         %addi(t1, t1, 1)
   1074         %addi(a0, a0, 1)
   1075         %b(&.scan)
   1076 
   1077         :.scan_esc
   1078         # Backslash plus the next byte yield one decoded byte. \xHEX; runs
   1079         # until the terminating ';' (validated in pass 2); every other escape
   1080         # is exactly two source bytes.
   1081         %addi(t1, t1, 1)
   1082         %beq(t1, t2, &.eof)
   1083         %readbuf_byte(a3, t1)
   1084         %bceq(a3, -120, &.scan_hex, a1)    ; 'x'
   1085         %addi(t1, t1, 1)
   1086         %addi(a0, a0, 1)
   1087         %b(&.scan)
   1088 
   1089         :.scan_hex
   1090         # Skip past 'x' and scan to the terminating ';'. EOF before ';'
   1091         # falls into the unterminated-string path below, matching how an
   1092         # unterminated body is reported.
   1093         %addi(t1, t1, 1)
   1094         :.scan_hex_loop
   1095         %beq(t1, t2, &.eof)
   1096         %readbuf_byte(a3, t1)
   1097         %bceq(a3, -59, &.scan_hex_done, a1)    ; ';'
   1098         %addi(t1, t1, 1)
   1099         %b(&.scan_hex_loop)
   1100         :.scan_hex_done
   1101         %addi(t1, t1, 1)                ; consume ';'
   1102         %addi(a0, a0, 1)                ; +1 output byte
   1103         %b(&.scan)
   1104     :.scan_done
   1105 
   1106     %stl(t1, end)
   1107     %call(&string_alloc)
   1108     %stl(a0, bv)
   1109 
   1110     # Pass 2: decode into the freshly allocated data buffer.
   1111     %ldl(t1, start)
   1112     %ldl(t2, end)
   1113     %heap_ld(a3, a0, %BV.data)
   1114 
   1115     :.fill
   1116     %beq(t1, t2, &.fill_done)
   1117     %readbuf_byte(a1, t1)
   1118     %bceq(a1, -92, &.fill_esc, a2)    ; '\\'
   1119     %sb(a1, a3, 0)
   1120     %addi(a3, a3, 1)
   1121     %addi(t1, t1, 1)
   1122     %b(&.fill)
   1123 
   1124     :.fill_esc
   1125     %addi(t1, t1, 1)                ; consume backslash
   1126     %readbuf_byte(a1, t1)
   1127     %bceq(a1, -110, &.esc_n,      a2)    ; 'n'
   1128     %bceq(a1, -116, &.esc_t,      a2)    ; 't'
   1129     %bceq(a1, -114, &.esc_r,      a2)    ; 'r'
   1130     %bceq(a1,  -92, &.write_byte, a2)    ; '\\'
   1131     %bceq(a1,  -34, &.write_byte, a2)    ; '"'
   1132     %bceq(a1, -120, &.esc_hex,    a2)    ; 'x'
   1133     %die(msg_bad_escape)
   1134 
   1135     :.esc_n
   1136     %li(a1, 10)
   1137     %b(&.write_byte)
   1138     :.esc_t
   1139     %li(a1, 9)
   1140     %b(&.write_byte)
   1141     :.esc_r
   1142     %li(a1, 13)
   1143     :.write_byte
   1144     %sb(a1, a3, 0)
   1145     %addi(a3, a3, 1)
   1146     %addi(t1, t1, 1)
   1147     %b(&.fill)
   1148 
   1149     :.esc_hex
   1150     # Skip past 'x', locate the immediate ';', and normalize leading zeroes.
   1151     # A byte has at most two significant hex digits; capping before parse_hex
   1152     # prevents a huge escape from wrapping the host word back into 0..255.
   1153     %addi(t1, t1, 1)                ; t1 -> first hex digit
   1154     %stl(a3, spill)
   1155     %mov(t0, t1)
   1156     :.esc_hex_scan
   1157     %beq(t0, t2, &.hex_bad)
   1158     %readbuf_byte(a1, t0)
   1159     %bceq(a1, -59, &.esc_hex_found, a2)
   1160     %addi(t0, t0, 1)
   1161     %b(&.esc_hex_scan)
   1162     :.esc_hex_found
   1163     %stl(t0, start)                 ; semicolon cursor
   1164     %sub(a1, t0, t1)                ; digit count
   1165     %beqz(a1, &.hex_bad)
   1166     :.esc_hex_trim
   1167     %li(a2, 1)
   1168     %beq(a1, a2, &.esc_hex_count)
   1169     %readbuf_byte(a0, t1)
   1170     %bcne(a0, -48, &.esc_hex_count, a2)
   1171     %addi(t1, t1, 1)
   1172     %addi(a1, a1, -1)
   1173     %b(&.esc_hex_trim)
   1174     :.esc_hex_count
   1175     %li(a2, 3)
   1176     %bltu(a1, a2, &.esc_hex_parse)
   1177     %b(&.hex_bad)
   1178     :.esc_hex_parse
   1179     %stl(a1, hex_len)
   1180     %ld_global(t0, &readbuf_buf_ptr)
   1181     %add(a0, t0, t1)                ; ptr to first hex digit
   1182     %call(&parse_hex)               ; -> (a0=value, a1=consumed)
   1183     %ldl(t0, hex_len)
   1184     %bne(a1, t0, &.hex_bad)
   1185     %ldl(t1, start)                  ; semicolon cursor
   1186     %addi(t1, t1, 1)                ; consume ';'
   1187     %ldl(t2, end)                    ; parse_hex clobbered the body end
   1188     %ldl(a3, spill)
   1189     %sb(a0, a3, 0)
   1190     %addi(a3, a3, 1)
   1191     %b(&.fill)
   1192 
   1193     :.hex_bad
   1194     %die(msg_bad_escape)
   1195 
   1196     :.fill_done
   1197     %addi(t1, t1, 1)                ; consume closing '"'
   1198     %st_global(t1, &readbuf_pos, t0)
   1199     %ldl(a0, bv)
   1200     %eret
   1201 
   1202     :.eof
   1203     %die(msg_unterm_string)
   1204 })
   1205 
   1206 # Emit one named-char arm inside parse_char's multi-byte dispatch. t2
   1207 # must hold the slice pointer; ::bad must be in scope. name_label is a
   1208 # full label reference (e.g. &name_ch_tab).
   1209 %macro match_named_char(name_label, len, value)
   1210     %mov(a0, t2)
   1211     %la(a1, name_label)
   1212     %li(a2, len)
   1213     %call(&memcmp)
   1214     %bnez(a0, &.bad)
   1215     %li(a0, value)
   1216     %mkchar(a0, a0)
   1217     %eret
   1218 %endm
   1219 
   1220 # parse_char() -> tagged character (the u8 value) in a0. Cursor sits
   1221 # past '#\\' (consumed by parse_one's hash dispatch). Always consumes
   1222 # at least one byte; then continues until ws/paren/EOF. Single-byte
   1223 # bodies yield that byte; multi-byte bodies dispatch to hex (#\xNN) or
   1224 # named (#\space, #\newline, #\tab, #\return, #\null) forms.
   1225 #
   1226 # Locals:
   1227 #   start  cursor
   1228 #   end  cursor
   1229 %fn2(parse_char, {start end}, {
   1230     %lda_global(t1, t0, &readbuf_pos)
   1231     %stl(t1, start)
   1232 
   1233     %ld_global(t2, &readbuf_len)
   1234 
   1235     %beq(t1, t2, &.short)
   1236 
   1237     # Always consume the first byte unconditionally — it might itself be
   1238     # a delimiter (e.g., '(' in `#\(`) and is still the character value.
   1239     %addi(t1, t1, 1)
   1240 
   1241     :.scan
   1242     %beq(t1, t2, &.scan_done)
   1243     %readbuf_byte(a0, t1)
   1244     %is_token_delimiter_branch(a1, a0, &.scan_done)
   1245     %addi(t1, t1, 1)
   1246     %b(&.scan)
   1247 
   1248     :.scan_done
   1249     %stl(t1, end)
   1250     %st(t1, t0, 0)
   1251 
   1252     %ldl(t0, start)
   1253     %ldl(t1, end)
   1254     %sub(a2, t1, t0)                ; length
   1255 
   1256     %bieq(a2, 1, &.single, a3)
   1257 
   1258     %ld_global(t2, &readbuf_buf_ptr)
   1259     %add(t2, t2, t0)                ; t2 = slice ptr
   1260 
   1261     # Hex form: first byte is 'x'.
   1262     %lb(a0, t2, 0)
   1263     %addi(a1, a0, -120)             ; 'x'
   1264     %beqz(a1, &.hex_form)
   1265 
   1266     # Named form: dispatch on length.
   1267     %bieq(a2, 3, &.try_tab,     a3)
   1268     %bieq(a2, 4, &.try_null,    a3)
   1269     %bieq(a2, 5, &.try_space,   a3)
   1270     %bieq(a2, 6, &.try_return,  a3)
   1271     %bieq(a2, 7, &.try_newline, a3)
   1272     %b(&.bad)
   1273 
   1274     :.single
   1275     %ld_global(t2, &readbuf_buf_ptr)
   1276     %add(t2, t2, t0)
   1277     %lb(a0, t2, 0)
   1278     %mkchar(a0, a0)
   1279     %eret
   1280 
   1281     :.hex_form
   1282     %addi(a0, t2, 1)
   1283     %addi(a1, a2, -1)
   1284     # Strip leading zeroes and reject more than two significant digits
   1285     # before calling the machine-word parser.
   1286     :.hex_trim
   1287     %li(t0, 1)
   1288     %beq(a1, t0, &.hex_count)
   1289     %lb(t1, a0, 0)
   1290     %addi(t1, t1, -48)
   1291     %bnez(t1, &.hex_count)
   1292     %addi(a0, a0, 1)
   1293     %addi(a1, a1, -1)
   1294     %b(&.hex_trim)
   1295     :.hex_count
   1296     %li(t0, 3)
   1297     %bltu(a1, t0, &.hex_parse)
   1298     %b(&.bad)
   1299     :.hex_parse
   1300     %stl(a1, start)
   1301     %call(&parse_hex)
   1302     %ldl(t0, start)
   1303     %bne(a1, t0, &.bad)
   1304     %li(t0, 256)
   1305     %bltu(a0, t0, &.hex_ok)
   1306     %b(&.bad)
   1307     :.hex_ok
   1308     %mkchar(a0, a0)
   1309     %eret
   1310 
   1311     :.try_tab
   1312     %match_named_char(&name_ch_tab, 3, 9)
   1313 
   1314     :.try_null
   1315     %match_named_char(&name_ch_null, 4, 0)
   1316 
   1317     :.try_space
   1318     %match_named_char(&name_ch_space, 5, 32)
   1319 
   1320     :.try_return
   1321     %match_named_char(&name_ch_return, 6, 13)
   1322 
   1323     :.try_newline
   1324     %match_named_char(&name_ch_newline, 7, 10)
   1325 
   1326     :.bad
   1327     %die(msg_bad_char)
   1328 
   1329     :.short
   1330     %die(msg_bad_char)
   1331 })
   1332 
   1333 
   1334 # =========================================================================
   1335 # eval / apply
   1336 # =========================================================================
   1337 
   1338 # eval(expr=a0, env=a1) -> value (a0)
   1339 #
   1340 # Locals:
   1341 #   expr
   1342 #   env
   1343 #   fn  (head value, while args are being evaluated)
   1344 #   pad  (first evaluated argument on the direct record-mutator path)
   1345 %gcfn2(eval, {expr env fn pad}, 15, 0, {
   1346     %stl(a0, expr)
   1347     %stl(a1, env)
   1348 
   1349     %tagof(t0, a0)
   1350     %bieq(t0, %TAG.SYM,  &.sym,  t1)
   1351     %bieq(t0, %TAG.PAIR, &.pair, t1)
   1352     # FIXNUM, HEAP, IMM all self-evaluate.
   1353     %gceret
   1354 
   1355     :.sym
   1356     # The cumulative environment filter rejects most global references in
   1357     # constant time. A possible lexical hit is resolved by exact symbol
   1358     # identity along the parent chain. a0 still holds sym; a1 holds env.
   1359     %if_nil(t0, a1, &.env_miss)
   1360     %call(&env_find)
   1361     %beqz(a0, &.env_miss)
   1362     %heap_ld(a0, a0, %ENV.value)
   1363     %gceret
   1364 
   1365     :.env_miss
   1366     %ldl(a0, expr)
   1367     %untag_sym(a0, a0)
   1368     %call(&sym_global)
   1369     %bieq(a0, %imm_val(%IMM.UNBOUND), &.unbound, t0)
   1370     %gceret
   1371 
   1372     :.unbound
   1373     %die(msg_unbound)
   1374 
   1375 
   1376     :.pair
   1377 
   1378     # Special-form dispatch: pointer-compare head against the cached
   1379     # special-form symbol values. SYM is a distinct tag, so a head that
   1380     # isn't a symbol cannot collide with any sym_* slot.
   1381     %ldl(t0, expr)
   1382     %car(t0, t0)            ; t0 = head
   1383     %dispatch_form(&sym_quote,   &.do_quote)
   1384     %dispatch_form(&sym_if,      &.do_if)
   1385     %dispatch_form(&sym_lambda,  &.do_lambda)
   1386     %dispatch_form(&sym_define,  &.do_define)
   1387     %dispatch_form(&sym_begin,   &.do_begin)
   1388     %dispatch_form(&sym_cond,    &.do_cond)
   1389     %dispatch_form(&sym_let,     &.do_let)
   1390     %dispatch_form(&sym_letstar, &.do_letstar)
   1391     %dispatch_form(&sym_let_values, &.do_let_values)
   1392     %dispatch_form(&sym_letstar_values, &.do_letstar_values)
   1393     %dispatch_form(&sym_and,     &.do_and)
   1394     %dispatch_form(&sym_or,      &.do_or)
   1395     %dispatch_form(&sym_when,    &.do_when)
   1396     %dispatch_form(&sym_case,    &.do_case)
   1397     %dispatch_form(&sym_setbang, &.do_setbang)
   1398     %dispatch_form(&sym_define_record_type, &.do_define_record_type)
   1399     %dispatch_form(&sym_pmatch,  &.do_pmatch)
   1400     %dispatch_form(&sym_do,      &.do_do)
   1401 
   1402     # Apply car to cdr
   1403     # fn = eval(car(expr), env)
   1404     %ldl(a0, expr)
   1405     %car(a0, a0)
   1406     %ldl(a1, env)
   1407     %call(&eval)
   1408     %stl(a0, fn)
   1409 
   1410     # Generated record predicates/accessors/mutators dominate cc.scm's
   1411     # primitive traffic. For their exact call shapes, evaluate directly into
   1412     # registers and skip construction of the transient evaluated-argument
   1413     # list. Calls with another arity retain the generic path (and therefore
   1414     # its existing evaluation/error behavior).
   1415     %tagof(t0, a0)
   1416     %bine(t0, %TAG.HEAP, &.generic_apply, t1)
   1417     %hdr_type(t0, a0)
   1418     %bine(t0, %HDR.PRIM, &.generic_apply, t1)
   1419     %heap_ld(t0, a0, %PRIM.entry_w)
   1420     %la(t1, &prim_predicate_entry)
   1421     %beq(t0, t1, &.fast_record_unary)
   1422     %la(t1, &prim_accessor_entry)
   1423     %beq(t0, t1, &.fast_record_unary)
   1424     %la(t1, &prim_mutator_entry)
   1425     %beq(t0, t1, &.fast_record_mutator)
   1426     %b(&.generic_apply)
   1427 
   1428     :.fast_record_unary
   1429     # Require exactly one expression argument.
   1430     %ldl(t0, expr)
   1431     %cdr(t0, t0)
   1432     %tagof(t1, t0)
   1433     %bine(t1, %TAG.PAIR, &.generic_apply, t2)
   1434     %cdr(t1, t0)
   1435     %if_nil(t2, t1, &.fast_record_unary_eval)
   1436     %b(&.generic_apply)
   1437 
   1438     :.fast_record_unary_eval
   1439     %car(a0, t0)
   1440     %ldl(a1, env)
   1441     %call(&eval)
   1442     %ldl(t0, fn)
   1443     %heap_ld(t1, t0, %PRIM.entry_w)
   1444     %la(t2, &prim_accessor_entry)
   1445     %beq(t1, t2, &.fast_record_accessor_done)
   1446 
   1447     # Predicate: prim.data is the TD and a0 is the candidate record.
   1448     %heap_ld(t1, t0, %PRIM.data)
   1449     %tagof(t2, a0)
   1450     %li(t0, %imm_val(%IMM.FALSE))
   1451     %bine(t2, %TAG.HEAP, &.fast_record_pred_done, a1)
   1452     %hdr_type(t2, a0)
   1453     %bine(t2, %HDR.REC, &.fast_record_pred_done, a1)
   1454     %heap_ld(t2, a0, %REC.td)
   1455     %bne(t2, t1, &.fast_record_pred_done)
   1456     %li(t0, %imm_val(%IMM.TRUE))
   1457     :.fast_record_pred_done
   1458     %mov(a0, t0)
   1459     %gceret
   1460 
   1461     :.fast_record_accessor_done
   1462     # Accessor: prim.data is a tagged field index.
   1463     %heap_ld(t1, t0, %PRIM.data)
   1464     %addi(t1, t1, 13)
   1465     %add(t1, t1, a0)
   1466     %ld(a0, t1, 0)
   1467     %gceret
   1468 
   1469     :.fast_record_mutator
   1470     # Require exactly two expression arguments.
   1471     %ldl(t0, expr)
   1472     %cdr(t0, t0)
   1473     %tagof(t1, t0)
   1474     %bine(t1, %TAG.PAIR, &.generic_apply, t2)
   1475     %cdr(t1, t0)
   1476     %tagof(t2, t1)
   1477     %bine(t2, %TAG.PAIR, &.generic_apply, a0)
   1478     %cdr(t2, t1)
   1479     %if_nil(a0, t2, &.fast_record_mutator_eval)
   1480     %b(&.generic_apply)
   1481 
   1482     :.fast_record_mutator_eval
   1483     %car(a0, t0)
   1484     %ldl(a1, env)
   1485     %call(&eval)
   1486     %stl(a0, pad)
   1487     %ldl(t0, expr)
   1488     %cdr(t0, t0)
   1489     %cdr(t0, t0)
   1490     %car(a0, t0)
   1491     %ldl(a1, env)
   1492     %call(&eval)
   1493     %ldl(t0, fn)
   1494     %heap_ld(t1, t0, %PRIM.data)
   1495     %ldl(t0, pad)
   1496     %addi(t1, t1, 13)
   1497     %add(t1, t1, t0)
   1498     %st(a0, t1, 0)
   1499     %li(a0, %imm_val(%IMM.UNSPEC))
   1500     %gceret
   1501 
   1502     :.generic_apply
   1503     # args = eval_args(cdr(expr), env)
   1504     %ldl(a0, expr)
   1505     %cdr(a0, a0)
   1506     %ldl(a1, env)
   1507     %call(&eval_args)
   1508     # apply(fn, args) -- tail call
   1509     %mov(a1, a0)
   1510     %ldl(a0, fn)
   1511     %gctail(&apply)
   1512 
   1513     :.do_quote
   1514     %tail_to_handler(&eval_quote)
   1515     :.do_if
   1516     %tail_to_handler(&eval_if)
   1517     :.do_lambda
   1518     %tail_to_handler(&eval_lambda)
   1519     :.do_define
   1520     %tail_to_handler(&eval_define)
   1521     :.do_begin
   1522     %tail_to_handler(&eval_body)
   1523     :.do_cond
   1524     %tail_to_handler(&eval_cond)
   1525     :.do_let
   1526     %tail_to_handler(&eval_let)
   1527     :.do_letstar
   1528     %tail_to_handler(&eval_letstar)
   1529     :.do_let_values
   1530     %tail_to_handler(&eval_let_values)
   1531     :.do_letstar_values
   1532     %tail_to_handler(&eval_letstar_values)
   1533     :.do_and
   1534     %tail_to_handler(&eval_and)
   1535     :.do_or
   1536     %tail_to_handler(&eval_or)
   1537     :.do_when
   1538     %tail_to_handler(&eval_when)
   1539     :.do_case
   1540     %tail_to_handler(&eval_case)
   1541     :.do_setbang
   1542     %tail_to_handler(&eval_setbang)
   1543     :.do_define_record_type
   1544     %tail_to_handler(&eval_define_record_type)
   1545     :.do_pmatch
   1546     %tail_to_handler(&eval_pmatch)
   1547     :.do_do
   1548     %tail_to_handler(&eval_do)
   1549 })
   1550 
   1551 # eval_args(args=a0, env=a1) -> evaluated args list (cons-built).
   1552 # Iterative head/tail-cdr build: each iteration evals one arg, allocates
   1553 # a (val . NIL) cell, and either seeds head/tail or set-cdr!s onto the
   1554 # previous tail. Host stack stays O(1) regardless of arg-list length;
   1555 # eval order is left-to-right.
   1556 #
   1557 # Locals:
   1558 #   args  (advances)
   1559 #   env
   1560 #   head  (NIL until first val is appended)
   1561 #   tail  (most recent cell; set-cdr! target)
   1562 %gcfn2(eval_args, {args env head tail}, 15, 0, {
   1563     %stl(a0, args)
   1564     %stl(a1, env)
   1565     %li(t0, %imm_val(%IMM.NIL))
   1566     %stl(t0, head)
   1567     %stl(t0, tail)
   1568 
   1569     :.loop
   1570         %ldl(t0, args)
   1571         %if_nil(t1, t0, &.done)
   1572 
   1573         # val = eval(car(args), env)
   1574         %car(a0, t0)
   1575         %ldl(a1, env)
   1576         %call(&eval)
   1577 
   1578         # cell = cons(val, NIL); append to head/tail.
   1579         %li(a1, %imm_val(%IMM.NIL))
   1580         %call(&cons)
   1581 
   1582         %ldl(t0, head)
   1583         %if_nil(t1, t0, &.first)
   1584         %ldl(t0, tail)
   1585         %set_cdr(a0, t0)
   1586         %stl(a0, tail)
   1587         %b(&.advance)
   1588 
   1589         :.first
   1590         %stl(a0, head)
   1591         %stl(a0, tail)
   1592 
   1593         :.advance
   1594         %advance_walk(args)
   1595         %b(&.loop)
   1596     :.done
   1597 
   1598     %ldl(a0, head)
   1599 })
   1600 
   1601 
   1602 # apply(fn=a0, args=a1) -> result (a0)
   1603 #
   1604 # Locals:
   1605 #   args
   1606 #   body  (saved across bind_params for the closure path)
   1607 %gcfn2(apply, {args body}, 3, 0, {
   1608     %stl(a1, args)
   1609 
   1610     %hdr_type(t0, a0)
   1611     %bieq(t0, %HDR.PRIM,    &.prim,    t1)
   1612     %bieq(t0, %HDR.CLOSURE, &.closure, t1)
   1613 
   1614     :.prim
   1615     # Primitive calling convention:
   1616     #   a0 = args list (proper list of evaluated args)
   1617     #   a1 = the PRIM object itself (HEAP-tagged)
   1618     # Parameterized PRIMs (e.g. the per-field record accessors built
   1619     # by define-record-type) read their closed-over datum from
   1620     # a1+13. Plain PRIMs ignore a1. A primitive that needs a1 as a
   1621     # working register must save it first; this convention is shared
   1622     # across every entry in prim_table and is not negotiable per
   1623     # primitive. prim_apply_entry maintains the same contract when it
   1624     # tail-calls back into apply.
   1625     %mov(a1, a0)
   1626     %heap_ld(t0, a0, %PRIM.entry_w)
   1627     %ldl(a0, args)
   1628     %gctailr(t0)
   1629 
   1630     :.closure
   1631     # Closure layout (HEAP-tagged): [hdr][params][body][env]
   1632     %heap_ld(t0, a0, %CLOSURE.params)
   1633     %heap_ld(t1, a0, %CLOSURE.body)
   1634     %heap_ld(t2, a0, %CLOSURE.env)
   1635     %stl(t1, body)  ; persist body past bind_params
   1636 
   1637     # bind_params(params, args, env)
   1638     %mov(a0, t0)
   1639     %ldl(a1, args)
   1640     %mov(a2, t2)
   1641     %call(&bind_params)
   1642 
   1643     # eval_body(body, new_env) -- tail call
   1644     %mov(a1, a0)
   1645     %ldl(a0, body)
   1646     %gctail(&eval_body)
   1647 })
   1648 
   1649 # =========================================================================
   1650 # Special forms
   1651 # =========================================================================
   1652 #
   1653 # intern_special_forms runs at startup, before register_primitives, so
   1654 # the symbols `if`, ... occupy the first sym_idx slots (per LISP-C.md
   1655 # §Reservation convention). For now we just cache each one's tagged
   1656 # value in a labeled slot; eval's pair branch compares head against
   1657 # these slots before falling through to ordinary application.
   1658 
   1659 %fn(intern_special_forms, 0, {
   1660     %intern_form(quote,              "quote",              &sym_quote)
   1661     %intern_form(if,                 "if",                 &sym_if)
   1662     %intern_form(lambda,             "lambda",             &sym_lambda)
   1663     %intern_form(define,             "define",             &sym_define)
   1664     %intern_form(begin,              "begin",              &sym_begin)
   1665     %intern_form(cond,               "cond",               &sym_cond)
   1666     %intern_form(else,               "else",               &sym_else)
   1667     %intern_form(arrow,              "=>",                 &sym_arrow)
   1668     %intern_form(let,                "let",                &sym_let)
   1669     %intern_form(letstar,            "let*",               &sym_letstar)
   1670     %intern_form(let_values,         "let-values",         &sym_let_values)
   1671     %intern_form(letstar_values,     "let*-values",        &sym_letstar_values)
   1672     %intern_form(and,                "and",                &sym_and)
   1673     %intern_form(or,                 "or",                 &sym_or)
   1674     %intern_form(when,               "when",               &sym_when)
   1675     %intern_form(case,               "case",               &sym_case)
   1676     %intern_form(setbang,            "set!",               &sym_setbang)
   1677     %intern_form(define_record_type, "define-record-type", &sym_define_record_type)
   1678     %intern_form(pmatch,             "pmatch",             &sym_pmatch)
   1679     %intern_form(do,                 "do",                 &sym_do)
   1680     %intern_form(unquote,            "unquote",            &sym_unquote)
   1681     %intern_form(guard,              "guard",              &sym_guard)
   1682     %intern_form(underscore,         "_",                  &sym_underscore)
   1683     %intern_form(dollar,             "$",                  &sym_dollar)
   1684 })
   1685 
   1686 # eval_quote(rest=a0, env=a1) -> value (a0). rest is (datum); return datum.
   1687 %fn(eval_quote, 0, {
   1688     %car(a0, a0)
   1689 })
   1690 
   1691 # eval_if(rest=a0, env=a1) -> value (a0). `rest` is (test then) or
   1692 # (test then else). Single-arm form returns UNSPEC when test is #f.
   1693 # No arity check beyond that -- spec policy: malformed special forms
   1694 # are UB.
   1695 #
   1696 # Locals:
   1697 #   rest
   1698 #   env
   1699 %gcfn2(eval_if, {rest env}, 3, 0, {
   1700     %stl(a0, rest)
   1701     %stl(a1, env)
   1702 
   1703     # val = eval(car(rest), env)
   1704     %car(a0, a0)
   1705     %call(&eval)
   1706 
   1707     %bieq(a0, %imm_val(%IMM.FALSE), &.else_branch, t0)
   1708 
   1709     # then-branch: tail-eval(cadr(rest), env)
   1710     %ldl(a0, rest)
   1711     %cdr(a0, a0)
   1712     %car(a0, a0)
   1713     %ldl(a1, env)
   1714     %gctail(&eval)
   1715 
   1716     :.else_branch
   1717     # If cddr(rest) is NIL, this is single-arm `if` -> UNSPEC.
   1718     %ldl(t0, rest)
   1719     %cdr(t0, t0)
   1720     %cdr(t0, t0)
   1721     %if_nil(t1, t0, &.no_else)
   1722 
   1723     # else-branch: tail-eval(car(cddr(rest)), env)
   1724     %car(a0, t0)
   1725     %ldl(a1, env)
   1726     %gctail(&eval)
   1727 
   1728     :.no_else
   1729     %li(a0, %imm_val(%IMM.UNSPEC))
   1730 })
   1731 
   1732 # eval_lambda(rest=a0, env=a1) -> closure (a0).
   1733 # rest is (params . body). Allocates a 32-byte CLOSURE on the heap
   1734 # and stores params, body, and the captured env directly.
   1735 #
   1736 # Locals:
   1737 #   rest
   1738 #   env
   1739 #   closure  ptr (HEAP-tagged)
   1740 %gcfn2(eval_lambda, {rest env closure}, 7, 0, {
   1741     %stl(a0, rest)
   1742     %stl(a1, env)
   1743 
   1744     %li(a0, %CLOSURE.SIZE)
   1745     %li(a1, %HDR.CLOSURE)
   1746     %call(&alloc_hdr)
   1747     %stl(a0, closure)
   1748 
   1749     # closure[params] = car(rest)
   1750     %ldl(t0, rest)
   1751     %car(t1, t0)
   1752     %ldl(t0, closure)
   1753     %heap_st(t1, t0, %CLOSURE.params)
   1754 
   1755     # closure[body] = cdr(rest)
   1756     %ldl(t1, rest)
   1757     %cdr(t1, t1)
   1758     %heap_st(t1, t0, %CLOSURE.body)
   1759 
   1760     # closure[env] = captured env
   1761     %ldl(t1, env)
   1762     %heap_st(t1, t0, %CLOSURE.env)
   1763 
   1764     %ldl(a0, closure)
   1765 })
   1766 
   1767 # eval_define(rest=a0, env=a1) -> UNSPEC (a0).
   1768 # Top-level only. Two surface forms:
   1769 #   (define <sym> <expr>)              ; head of rest is a SYM
   1770 #   (define (<sym> . <params>) . body) ; head of rest is a PAIR; sugar for
   1771 #                                         (define <sym> (lambda <params> . body))
   1772 # Internal `define` is rejected by eval_body before this entry is reached
   1773 # (every internal body context routes through eval_body); see the check
   1774 # at the head of eval_body's loop.
   1775 #
   1776 # Locals:
   1777 #   rest
   1778 #   env
   1779 %gcfn2(eval_define, {rest env}, 3, 0, {
   1780     %stl(a0, rest)
   1781     %stl(a1, env)
   1782 
   1783     # If car(rest) is a pair, this is the lambda-sugar form.
   1784     %car(t0, a0)
   1785     %tagof(t1, t0)
   1786     %bieq(t1, %TAG.PAIR, &.sugar, t2)
   1787 
   1788     # Plain define: value = eval(car(cdr(rest)), env)
   1789     %ldl(t0, rest)
   1790     %cdr(a0, t0)
   1791     %car(a0, a0)
   1792     %ldl(a1, env)
   1793     %call(&eval)
   1794 
   1795     %ldl(t0, rest)
   1796     %car(t0, t0)
   1797     %set_global(t0, a0)
   1798     %li(a0, %imm_val(%IMM.UNSPEC))
   1799     %gceret
   1800 
   1801     :.sugar
   1802     # rest = ((name . params) . body); build (params . body) for eval_lambda.
   1803     %ldl(t0, rest)
   1804     %car(t0, t0)
   1805     %cdr(a0, t0)            ; params
   1806     %ldl(t0, rest)
   1807     %cdr(a1, t0)            ; body
   1808     %call(&cons)
   1809     %ldl(a1, env)
   1810     %call(&eval_lambda)
   1811 
   1812     %ldl(t0, rest)
   1813     %car(t0, t0)
   1814     %car(t0, t0)            ; name
   1815     %set_global(t0, a0)
   1816     %li(a0, %imm_val(%IMM.UNSPEC))
   1817 })
   1818 
   1819 # eval_setbang(rest=a0, env=a1) -> UNSPEC (a0).
   1820 # rest = (sym value-expr). Evaluates value-expr in env, then finds the target
   1821 # ENV node through the same filtered lookup used by eval. On a lexical hit it
   1822 # mutates the node's value slot. On a miss it may update an existing global
   1823 # binding, but it must not create one.
   1824 #
   1825 # Locals:
   1826 #   rest  (sym . (value-expr . ()))
   1827 #   env
   1828 #   saved  value   (eval'd value-expr)
   1829 %gcfn2(eval_setbang, {rest env saved}, 7, 0, {
   1830     %stl(a0, rest)
   1831     %stl(a1, env)
   1832 
   1833     # value = eval(cadr(rest), env)
   1834     %cdr(a0, a0)
   1835     %car(a0, a0)
   1836     %ldl(a1, env)
   1837     %call(&eval)
   1838     %stl(a0, saved)
   1839 
   1840     %ldl(a0, rest)
   1841     %car(a0, a0)            ; target sym
   1842     %ldl(a1, env)
   1843     %call(&env_find)
   1844     %beqz(a0, &.miss)
   1845     %mov(t0, a0)            ; lexical ENV node
   1846     %ldl(a0, saved)
   1847     %heap_st(a0, t0, %ENV.value)
   1848     %li(a0, %imm_val(%IMM.UNSPEC))
   1849     %gceret
   1850 
   1851     :.miss
   1852     # Miss: require an existing global binding before replacing it.
   1853     %ldl(t0, rest)
   1854     %car(a0, t0)
   1855     %untag_sym(a0, a0)
   1856     %call(&sym_global)
   1857     %bieq(a0, %imm_val(%IMM.UNBOUND), &.unbound, t0)
   1858     %ldl(a0, saved)
   1859     %ldl(t0, rest)
   1860     %car(t0, t0)
   1861     %set_global(t0, a0)
   1862     %li(a0, %imm_val(%IMM.UNSPEC))
   1863     %gceret
   1864 
   1865     :.unbound
   1866     %die(msg_unbound_set)
   1867 })
   1868 
   1869 # eval_cond(clauses=a0, env=a1) -> value (a0).
   1870 # Clause shapes: (else body...), (test body...), (test => proc-expr).
   1871 # else / => are literal symbols matched by pointer equality. The =>
   1872 # arrow is only recognized in non-else clauses. A successful one-expression
   1873 # clause returns its test value, and `else` must be final.
   1874 #
   1875 # Locals:
   1876 #   clauses  (advances)
   1877 #   env
   1878 #   test  validation cursor, then value (live across the => eval/cons calls)
   1879 #   proc  (live across the => cons call)
   1880 %gcfn2(eval_cond, {clauses env test proc}, 15, 0, {
   1881     %stl(a0, clauses)
   1882     %stl(a1, env)
   1883 
   1884     # Validate the placement of `else` before evaluating any test. Otherwise
   1885     # an earlier true clause could make a syntactically invalid later `else`
   1886     # silently unreachable.
   1887     %stl(a0, test)
   1888     :.validate
   1889     %ldl(t0, test)
   1890     %if_nil(t1, t0, &.loop)
   1891     %car(t1, t0)            ; clause
   1892     %car(t2, t1)            ; test expression
   1893     %ld_global(a0, &sym_else)
   1894     %bne(t2, a0, &.validate_next)
   1895     %cdr(t0, t0)
   1896     %if_nil(t1, t0, &.loop)
   1897     %die(msg_cond_else_not_final)
   1898     :.validate_next
   1899     %advance_walk(test)
   1900     %b(&.validate)
   1901 
   1902     :.loop
   1903     %ldl(t0, clauses)
   1904     %if_nil(t1, t0, &.no_match)
   1905 
   1906     %car(t1, t0)            ; clause
   1907     %car(t2, t1)            ; test_expr
   1908 
   1909     %ld_global(a0, &sym_else)
   1910     %beq(t2, a0, &.else_clause)
   1911 
   1912     %mov(a0, t2)
   1913     %ldl(a1, env)
   1914     %call(&eval)
   1915     %li(t0, %imm_val(%IMM.FALSE))
   1916     %beq(a0, t0, &.next)
   1917 
   1918     # Truthy. Spill test value and inspect cdr(clause): empty -> test value,
   1919     # car == => -> arrow path, else regular body.
   1920     %stl(a0, test)
   1921     %ldl(t0, clauses)
   1922     %car(t0, t0)
   1923     %cdr(t0, t0)
   1924     %if_nil(t1, t0, &.test_only)
   1925     %car(t1, t0)
   1926     %ld_global(t2, &sym_arrow)
   1927     %beq(t1, t2, &.arrow)
   1928 
   1929     %mov(a0, t0)            ; regular body
   1930     %ldl(a1, env)
   1931     %gctail(&eval_body)
   1932 
   1933     :.arrow
   1934     %cdr(t0, t0)
   1935     %car(a0, t0)            ; proc-expr
   1936     %ldl(a1, env)
   1937     %call(&eval)
   1938     %stl(a0, proc)
   1939     %ldl(a0, test)
   1940     %li(a1, %imm_val(%IMM.NIL))
   1941     %call(&cons)
   1942     %mov(a1, a0)
   1943     %ldl(a0, proc)
   1944     %gctail(&apply)
   1945 
   1946     :.test_only
   1947     %ldl(a0, test)
   1948     %gceret
   1949 
   1950     :.else_clause
   1951     %ldl(t0, clauses)
   1952     %cdr(t0, t0)
   1953     %if_nil(t1, t0, &.else_final)
   1954     %die(msg_cond_else_not_final)
   1955     :.else_final
   1956     %ldl(t0, clauses)
   1957     %car(t0, t0)
   1958     %cdr(a0, t0)
   1959     %ldl(a1, env)
   1960     %gctail(&eval_body)
   1961 
   1962     :.next
   1963     %advance_walk(clauses)
   1964     %b(&.loop)
   1965 
   1966     :.no_match
   1967     %li(a0, %imm_val(%IMM.UNSPEC))
   1968 })
   1969 
   1970 # eval_let(rest=a0, env=a1) -> value (a0).
   1971 # Two surface forms:
   1972 #   (let ((p v) ...) body...)
   1973 #   (let name ((p v) ...) body...)   ; named let, dispatches to eval_let_named
   1974 # Standard `let` evaluates every init in `env`, then extends env with all
   1975 # bindings simultaneously and tail-evaluates the body.
   1976 #
   1977 # Locals:
   1978 #   rest
   1979 #   env  (original)
   1980 #   walk  (bindings, advances)
   1981 #   new_env  (built up)
   1982 %gcfn2(eval_let, {rest env walk new_env}, 15, 0, {
   1983     %stl(a0, rest)
   1984     %stl(a1, env)
   1985 
   1986     # Named let?
   1987     %car(t0, a0)
   1988     %tagof(t1, t0)
   1989     %bieq(t1, %TAG.SYM, &.named, t2)
   1990 
   1991     %ldl(t0, rest)
   1992     %car(t0, t0)            ; bindings
   1993     %stl(t0, walk)
   1994     %ldl(t0, env)
   1995     %stl(t0, new_env)         ; new_env = env
   1996 
   1997     :.loop
   1998     %ldl(t0, walk)
   1999     %if_nil(t1, t0, &.done)
   2000 
   2001     %car(t1, t0)            ; pair = (name init)
   2002     %cdr(t2, t1)
   2003     %car(t2, t2)            ; init
   2004 
   2005     # val = eval(init, env_orig)
   2006     %mov(a0, t2)
   2007     %ldl(a1, env)
   2008     %call(&eval)
   2009 
   2010     # new_env = env_extend(name, val, new_env)
   2011     %ldl(t0, walk)
   2012     %car(t1, t0)
   2013     %car(t2, t1)
   2014     %mov(a1, a0)
   2015     %mov(a0, t2)
   2016     %ldl(a2, new_env)
   2017     %call(&env_extend)
   2018     %stl(a0, new_env)
   2019 
   2020     %advance_walk(walk)
   2021     %b(&.loop)
   2022 
   2023     :.done
   2024     %ldl(a0, rest)
   2025     %cdr(a0, a0)            ; body
   2026     %ldl(a1, new_env)
   2027     %gctail(&eval_body)
   2028 
   2029     :.named
   2030     %ldl(a0, rest)
   2031     %ldl(a1, env)
   2032     %gctail(&eval_let_named)
   2033 })
   2034 
   2035 # eval_letstar(rest=a0, env=a1) -> value (a0).
   2036 # Like let, but each init is evaluated in the env extended by all prior
   2037 # bindings of the same let* form (left-to-right shadowing).
   2038 #
   2039 # Locals:
   2040 #   rest
   2041 #   env
   2042 #   walk
   2043 #   new_env
   2044 %gcfn2(eval_letstar, {rest env walk new_env}, 15, 0, {
   2045     %stl(a0, rest)
   2046     %stl(a1, env)
   2047 
   2048     %ldl(t0, rest)
   2049     %car(t0, t0)
   2050     %stl(t0, walk)
   2051     %ldl(t0, env)
   2052     %stl(t0, new_env)
   2053 
   2054     :.loop
   2055         %ldl(t0, walk)
   2056         %if_nil(t1, t0, &.done)
   2057 
   2058         %car(t1, t0)
   2059         %cdr(t2, t1)
   2060         %car(t2, t2)
   2061 
   2062         # val = eval(init, new_env)
   2063         %mov(a0, t2)
   2064         %ldl(a1, new_env)
   2065         %call(&eval)
   2066 
   2067         %ldl(t0, walk)
   2068         %car(t1, t0)
   2069         %car(t2, t1)
   2070         %mov(a1, a0)
   2071         %mov(a0, t2)
   2072         %ldl(a2, new_env)
   2073         %call(&env_extend)
   2074         %stl(a0, new_env)
   2075 
   2076         %advance_walk(walk)
   2077         %b(&.loop)
   2078     :.done
   2079 
   2080     %ldl(a0, rest)
   2081     %cdr(a0, a0)
   2082     %ldl(a1, new_env)
   2083     %gctail(&eval_body)
   2084 })
   2085 
   2086 # eval_let_values(rest=a0, env=a1) -> value (a0).
   2087 # rest = (((formals init) ...) body...). Each init is evaluated in the
   2088 # OUTER env; mv_to_list normalizes its result so bind_params can drive
   2089 # both list-style and dotted/rest formals identically. Then bodies run
   2090 # in the env extended by all clauses.
   2091 #
   2092 # Locals:
   2093 #   rest
   2094 #   env  (original)
   2095 #   walk  (clauses, advances)
   2096 #   new_env  (built up)
   2097 %gcfn2(eval_let_values, {rest env walk new_env}, 15, 0, {
   2098     %stl(a0, rest)
   2099     %stl(a1, env)
   2100 
   2101     %ldl(t0, rest)
   2102     %car(t0, t0)            ; clauses
   2103     %stl(t0, walk)
   2104     %ldl(t0, env)
   2105     %stl(t0, new_env)         ; new_env = env
   2106 
   2107     :.loop
   2108         %ldl(t0, walk)
   2109         %if_nil(t1, t0, &.done)
   2110 
   2111         %car(t1, t0)            ; clause = (formals init)
   2112         %cdr(t2, t1)
   2113         %car(t2, t2)            ; init
   2114 
   2115         # val = eval(init, env_orig)
   2116         %mov(a0, t2)
   2117         %ldl(a1, env)
   2118         %call(&eval)
   2119 
   2120         # vals = mv_to_list(val)
   2121         %call(&mv_to_list)
   2122 
   2123         # new_env = bind_params(formals, vals, new_env)
   2124         %ldl(t0, walk)
   2125         %car(t1, t0)
   2126         %car(t1, t1)            ; formals
   2127         %mov(a1, a0)
   2128         %mov(a0, t1)
   2129         %ldl(a2, new_env)
   2130         %call(&bind_params)
   2131         %stl(a0, new_env)
   2132 
   2133         %advance_walk(walk)
   2134         %b(&.loop)
   2135     :.done
   2136 
   2137     %ldl(a0, rest)
   2138     %cdr(a0, a0)            ; body
   2139     %ldl(a1, new_env)
   2140     %gctail(&eval_body)
   2141 })
   2142 
   2143 # eval_letstar_values(rest=a0, env=a1) -> value (a0).
   2144 # Like let-values but each init is evaluated in new_env (the env extended
   2145 # by all prior clauses' bindings), giving sequential / shadowing semantics.
   2146 #
   2147 # Locals:
   2148 #   rest
   2149 #   env
   2150 #   walk
   2151 #   new_env
   2152 %gcfn2(eval_letstar_values, {rest env walk new_env}, 15, 0, {
   2153     %stl(a0, rest)
   2154     %stl(a1, env)
   2155 
   2156     %ldl(t0, rest)
   2157     %car(t0, t0)
   2158     %stl(t0, walk)
   2159     %ldl(t0, env)
   2160     %stl(t0, new_env)
   2161 
   2162     :.loop
   2163         %ldl(t0, walk)
   2164         %if_nil(t1, t0, &.done)
   2165 
   2166         %car(t1, t0)
   2167         %cdr(t2, t1)
   2168         %car(t2, t2)            ; init
   2169 
   2170         # val = eval(init, new_env)
   2171         %mov(a0, t2)
   2172         %ldl(a1, new_env)
   2173         %call(&eval)
   2174 
   2175         %call(&mv_to_list)
   2176 
   2177         %ldl(t0, walk)
   2178         %car(t1, t0)
   2179         %car(t1, t1)            ; formals
   2180         %mov(a1, a0)
   2181         %mov(a0, t1)
   2182         %ldl(a2, new_env)
   2183         %call(&bind_params)
   2184         %stl(a0, new_env)
   2185 
   2186         %advance_walk(walk)
   2187         %b(&.loop)
   2188     :.done
   2189 
   2190     %ldl(a0, rest)
   2191     %cdr(a0, a0)
   2192     %ldl(a1, new_env)
   2193     %gctail(&eval_body)
   2194 })
   2195 
   2196 # eval_and(rest=a0, env=a1) -> value (a0).
   2197 # (and) is #t. Otherwise eval forms left-to-right, short-circuiting to #f
   2198 # the moment one yields #f. The last form is tail-evaluated so a tail call
   2199 # inside `and` doesn't grow the host stack.
   2200 #
   2201 # Locals:
   2202 #   rest
   2203 #   env
   2204 %gcfn2(eval_and, {rest env}, 3, 0, {
   2205     %li(t0, %imm_val(%IMM.TRUE))
   2206     %if_nil(t1, a0, &.done_imm)
   2207 
   2208     :.loop
   2209         %stl(a0, rest)
   2210         %stl(a1, env)
   2211 
   2212         # If cdr(rest) is NIL, the head is the last form -> tail-eval.
   2213         %cdr(t0, a0)
   2214         %if_nil(t1, t0, &.last)
   2215 
   2216         # Non-last: eval, short-circuit on #f, otherwise advance.
   2217         %car(a0, a0)
   2218         %call(&eval)
   2219         %li(t0, %imm_val(%IMM.FALSE))
   2220         %beq(a0, t0, &.done)
   2221         %ldl(a0, rest)
   2222         %cdr(a0, a0)
   2223         %ldl(a1, env)
   2224         %b(&.loop)
   2225 
   2226         :.last
   2227         %ldl(a0, rest)
   2228         %car(a0, a0)
   2229         %ldl(a1, env)
   2230         %gctail(&eval)
   2231     :.done
   2232 
   2233     %gceret
   2234 
   2235     :.done_imm
   2236     %mov(a0, t0)
   2237 })
   2238 
   2239 # eval_or(rest=a0, env=a1) -> value (a0).
   2240 # (or) is #f. Otherwise eval forms left-to-right and return the first
   2241 # non-#f value; if every form was #f, return #f. The last form is
   2242 # tail-evaluated.
   2243 #
   2244 # Locals:
   2245 #   rest
   2246 #   env
   2247 %gcfn2(eval_or, {rest env}, 3, 0, {
   2248     %li(t0, %imm_val(%IMM.FALSE))
   2249     %if_nil(t1, a0, &.done_imm)
   2250 
   2251     :.loop
   2252         %stl(a0, rest)
   2253         %stl(a1, env)
   2254 
   2255         %cdr(t0, a0)
   2256         %if_nil(t1, t0, &.last)
   2257 
   2258         %car(a0, a0)
   2259         %call(&eval)
   2260         %bine(a0, %imm_val(%IMM.FALSE), &.done, t0)
   2261         %ldl(a0, rest)
   2262         %cdr(a0, a0)
   2263         %ldl(a1, env)
   2264         %b(&.loop)
   2265 
   2266         :.last
   2267         %ldl(a0, rest)
   2268         %car(a0, a0)
   2269         %ldl(a1, env)
   2270         %gctail(&eval)
   2271     :.done
   2272 
   2273     %gceret
   2274 
   2275     :.done_imm
   2276     %mov(a0, t0)
   2277 })
   2278 
   2279 # eval_when(rest=a0, env=a1) -> value (a0).
   2280 # (when test body...) -- if test evaluates non-#f, tail-eval body and
   2281 # return its last value; otherwise return UNSPEC. Body never enters a
   2282 # new scope.
   2283 #
   2284 # Locals:
   2285 #   rest
   2286 #   env
   2287 %gcfn2(eval_when, {rest env}, 3, 0, {
   2288     %stl(a0, rest)
   2289     %stl(a1, env)
   2290 
   2291     %car(a0, a0)            ; test
   2292     %call(&eval)
   2293 
   2294     %bieq(a0, %imm_val(%IMM.FALSE), &.skip, t0)
   2295 
   2296     %ldl(a0, rest)
   2297     %cdr(a0, a0)            ; body
   2298     %ldl(a1, env)
   2299     %gctail(&eval_body)
   2300 
   2301     :.skip
   2302     %li(a0, %imm_val(%IMM.UNSPEC))
   2303 })
   2304 
   2305 # eval_case(rest=a0, env=a1) -> value (a0).
   2306 # rest is (key-expr clause...). The key is evaluated once; clauses are
   2307 # tried in order. Clause shape:
   2308 #   ((datum...) body...)   ; datums are literal, eq?-compared to key
   2309 #   (else body...)         ; matches unconditionally
   2310 # Matching uses pointer equality (eq?), which is correct for fixnums,
   2311 # symbols, chars, and booleans -- the values case is meant for. The
   2312 # matched clause's body is tail-evaluated via eval_body. No-match (and
   2313 # no else) returns UNSPEC, mirroring eval_cond's no-match policy.
   2314 #
   2315 # Locals:
   2316 #   subject  (evaluated key)
   2317 #   env
   2318 #   clauses  (advances)
   2319 #   datums   (advances within a clause)
   2320 %gcfn2(eval_case, {subject env clauses datums}, 15, 0, {
   2321     %stl(a1, env)
   2322 
   2323     # subject = eval(car(rest), env); clauses = cdr(rest).
   2324     %mov(t0, a0)
   2325     %cdr(t1, t0)
   2326     %stl(t1, clauses)
   2327     %car(a0, t0)
   2328     %ldl(a1, env)
   2329     %call(&eval)
   2330     %stl(a0, subject)
   2331 
   2332     :.loop
   2333         %ldl(t0, clauses)
   2334         %if_nil(t1, t0, &.no_match)
   2335 
   2336         %car(t1, t0)                  ; clause
   2337         %car(t2, t1)                  ; head: datum-list or `else`
   2338 
   2339         %ld_global(a3, &sym_else)
   2340         %beq(t2, a3, &.do_else)
   2341 
   2342         # Walk the datum list, eq?-compare each against subject.
   2343         %stl(t2, datums)
   2344         %ldl(a0, subject)
   2345         :.scan
   2346         %ldl(t0, datums)
   2347         %if_nil(t1, t0, &.next_clause)
   2348         %car(t1, t0)                  ; datum
   2349         %beq(t1, a0, &.do_body)
   2350         %cdr(t0, t0)
   2351         %stl(t0, datums)
   2352         %b(&.scan)
   2353 
   2354         :.do_body
   2355         %ldl(t0, clauses)
   2356         %car(t0, t0)
   2357         %cdr(a0, t0)                  ; body
   2358         %ldl(a1, env)
   2359         %gctail(&eval_body)
   2360 
   2361         :.do_else
   2362         %ldl(t0, clauses)
   2363         %car(t0, t0)
   2364         %cdr(a0, t0)                  ; body
   2365         %ldl(a1, env)
   2366         %gctail(&eval_body)
   2367 
   2368         :.next_clause
   2369         %ldl(t0, clauses)
   2370         %cdr(t0, t0)
   2371         %stl(t0, clauses)
   2372         %b(&.loop)
   2373 
   2374     :.no_match
   2375     %li(a0, %imm_val(%IMM.UNSPEC))
   2376 })
   2377 
   2378 # eval_pmatch(rest=a0, env=a1) -> value (a0).
   2379 # rest is (subject-expr . clauses). The subject is evaluated once; each
   2380 # clause is then tried in order against the same subject value, restarting
   2381 # from the outer env per clause. Clause shape:
   2382 #   (<pat> <body>...)
   2383 #   (<pat> (guard <g>...) <body>...)
   2384 #   (else <body>...)
   2385 # An `else` clause always matches with no bindings; a guarded clause is
   2386 # selected only if every guard expression evaluates non-#f. The matched
   2387 # clause's body is tail-evaluated via eval_body so the last form keeps
   2388 # tail position. No-match (and no else) dies via runtime_error.
   2389 #
   2390 # Locals:
   2391 #   subject
   2392 #   env_outer  (per-clause restart point)
   2393 #   clauses  (current cursor; advances on miss / failed guard)
   2394 #   env_ext  (env extended with the matched clause's bindings)
   2395 #   guard  cursor (advances during the guard AND-fold)
   2396 #   body  (saved across guard evals, tail-evaluated on success)
   2397 %gcfn2(eval_pmatch, {subject env_outer clauses env_ext guard body}, 63, 0, {
   2398     %stl(a1, env_outer)
   2399 
   2400     # subject = eval(car(rest), env_outer); clauses = cdr(rest).
   2401     %mov(t0, a0)
   2402     %cdr(t1, t0)
   2403     %stl(t1, clauses)
   2404     %car(a0, t0)
   2405     %ldl(a1, env_outer)
   2406     %call(&eval)
   2407     %stl(a0, subject)
   2408 
   2409     :.loop
   2410         %ldl(t0, clauses)
   2411         %if_nil(t1, t0, &.no_match)
   2412 
   2413         %car(t1, t0)                  ; clause
   2414         %car(t2, t1)                  ; pat
   2415 
   2416         %ld_global(a3, &sym_else)
   2417         %beq(t2, a3, &.do_else)
   2418 
   2419         # pmatch_match(pat, subject, env_outer) -> (a0=env_ext, a1=ok)
   2420         %mov(a0, t2)
   2421         %ldl(a1, subject)
   2422         %ldl(a2, env_outer)
   2423         %call(&pmatch_match)
   2424         %beqz(a1, &.next)
   2425 
   2426         %stl(a0, env_ext)               ; env_ext
   2427 
   2428         # tail = cdr(clause)
   2429         %ldl(t0, clauses)
   2430         %car(t0, t0)
   2431         %cdr(t0, t0)                  ; tail = (body...) or ((guard ...) body...)
   2432 
   2433         # Guard form? tail is a pair, car(tail) is a pair, head of car(tail)
   2434         # eq? sym_guard.
   2435         %tagof(t1, t0)
   2436         %bine(t1, %TAG.PAIR, &.body_simple, t2)
   2437         %car(t1, t0)                  ; first form of tail
   2438         %tagof(t2, t1)
   2439         %bine(t2, %TAG.PAIR, &.body_simple, a0)
   2440         %car(a0, t1)                  ; head of first form
   2441         %ld_global(a1, &sym_guard)
   2442         %bne(a0, a1, &.body_simple)
   2443 
   2444         # Guard clause. guards = cdr(car(tail)); body = cdr(tail).
   2445         %cdr(a0, t1)
   2446         %stl(a0, guard)
   2447         %cdr(t0, t0)
   2448         %stl(t0, body)
   2449 
   2450         :.g_loop
   2451             %ldl(t0, guard)
   2452             %if_nil(t1, t0, &.body_run)
   2453 
   2454             %car(a0, t0)                  ; guard expr
   2455             %ldl(a1, env_ext)               ; env_ext
   2456             %call(&eval)
   2457             %bieq(a0, %imm_val(%IMM.FALSE), &.next, t0)
   2458 
   2459             %ldl(t0, guard)
   2460             %cdr(t0, t0)
   2461             %stl(t0, guard)
   2462             %b(&.g_loop)
   2463 
   2464         :.body_run
   2465         %ldl(a0, body)
   2466         %ldl(a1, env_ext)
   2467         %gctail(&eval_body)
   2468 
   2469         :.body_simple
   2470         # tail itself is the body (no guard wrapper). Tail-call eval_body
   2471         # with the extended env; tail position of the matched clause's body
   2472         # is preserved.
   2473         %mov(a0, t0)
   2474         %ldl(a1, env_ext)
   2475         %gctail(&eval_body)
   2476 
   2477         :.do_else
   2478         %ldl(t0, clauses)
   2479         %car(t0, t0)
   2480         %cdr(a0, t0)                  ; body
   2481         %ldl(a1, env_outer)                ; env_outer (no bindings introduced)
   2482         %gctail(&eval_body)
   2483 
   2484         :.next
   2485         %ldl(t0, clauses)
   2486         %cdr(t0, t0)
   2487         %stl(t0, clauses)
   2488         %b(&.loop)
   2489 
   2490     :.no_match
   2491     %die(msg_pmatch_no_match)
   2492 })
   2493 
   2494 # eval_do(rest=a0, env=a1) -> value (a0).
   2495 # rest = (((var init step?) ...) (test result?...) body...).
   2496 #
   2497 # Phase 1 (init): walk binding-specs in order, eval each `init` in the
   2498 # outer env, build new_env by prepending ENV nodes. A parallel list
   2499 # `pairs_head` records those nodes in spec order so the iteration can mutate
   2500 # their value slots. A second parallel list
   2501 # `vals_head` is preallocated (one cell per spec) to hold each iteration's
   2502 # computed step values without per-iteration cell allocation.
   2503 #
   2504 # Phase 2 (loop): eval test in new_env. Truthy -> tail-eval result body
   2505 # (UNSPEC if no result forms). Falsy -> eval each command form in
   2506 # new_env (discard), collect new step values into vals_head (parallel
   2507 # semantics: every step is evaluated against the iteration's pre-update
   2508 # bindings; specs without a step keep their current value), then walk
   2509 # pairs_head/vals_head together and update each ENV node to its
   2510 # new value. Loop.
   2511 #
   2512 # Locals:
   2513 #   rest          original rest pointer
   2514 #   env           outer env
   2515 #   new_env       env extended with ENV nodes (mutated each iter)
   2516 #   walk          generic cdr-cursor (binding-specs / commands / steps)
   2517 #   pairs_head    list of ENV-node refs in spec order
   2518 #   pairs_tail    append point during init
   2519 #   vals_head     parallel list of cells holding each iteration's step vals
   2520 #   vals_tail     append point during init
   2521 #   body          body command-forms (cddr of rest)
   2522 #   pair_walk     cdr-cursor over pairs_head during step/update
   2523 #   val_walk      cdr-cursor over vals_head during step/update
   2524 %gcfn2(eval_do, {rest env new_env walk pairs_head pairs_tail vals_head vals_tail body pair_walk val_walk}, 2047, 0, {
   2525     %stl(a0, rest)
   2526     %stl(a1, env)
   2527 
   2528     %ldl(t0, rest)
   2529     %car(t0, t0)
   2530     %stl(t0, walk)
   2531     %ldl(t0, env)
   2532     %stl(t0, new_env)
   2533     %li(t0, %imm_val(%IMM.NIL))
   2534     %stl(t0, pairs_head)
   2535     %stl(t0, pairs_tail)
   2536     %stl(t0, vals_head)
   2537     %stl(t0, vals_tail)
   2538 
   2539     :.init_loop
   2540         %ldl(t0, walk)
   2541         %if_nil(t1, t0, &.init_done)
   2542 
   2543         # spec = car(walk); init-expr = car(cdr(spec)).
   2544         %car(t1, t0)
   2545         %cdr(a0, t1)
   2546         %car(a0, a0)            ; init expression
   2547 
   2548         # val = eval(init, env)
   2549         %ldl(a1, env)
   2550         %call(&eval)
   2551 
   2552         # node = env_extend(var, val, new_env); var = car(car(walk)).
   2553         %ldl(t0, walk)
   2554         %car(t1, t0)
   2555         %car(t2, t1)            ; var
   2556         %mov(a1, a0)
   2557         %mov(a0, t2)
   2558         %ldl(a2, new_env)
   2559         %call(&env_extend)      ; a0 = new ENV head/node
   2560         %stl(a0, new_env)
   2561 
   2562         # pcell = cons(node, NIL). a0 still holds the new ENV node.
   2563         %li(a1, %imm_val(%IMM.NIL))
   2564         %call(&cons)
   2565 
   2566         %ldl(t0, pairs_head)
   2567         %if_nil(t1, t0, &.pairs_first)
   2568         %ldl(t0, pairs_tail)
   2569         %set_cdr(a0, t0)
   2570         %stl(a0, pairs_tail)
   2571         %b(&.vals_alloc)
   2572 
   2573         :.pairs_first
   2574         %stl(a0, pairs_head)
   2575         %stl(a0, pairs_tail)
   2576 
   2577         :.vals_alloc
   2578         # vcell = cons(NIL, NIL). Append onto vals list.
   2579         %li(a0, %imm_val(%IMM.NIL))
   2580         %li(a1, %imm_val(%IMM.NIL))
   2581         %call(&cons)
   2582 
   2583         %ldl(t0, vals_head)
   2584         %if_nil(t1, t0, &.vals_first)
   2585         %ldl(t0, vals_tail)
   2586         %set_cdr(a0, t0)
   2587         %stl(a0, vals_tail)
   2588         %b(&.init_advance)
   2589 
   2590         :.vals_first
   2591         %stl(a0, vals_head)
   2592         %stl(a0, vals_tail)
   2593 
   2594         :.init_advance
   2595         %advance_walk(walk)
   2596         %b(&.init_loop)
   2597     :.init_done
   2598 
   2599     # body = cddr(rest).
   2600     %ldl(t0, rest)
   2601     %cdr(t0, t0)
   2602     %cdr(t0, t0)
   2603     %stl(t0, body)
   2604 
   2605     :.iter_loop
   2606     # test = car(car(cdr(rest))). Eval in new_env.
   2607     %ldl(t0, rest)
   2608     %cdr(t0, t0)
   2609     %car(t0, t0)            ; (test result?...)
   2610     %car(a0, t0)            ; test
   2611     %ldl(a1, new_env)
   2612     %call(&eval)
   2613 
   2614     %bieq(a0, %imm_val(%IMM.FALSE), &.commands, t0)
   2615 
   2616     # Truthy: results = cdr(car(cdr(rest))).
   2617     %ldl(t0, rest)
   2618     %cdr(t0, t0)
   2619     %car(t0, t0)
   2620     %cdr(t0, t0)            ; results
   2621     %if_nil(t1, t0, &.no_results)
   2622     %mov(a0, t0)
   2623     %ldl(a1, new_env)
   2624     %gctail(&eval_body)
   2625 
   2626     :.no_results
   2627     %li(a0, %imm_val(%IMM.UNSPEC))
   2628     %gceret
   2629 
   2630     :.commands
   2631     %ldl(t0, body)
   2632     %stl(t0, walk)
   2633 
   2634     :.cmd_loop
   2635     %ldl(t0, walk)
   2636     %if_nil(t1, t0, &.step_phase)
   2637     %car(a0, t0)
   2638     %ldl(a1, new_env)
   2639     %call(&eval)
   2640     %advance_walk(walk)
   2641     %b(&.cmd_loop)
   2642 
   2643     :.step_phase
   2644     # Compute new step values. walk = specs, pair_walk = pairs_head,
   2645     # val_walk = vals_head. For each spec: if spec has step (cddr non-NIL),
   2646     # val = eval(step, new_env); else val = ENV.value (current).
   2647     # Store val into car(val_walk).
   2648     %ldl(t0, rest)
   2649     %car(t0, t0)
   2650     %stl(t0, walk)
   2651     %ldl(t0, pairs_head)
   2652     %stl(t0, pair_walk)
   2653     %ldl(t0, vals_head)
   2654     %stl(t0, val_walk)
   2655 
   2656     :.step_loop
   2657     %ldl(t0, walk)
   2658     %if_nil(t1, t0, &.update_phase)
   2659 
   2660     %car(t1, t0)            ; spec
   2661     %cdr(t2, t1)
   2662     %cdr(t2, t2)            ; (step?) or NIL
   2663     %if_nil(t1, t2, &.no_step)
   2664 
   2665     %car(a0, t2)            ; step
   2666     %ldl(a1, new_env)
   2667     %call(&eval)
   2668     %b(&.store_val)
   2669 
   2670     :.no_step
   2671     %ldl(t0, pair_walk)
   2672     %car(t0, t0)            ; ENV node
   2673     %heap_ld(a0, t0, %ENV.value)
   2674 
   2675     :.store_val
   2676     %ldl(t0, val_walk)
   2677     %set_car(a0, t0)
   2678 
   2679     %advance_walk(walk)
   2680     %advance_walk(pair_walk)
   2681     %advance_walk(val_walk)
   2682     %b(&.step_loop)
   2683 
   2684     :.update_phase
   2685     # Walk pairs_head and vals_head; update ENV.value for each node.
   2686     %ldl(t0, pairs_head)
   2687     %stl(t0, pair_walk)
   2688     %ldl(t0, vals_head)
   2689     %stl(t0, val_walk)
   2690 
   2691     :.update_loop
   2692     %ldl(t0, pair_walk)
   2693     %if_nil(t1, t0, &.iter_loop)
   2694     %car(t1, t0)            ; ENV node
   2695     %ldl(t0, val_walk)
   2696     %car(t2, t0)            ; new val
   2697     %heap_st(t2, t1, %ENV.value)
   2698     %advance_walk(pair_walk)
   2699     %advance_walk(val_walk)
   2700     %b(&.update_loop)
   2701 })
   2702 
   2703 # pmatch_match(pat=a0, subj=a1, env=a2) -> (env=a0, ok=a1)
   2704 #
   2705 # Walks pat and subj structurally. On success returns the (possibly
   2706 # extended) env in a0 and 1 in a1; on failure returns 0 in a1 (a0 is
   2707 # undefined and callers must not use it). Pattern shapes:
   2708 #
   2709 #   - pair (car eq? sym_unquote): binder `,ident` or wildcard `,_`. The
   2710 #     pattern must be exactly (unquote <sym>) — any other shape dies
   2711 #     with msg_bad_unquote_pattern (the only carve-out from the spec's
   2712 #     primitive-failure UB policy, since pattern shape is a syntax
   2713 #     error in the user's source).
   2714 #   - pair (car eq? sym_dollar): record pattern `($ pred (f1 p1) ...)`.
   2715 #     Looks up `pred` in the current env; expects a record predicate
   2716 #     PRIM (the one bound by define-record-type). The TD pulled from
   2717 #     PRIM.data drives the type check on subj and the field-name -> idx
   2718 #     lookup. Each clause matches recursively. Listed fields only;
   2719 #     missing fields are unconstrained. Malformed pattern shape, an
   2720 #     unknown field name, a non-record subject, or a TD mismatch fall
   2721 #     through as ::no.
   2722 #   - pair (otherwise): subj must be a pair; recurse on car, then cdr.
   2723 #   - atomic (fixnum, sym, immediate, identical heap pointer): raw
   2724 #     word equality.
   2725 #   - HEAP-tagged HDR.BV / HDR.STRING: structural byte-for-byte equality
   2726 #     via bv_equal_check, including mixed string/bytevector matches.
   2727 #
   2728 # Locals:
   2729 #   pat
   2730 #   subj
   2731 #   env
   2732 #   td   (record-pattern: TD pulled from the predicate PRIM)
   2733 #   flw  (record-pattern: cursor over remaining (fname pat) clauses)
   2734 %gcfn2(pmatch_match, {pat subj env td flw}, 31, 0, {
   2735     %stl(a0, pat)
   2736     %stl(a1, subj)
   2737     %stl(a2, env)
   2738 
   2739     %tagof(t0, a0)
   2740     %li(t1, %TAG.PAIR)
   2741     %beq(t0, t1, &.pair_pat)
   2742 
   2743     # Atomic pattern. Identity covers fixnum / symbol / immediate / same
   2744     # heap pointer.
   2745     %beq(a0, a1, &.ok)
   2746 
   2747     # Byte-literal equality. pmatch intentionally widens this one literal
   2748     # rule so a string pattern can match a bytevector token and vice versa.
   2749     %bine(t0, %TAG.HEAP, &.no, t1)
   2750     %hdr_type(t1, a0)
   2751     %li(t2, %HDR.BV)
   2752     %beq(t1, t2, &.pat_bytes)
   2753     %li(t2, %HDR.STRING)
   2754     %bne(t1, t2, &.no)
   2755     :.pat_bytes
   2756     %tagof(t1, a1)
   2757     %bine(t1, %TAG.HEAP, &.no, t2)
   2758     %hdr_type(t1, a1)
   2759     %li(t2, %HDR.BV)
   2760     %beq(t1, t2, &.subj_bytes)
   2761     %li(t2, %HDR.STRING)
   2762     %bne(t1, t2, &.no)
   2763     :.subj_bytes
   2764     %call(&bv_equal_check)
   2765     %bieq(a0, %imm_val(%IMM.TRUE), &.ok, t0)
   2766     %b(&.no)
   2767 
   2768     :.pair_pat
   2769     %car(t0, a0)                   ; phead
   2770     %ld_global(t1, &sym_unquote)
   2771     %beq(t0, t1, &.binder)
   2772     %ld_global(t1, &sym_dollar)
   2773     %beq(t0, t1, &.record_pat)
   2774 
   2775     # Structural pair. subj must be a pair too.
   2776     %tagof(t0, a1)
   2777     %bine(t0, %TAG.PAIR, &.no, t1)
   2778 
   2779     # Recurse on the cars; on success, recurse on the cdrs as a tail call.
   2780     %ldl(t0, pat)
   2781     %car(a0, t0)
   2782     %ldl(t0, subj)
   2783     %car(a1, t0)
   2784     %ldl(a2, env)
   2785     %call(&pmatch_match)
   2786     %beqz(a1, &.no)
   2787 
   2788     %mov(a2, a0)                  ; env_after_car
   2789     %ldl(t0, pat)
   2790     %cdr(a0, t0)
   2791     %ldl(t0, subj)
   2792     %cdr(a1, t0)
   2793     %gctail(&pmatch_match)
   2794 
   2795     :.binder
   2796     # Validate (unquote <sym>): cdr(pat) is a pair, cdr(cdr(pat)) is NIL,
   2797     # car(cdr(pat)) is a symbol.
   2798     %ldl(t0, pat)
   2799     %cdr(t1, t0)                  ; cdr(pat)
   2800     %tagof(t0, t1)
   2801     %bine(t0, %TAG.PAIR,           &.bad, t2)
   2802     %cdr(t0, t1)                  ; cdr(cdr(pat))
   2803     %bine(t0, %imm_val(%IMM.NIL), &.bad, t2)
   2804     %car(t0, t1)                  ; pident (kept in t0)
   2805     %tagof(t2, t0)
   2806     %bine(t2, %TAG.SYM,           &.bad, a3)
   2807 
   2808     # Wildcard? Compare against sym_underscore; if so, no binding.
   2809     %ld_global(t1, &sym_underscore)
   2810     %beq(t0, t1, &.ok)
   2811 
   2812     # Bind: env' = env_extend(pident, subj, env). pident lives in t0;
   2813     # env_extend clobbers t0..t2, so move it into a0 right away.
   2814     %mov(a0, t0)
   2815     %ldl(a1, subj)
   2816     %ldl(a2, env)
   2817     %call(&env_extend)
   2818     %li(a1, 1)
   2819     %gceret
   2820 
   2821     :.record_pat
   2822     # pat = ($ pred-sym (f1 p1) (f2 p2) ...). Resolve pred-sym -> PRIM via
   2823     # eval, pull TD from PRIM.data, type-check subj, then iterate the
   2824     # (fname pat_i) clauses. Clobbers `pat` local once we begin the loop:
   2825     # we stash each pat_i there before recursing so the recursion has the
   2826     # right argument and the local stays usable as scratch.
   2827     %ldl(t0, pat)
   2828     %cdr(t1, t0)                  ; (pred-sym . clauses)
   2829     %tagof(t0, t1)
   2830     %bine(t0, %TAG.PAIR, &.no, t2)
   2831     %car(t0, t1)                  ; t0 = pred-sym
   2832     %tagof(t2, t0)
   2833     %bine(t2, %TAG.SYM,  &.no, a3)
   2834     %cdr(t2, t1)                  ; t2 = clauses
   2835     %stl(t2, flw)
   2836 
   2837     # eval(pred-sym, env) -> a0 = pred PRIM (or dies "unbound").
   2838     %mov(a0, t0)
   2839     %ldl(a1, env)
   2840     %call(&eval)
   2841 
   2842     # Verify HEAP / HDR.PRIM, entry == &prim_predicate_entry; extract TD
   2843     # from PRIM.data; sanity-check TD is HEAP / HDR.TD.
   2844     %tagof(t0, a0)
   2845     %bine(t0, %TAG.HEAP, &.no, t1)
   2846     %hdr_type(t0, a0)
   2847     %bine(t0, %HDR.PRIM, &.no, t1)
   2848     %heap_ld(t1, a0, %PRIM.entry_w)
   2849     %la(t2, &prim_predicate_entry)
   2850     %bne(t1, t2, &.no)
   2851     %heap_ld(t1, a0, %PRIM.data)  ; t1 = TD
   2852     %tagof(t0, t1)
   2853     %li(t2, %TAG.HEAP)
   2854     %bne(t0, t2, &.no)
   2855     %hdr_type(t0, t1)
   2856     %li(t2, %HDR.TD)
   2857     %bne(t0, t2, &.no)
   2858     %stl(t1, td)
   2859 
   2860     # Verify subj is HDR.REC with REC.td == TD.
   2861     %ldl(a0, subj)
   2862     %tagof(t0, a0)
   2863     %li(t1, %TAG.HEAP)
   2864     %bne(t0, t1, &.no)
   2865     %hdr_type(t0, a0)
   2866     %li(t1, %HDR.REC)
   2867     %bne(t0, t1, &.no)
   2868     %heap_ld(t0, a0, %REC.td)
   2869     %ldl(t1, td)
   2870     %bne(t0, t1, &.no)
   2871 
   2872     :.record_field_loop
   2873     # flw points at remaining (fname pat_i) clauses; NIL ends the loop.
   2874     %ldl(t0, flw)
   2875     %if_nil(t1, t0, &.ok)
   2876     %car(t1, t0)                  ; t1 = clause
   2877     %tagof(t0, t1)
   2878     %bine(t0, %TAG.PAIR, &.no, t2)
   2879     %car(t2, t1)                  ; t2 = fname
   2880     %cdr(t1, t1)                  ; t1 = (pat_i)
   2881     %tagof(t0, t1)
   2882     %bine(t0, %TAG.PAIR, &.no, a3)
   2883     %car(a3, t1)                  ; a3 = pat_i
   2884     %stl(a3, pat)                 ; reuse `pat` local for pat_i across recursion
   2885 
   2886     # Linear scan of TD.fields for fname; idx accumulated in t1. t2 holds
   2887     # fname (still live); a3 is scratch (since we no longer need pat_i in
   2888     # a register — it's in the local).
   2889     %ldl(t0, td)
   2890     %heap_ld(t0, t0, %TD.fields)
   2891     %li(t1, 0)
   2892     :.record_field_idx_loop
   2893     %if_nil(a3, t0, &.no)
   2894     %car(a3, t0)
   2895     %beq(a3, t2, &.record_field_found)
   2896     %cdr(t0, t0)
   2897     %addi(t1, t1, 1)
   2898     %b(&.record_field_idx_loop)
   2899 
   2900     :.record_field_found
   2901     # val = ld(subj_tagged + (idx<<3) + 13). Same offset arithmetic as
   2902     # prim_accessor_entry. Compute the address into a1 directly so the
   2903     # recursive call's val arg is in place.
   2904     %ldl(a1, subj)
   2905     %shli(t1, t1, 3)
   2906     %add(a1, a1, t1)
   2907     %ld(a1, a1, 13)
   2908 
   2909     # Recurse: pmatch_match(pat_i, val, env). pat_i is in `pat` local.
   2910     %ldl(a0, pat)
   2911     %ldl(a2, env)
   2912     %call(&pmatch_match)
   2913     %beqz(a1, &.no)
   2914     %stl(a0, env)
   2915 
   2916     %ldl(t0, flw)
   2917     %cdr(t0, t0)
   2918     %stl(t0, flw)
   2919     %b(&.record_field_loop)
   2920 
   2921     :.ok
   2922     %ldl(a0, env)
   2923     %li(a1, 1)
   2924     %gceret
   2925 
   2926     :.no
   2927     %li(a1, 0)
   2928     %gceret
   2929 
   2930     :.bad
   2931     %die(msg_bad_unquote_pattern)
   2932 })
   2933 
   2934 # eval_let_named(rest=a0, env=a1) -> value (a0).
   2935 # rest = (name bindings . body). Builds a closure whose captured env
   2936 # contains a self-binding that resolves `name` to the closure itself
   2937 # (set after the closure is allocated by patching the placeholder ENV
   2938 # node). Inits are evaluated in the *original* env (matches let
   2939 # semantics), then we apply the closure.
   2940 #
   2941 # Locals:
   2942 #   rest
   2943 #   env_orig
   2944 #   self_binding  (the placeholder ENV node, patched at the end)
   2945 #   self_env  (same node; its parent is env_orig)
   2946 #   walk  (advances; reset between passes)
   2947 #   head  (current pass's list head — params, then args)
   2948 #   tail  (current pass's list tail)
   2949 #   params  (saved between passes)
   2950 %gcfn2(eval_let_named, {rest env_orig self_binding self_env walk head tail params}, 255, 0, {
   2951     %stl(a0, rest)
   2952     %stl(a1, env_orig)
   2953 
   2954     # 1. self_env = env_extend(name, UNSPEC, env_orig). Keep the same node
   2955     # separately as self_binding so its value can be patched after closure
   2956     # allocation.
   2957     %car(t0, a0)
   2958     %mov(a0, t0)
   2959     %li(a1, %imm_val(%IMM.UNSPEC))
   2960     %ldl(a2, env_orig)
   2961     %call(&env_extend)
   2962     %stl(a0, self_binding)
   2963     %stl(a0, self_env)
   2964 
   2965     # 2. Pass 1: build params list (cdr-tail trick) by walking bindings.
   2966     %li(t0, %imm_val(%IMM.NIL))
   2967     %stl(t0, head)
   2968     %stl(t0, tail)
   2969     %ldl(t0, rest)
   2970     %cdr(t0, t0)
   2971     %car(t0, t0)            ; bindings
   2972     %stl(t0, walk)
   2973 
   2974     :.p1_loop
   2975     %ldl(t0, walk)
   2976     %if_nil(t1, t0, &.p1_done)
   2977 
   2978     %car(t1, t0)
   2979     %car(t2, t1)            ; name
   2980     %mov(a0, t2)
   2981     %li(a1, %imm_val(%IMM.NIL))
   2982     %call(&cons)            ; cell = (name . NIL)
   2983 
   2984     %ldl(t0, head)
   2985     %if_nil(t1, t0, &.p1_first)
   2986     %ldl(t0, tail)
   2987     %set_cdr(a0, t0)
   2988     %stl(a0, tail)
   2989     %b(&.p1_advance)
   2990 
   2991     :.p1_first
   2992     %stl(a0, head)
   2993     %stl(a0, tail)
   2994 
   2995     :.p1_advance
   2996     %advance_walk(walk)
   2997     %b(&.p1_loop)
   2998 
   2999     :.p1_done
   3000     %ldl(t0, head)
   3001     %stl(t0, params)         ; save params
   3002 
   3003     # 3. Pass 2: build args list (eval inits in env_orig).
   3004     %li(t0, %imm_val(%IMM.NIL))
   3005     %stl(t0, head)
   3006     %stl(t0, tail)
   3007     %ldl(t0, rest)
   3008     %cdr(t0, t0)
   3009     %car(t0, t0)
   3010     %stl(t0, walk)
   3011 
   3012     :.p2_loop
   3013     %ldl(t0, walk)
   3014     %if_nil(t1, t0, &.p2_done)
   3015 
   3016     %car(t1, t0)
   3017     %cdr(t2, t1)
   3018     %car(t2, t2)            ; init
   3019     %mov(a0, t2)
   3020     %ldl(a1, env_orig)
   3021     %call(&eval)            ; val
   3022 
   3023     %li(a1, %imm_val(%IMM.NIL))
   3024     %call(&cons)            ; cell = (val . NIL)
   3025 
   3026     %ldl(t0, head)
   3027     %if_nil(t1, t0, &.p2_first)
   3028     %ldl(t0, tail)
   3029     %set_cdr(a0, t0)
   3030     %stl(a0, tail)
   3031     %b(&.p2_advance)
   3032 
   3033     :.p2_first
   3034     %stl(a0, head)
   3035     %stl(a0, tail)
   3036 
   3037     :.p2_advance
   3038     %advance_walk(walk)
   3039     %b(&.p2_loop)
   3040 
   3041     :.p2_done
   3042     # 4. Closure: eval_lambda((params . body), self_env).
   3043     %ldl(a0, params)         ; params
   3044     %ldl(t0, rest)
   3045     %cdr(t0, t0)
   3046     %cdr(a1, t0)            ; body
   3047     %call(&cons)
   3048     %ldl(a1, self_env)
   3049     %call(&eval_lambda)
   3050 
   3051     # 5. Patch self_binding value to closure.
   3052     %ldl(t0, self_binding)
   3053     %heap_st(a0, t0, %ENV.value)
   3054 
   3055     # 6. apply(closure, args).
   3056     %ldl(a1, head)
   3057     %gctail(&apply)
   3058 })
   3059 
   3060 # bind_params(params=a0, args=a1, env=a2) -> extended env (a0).
   3061 # Walks params and args in lockstep, prepending ENV nodes to env.
   3062 # Variadic `.`-tail: when params terminates with a SYM (rather than NIL),
   3063 # bind it to the remaining args list and stop.
   3064 #
   3065 # Locals:
   3066 #   params  (advanced each iteration)
   3067 #   args  (advanced each iteration)
   3068 #   env  (extended each iteration)
   3069 %gcfn2(bind_params, {params args env}, 7, 0, {
   3070     %stl(a0, params)
   3071     %stl(a1, args)
   3072     %stl(a2, env)
   3073 
   3074     :.loop
   3075         %ldl(t0, params)
   3076         %tagof(t1, t0)
   3077         %li(t2, %TAG.PAIR)
   3078         %beq(t1, t2, &.pair)
   3079         %li(t2, %TAG.SYM)
   3080         %beq(t1, t2, &.rest_bind)
   3081         %if_nil(t2, t0, &.fixed_done)
   3082         %b(&.arity_error)
   3083 
   3084         :.pair
   3085         # A fixed parameter requires a corresponding argument.
   3086         %ldl(t0, args)
   3087         %tagof(t1, t0)
   3088         %bine(t1, %TAG.PAIR, &.arity_error, t2)
   3089         # env = env_extend(car(params), car(args), env)
   3090         %ldl(t0, params)
   3091         %car(a0, t0)
   3092         %ldl(t0, args)
   3093         %car(a1, t0)
   3094         %ldl(a2, env)
   3095         %call(&env_extend)
   3096         %stl(a0, env)
   3097 
   3098         # advance params and args
   3099         %advance_walk(params)
   3100         %advance_walk(args)
   3101         %b(&.loop)
   3102 
   3103     :.rest_bind
   3104     # env = env_extend(params_sym, args_list, env)
   3105     %ldl(a0, params)
   3106     %ldl(a1, args)
   3107     %ldl(a2, env)
   3108     %call(&env_extend)
   3109     %stl(a0, env)
   3110     %b(&.done)
   3111 
   3112     :.fixed_done
   3113     # Fixed arity also rejects surplus arguments.
   3114     %ldl(t0, args)
   3115     %if_nil(t1, t0, &.done)
   3116     %b(&.arity_error)
   3117 
   3118     :.done
   3119     %ldl(a0, env)
   3120     %gceret
   3121 
   3122     :.arity_error
   3123     %die(msg_arity)
   3124 })
   3125 
   3126 # eval_body(body=a0, env=a1) -> value of last form (a0).
   3127 # Evaluates each non-last form for effect; tail-evaluates the last so
   3128 # that closures used in tail position do not grow the host stack.
   3129 #
   3130 # Internal `define` is rejected here (this is the single chokepoint for
   3131 # every body context: closure body via apply, let / letrec / named-let
   3132 # bodies, cond clause bodies, begin's body). Per-form check is one
   3133 # tagof + one symbol compare, regardless of body length.
   3134 #
   3135 # Locals:
   3136 #   body
   3137 #   env
   3138 %gcfn2(eval_body, {body env}, 3, 0, {
   3139     :.loop
   3140     %stl(a0, body)
   3141     %stl(a1, env)
   3142 
   3143     # Reject internal `define`. Detect (define ...) at the head of any
   3144     # form before dispatching to eval; top-level-only.
   3145     %car(t0, a0)              ; form
   3146     %tagof(t1, t0)
   3147     %li(t2, %TAG.PAIR)
   3148     %bne(t1, t2, &.not_define)
   3149     %car(t1, t0)              ; head sym
   3150     %ld_global(t2, &sym_define)
   3151     %beq(t1, t2, &.internal_define)
   3152 
   3153     :.not_define
   3154     # If cdr(body) is NIL, body's car is the last form.
   3155     %ldl(a0, body)
   3156     %cdr(t0, a0)
   3157     %if_nil(t1, t0, &.last)
   3158 
   3159     # Non-last form: eval and discard, advance.
   3160     %car(a0, a0)
   3161     %ldl(a1, env)
   3162     %call(&eval)
   3163     %ldl(a0, body)
   3164     %cdr(a0, a0)
   3165     %ldl(a1, env)
   3166     %b(&.loop)
   3167 
   3168     :.last
   3169     %ldl(a0, body)
   3170     %car(a0, a0)
   3171     %ldl(a1, env)
   3172     %gctail(&eval)
   3173 
   3174     :.internal_define
   3175     %die(msg_internal_define)
   3176 })
   3177 
   3178 # =========================================================================
   3179 # Runtime error -- single abort entry point
   3180 # =========================================================================
   3181 #
   3182 # runtime_error(msg_cstr=a0) -> never returns. Every overflow / bounds /
   3183 # unbound / type-failure path lands here so error reporting (and the
   3184 # eventual user-facing `error` primitive) only have to be implemented
   3185 # once. Today we tail into libp1pp's `panic`, which writes msg + LF to
   3186 # stderr and sys_exits 1.
   3187 :runtime_error
   3188     %tail(&panic)
   3189 
   3190 # =========================================================================
   3191 # Source loading -- argv[1] -> readbuf, length stored in readbuf_len
   3192 # =========================================================================
   3193 
   3194 %fn(load_source, 0, {
   3195     %ld_global(a1, &readbuf_buf_ptr)
   3196     %li(a2, %READBUF_CAP_BYTES)
   3197     %call(&read_file)
   3198     %bltz(a0, &.fail)
   3199 
   3200     # If the read filled (or would have filled) the buffer, the source
   3201     # is at least cap bytes; refuse rather than silently truncate.
   3202     # read_file does a single sys_read so n == cap is the only saturation
   3203     # signal we have. We treat n >= cap as overflow defensively.
   3204     %li(t0, %READBUF_CAP_BYTES)
   3205     %bltu(a0, t0, &.ok)
   3206     %die(msg_readbuf_full)
   3207 
   3208     :.ok
   3209     %st_global(a0, &readbuf_len, t0)
   3210     %li(a0, 0)
   3211     %st_global(a0, &readbuf_pos, t0)
   3212     %eret
   3213 
   3214     :.fail
   3215     %die(msg_load_fail)
   3216 })
   3217 
   3218 # =========================================================================
   3219 # Mark-and-sweep collector
   3220 # =========================================================================
   3221 #
   3222 # Marking never allocates.  A marked allocation header is linked through
   3223 # word 1 onto gc_mark_worklist.  Only exact shadow-frame slots and explicit
   3224 # symbol-table roots seed the traversal.
   3225 
   3226 # gc_mark_payload(raw_payload=a0, expected_kind=a1).  Invalid, interior,
   3227 # free, or already-marked addresses are ignored.
   3228 :gc_mark_payload
   3229 .scope
   3230     %beqz(a0, &.done)
   3231     %andi(t2, a0, 7)
   3232     %bnez(t2, &.done)
   3233     %ld_global(t0, &heap_base)
   3234     %addi(t0, t0, %GC_HEADER_BYTES)
   3235     %bltu(a0, t0, &.done)
   3236     %ld_global(t1, &heap_tail)
   3237     %bltu(a0, t1, &.in_heap)
   3238     # A zero-length RAW allocation has its payload exactly at heap_tail;
   3239     # its 16-byte header is still a real managed block.
   3240     %beq(a0, t1, &.in_heap)
   3241     %b(&.done)
   3242     :.in_heap
   3243     %addi(t0, a0, (- %GC_HEADER_BYTES))
   3244     %ld(t1, t0, 0)
   3245     %andi(t2, t1, %GC_KIND_MASK)
   3246     %bne(t2, a1, &.done)
   3247     # Validate the candidate header before putting it on the worklist.
   3248     # Exact roots should always name object starts; these checks reject
   3249     # malformed, truncated, and implausible interior candidates safely.
   3250     %shri(a2, t1, 8)
   3251     %li(a3, %GC_HEADER_BYTES)
   3252     %bltu(a2, a3, &.done)
   3253     %andi(a3, a2, 7)
   3254     %bnez(a3, &.done)
   3255     %add(a3, t0, a2)
   3256     %ld_global(t2, &heap_tail)
   3257     %bltu(t2, a3, &.done)
   3258     %andi(t2, t1, %GC_MARK_BIT)
   3259     %bnez(t2, &.done)
   3260     # An unmarked allocated block keeps its own header address in the
   3261     # intrusive word. This canary distinguishes a real object start from
   3262     # an aligned interior word without a quadratic physical-chain walk.
   3263     %ld(t2, t0, 8)
   3264     %bne(t2, t0, &.done)
   3265     %ori(t1, t1, %GC_MARK_BIT)
   3266     %st(t1, t0, 0)
   3267     %ld_global(t1, &gc_mark_worklist)
   3268     %st(t1, t0, 8)
   3269     %st_global(t0, &gc_mark_worklist, t1)
   3270     :.done
   3271     %ret
   3272 .endscope
   3273 
   3274 # gc_mark_scheme(value=a0).  Tags select the exact managed allocation
   3275 # kind; fixnums, symbols, and immediates are not heap roots.
   3276 :gc_mark_scheme
   3277 .scope
   3278     %tagof(t0, a0)
   3279     %li(t1, %TAG.PAIR)
   3280     %beq(t0, t1, &.pair)
   3281     %li(t1, %TAG.HEAP)
   3282     %beq(t0, t1, &.heap)
   3283     %ret
   3284     :.pair
   3285     %addi(a0, a0, (- %TAG.PAIR))
   3286     %li(a1, %GCKIND.PAIR)
   3287     %b(&gc_mark_payload)
   3288     :.heap
   3289     %addi(a0, a0, (- %TAG.HEAP))
   3290     %li(a1, %GCKIND.HEAP)
   3291     %b(&gc_mark_payload)
   3292 .endscope
   3293 
   3294 :gc_mark_raw
   3295     %li(a1, %GCKIND.RAW)
   3296     %b(&gc_mark_payload)
   3297 
   3298 # Mark all described native-frame slots.
   3299 %fn2(gc_mark_shadow_roots, {frame end native scheme_mask raw_mask slot}, {
   3300     %ld_global(t0, &gc_root_buf_ptr)
   3301     %stl(t0, frame)
   3302     %ld_global(t0, &gc_root_next)
   3303     %stl(t0, end)
   3304     :.frame_loop
   3305     %ldl(t0, frame)
   3306     %ldl(t1, end)
   3307     %beq(t0, t1, &.done)
   3308     %ld(t1, t0, 0)
   3309     %stl(t1, native)
   3310     %stl(t1, slot)
   3311     %ld(t1, t0, 8)
   3312     %stl(t1, scheme_mask)
   3313     %ld(t1, t0, 16)
   3314     %stl(t1, raw_mask)
   3315     :.slot_loop
   3316     %ldl(t0, scheme_mask)
   3317     %ldl(t1, raw_mask)
   3318     %or(t2, t0, t1)
   3319     %beqz(t2, &.next_frame)
   3320     %andi(t2, t0, 1)
   3321     %beqz(t2, &.maybe_raw)
   3322     %ldl(t2, slot)
   3323     %ld(a0, t2, 0)
   3324     %call(&gc_mark_scheme)
   3325     :.maybe_raw
   3326     %ldl(t0, raw_mask)
   3327     %andi(t1, t0, 1)
   3328     %beqz(t1, &.advance_slot)
   3329     %ldl(t1, slot)
   3330     %ld(a0, t1, 0)
   3331     %call(&gc_mark_raw)
   3332     :.advance_slot
   3333     %ldl(t0, scheme_mask)
   3334     %shri(t0, t0, 1)
   3335     %stl(t0, scheme_mask)
   3336     %ldl(t0, raw_mask)
   3337     %shri(t0, t0, 1)
   3338     %stl(t0, raw_mask)
   3339     %ldl(t0, slot)
   3340     %addi(t0, t0, 8)
   3341     %stl(t0, slot)
   3342     %b(&.slot_loop)
   3343     :.next_frame
   3344     %ldl(t0, frame)
   3345     %addi(t0, t0, %GC_ROOT_FRAME_BYTES)
   3346     %stl(t0, frame)
   3347     %b(&.frame_loop)
   3348     :.done
   3349 })
   3350 
   3351 # Symbol entries are permanent roots: the stable RAW name buffers and
   3352 # every Scheme value installed in a global binding.
   3353 %fn2(gc_mark_symbol_roots, {idx count entry}, {
   3354     %li(t0, 0)
   3355     %stl(t0, idx)
   3356     %ld_global(t0, &symtab_count)
   3357     %stl(t0, count)
   3358     :.loop
   3359     %ldl(t0, idx)
   3360     %ldl(t1, count)
   3361     %beq(t0, t1, &.done)
   3362     %symtab_entry(t1, t0, t2)
   3363     %stl(t1, entry)
   3364     %ld(a0, t1, %SYMENT.name_ptr)
   3365     %call(&gc_mark_raw)
   3366     %ldl(t1, entry)
   3367     %ld(a0, t1, %SYMENT.global_val)
   3368     %call(&gc_mark_scheme)
   3369     %ldl(t0, idx)
   3370     %addi(t0, t0, 1)
   3371     %stl(t0, idx)
   3372     %b(&.loop)
   3373     :.done
   3374 })
   3375 
   3376 # Trace one marked allocation header.  The header remains marked while it
   3377 # is off the worklist, so cycles terminate naturally.
   3378 %fn2(gc_trace_header, {header payload object_hdr count cursor td}, {
   3379     %stl(a0, header)
   3380     %addi(t0, a0, %GC_HEADER_BYTES)
   3381     %stl(t0, payload)
   3382     %ld(t1, a0, 0)
   3383     %andi(t1, t1, %GC_KIND_MASK)
   3384     %li(t2, %GCKIND.PAIR)
   3385     %beq(t1, t2, &.pair)
   3386     %li(t2, %GCKIND.HEAP)
   3387     %beq(t1, t2, &.heap)
   3388     %eret                         ; RAW has no outgoing references
   3389 
   3390     :.pair
   3391     %ld(a0, t0, %PAIR.car)
   3392     %call(&gc_mark_scheme)
   3393     %ldl(t0, payload)
   3394     %ld(a0, t0, %PAIR.cdr)
   3395     %tail(&gc_mark_scheme)
   3396 
   3397     :.heap
   3398     %ld(t1, t0, 0)
   3399     %stl(t1, object_hdr)
   3400     %andi(t1, t1, 255)
   3401     %li(t2, %HDR.BV)
   3402     %beq(t1, t2, &.bv)
   3403     %li(t2, %HDR.STRING)
   3404     %beq(t1, t2, &.bv)
   3405     %li(t2, %HDR.CLOSURE)
   3406     %beq(t1, t2, &.closure)
   3407     %li(t2, %HDR.PRIM)
   3408     %beq(t1, t2, &.prim)
   3409     %li(t2, %HDR.TD)
   3410     %beq(t1, t2, &.td)
   3411     %li(t2, %HDR.REC)
   3412     %beq(t1, t2, &.record)
   3413     %li(t2, %HDR.MV)
   3414     %beq(t1, t2, &.mv)
   3415     %li(t2, %HDR.HASH)
   3416     %beq(t1, t2, &.hash)
   3417     %li(t2, %HDR.HASHDATA)
   3418     %beq(t1, t2, &.hashdata)
   3419     %li(t2, %HDR.ENV)
   3420     %beq(t1, t2, &.env)
   3421     %eret
   3422 
   3423     :.bv
   3424     %ld(a0, t0, %BV.data)
   3425     %tail(&gc_mark_raw)
   3426 
   3427     :.closure
   3428     %ld(a0, t0, %CLOSURE.params)
   3429     %call(&gc_mark_scheme)
   3430     %ldl(t0, payload)
   3431     %ld(a0, t0, %CLOSURE.body)
   3432     %call(&gc_mark_scheme)
   3433     %ldl(t0, payload)
   3434     %ld(a0, t0, %CLOSURE.env)
   3435     %tail(&gc_mark_scheme)
   3436 
   3437     :.prim
   3438     # Plain primitives contain zero in data; parameterized primitives
   3439     # contain their TD or tagged field index.
   3440     %ld(a0, t0, %PRIM.data)
   3441     %tail(&gc_mark_scheme)
   3442 
   3443     :.td
   3444     %ld(a0, t0, %TD.fields)
   3445     %tail(&gc_mark_scheme)
   3446 
   3447     :.record
   3448     %ld(t1, t0, %REC.td)
   3449     %stl(t1, td)
   3450     %mov(a0, t1)
   3451     %call(&gc_mark_scheme)
   3452     # A freshly published record may still have its cleared TD slot while
   3453     # its constructor is filling fields. In that state it has no outgoing
   3454     # references yet and is safe to revisit after construction.
   3455     %ldl(t0, td)
   3456     %tagof(t1, t0)
   3457     %li(t2, %TAG.HEAP)
   3458     %bne(t1, t2, &.done)
   3459     %hdr_type(t1, t0)
   3460     %li(t2, %HDR.TD)
   3461     %bne(t1, t2, &.done)
   3462     %heap_ld(t1, t0, %TD.nfields)
   3463     %stl(t1, count)
   3464     %ldl(t0, payload)
   3465     %addi(t0, t0, 16)
   3466     %stl(t0, cursor)
   3467     %b(&.slots)
   3468 
   3469     :.mv
   3470     %ldl(t0, object_hdr)
   3471     %shri(t0, t0, 8)
   3472     %stl(t0, count)
   3473     %ldl(t0, payload)
   3474     %addi(t0, t0, 8)
   3475     %stl(t0, cursor)
   3476     %b(&.slots)
   3477 
   3478     :.hash
   3479     %ld(a0, t0, %HASH.data)
   3480     %tail(&gc_mark_scheme)
   3481 
   3482     :.env
   3483     # ENV.sym is an immediate symbol and ENV.bloom is raw. The mutable value
   3484     # and parent are the node's only managed outgoing references.
   3485     %ld(a0, t0, %ENV.value)
   3486     %call(&gc_mark_scheme)
   3487     %ldl(t0, payload)
   3488     %ld(a0, t0, %ENV.parent)
   3489     %tail(&gc_mark_scheme)
   3490 
   3491     :.hashdata
   3492     # HASHDATA's header carries capacity. Only live slots (stored hash >= 2)
   3493     # contain Scheme references; empty slots and tombstones are skipped.
   3494     %ldl(t0, object_hdr)
   3495     %shri(t0, t0, 8)
   3496     %stl(t0, count)
   3497     %ldl(t0, payload)
   3498     %addi(t0, t0, %HASHDATA.SIZE)
   3499     %stl(t0, cursor)
   3500 
   3501     :.hash_slots
   3502     %ldl(t0, count)
   3503     %beqz(t0, &.done)
   3504     %ldl(t1, cursor)
   3505     %ld(t2, t1, 0)
   3506     %li(a0, 2)
   3507     %bltu(t2, a0, &.hash_next)
   3508     %ld(a0, t1, 8)
   3509     %call(&gc_mark_scheme)
   3510     %ldl(t1, cursor)
   3511     %ld(a0, t1, 16)
   3512     %call(&gc_mark_scheme)
   3513 
   3514     :.hash_next
   3515     %ldl(t0, cursor)
   3516     %addi(t0, t0, 24)
   3517     %stl(t0, cursor)
   3518     %ldl(t0, count)
   3519     %addi(t0, t0, -1)
   3520     %stl(t0, count)
   3521     %b(&.hash_slots)
   3522 
   3523     :.slots
   3524     %ldl(t0, count)
   3525     %beqz(t0, &.done)
   3526     %ldl(t1, cursor)
   3527     %ld(a0, t1, 0)
   3528     %call(&gc_mark_scheme)
   3529     %ldl(t0, cursor)
   3530     %addi(t0, t0, 8)
   3531     %stl(t0, cursor)
   3532     %ldl(t0, count)
   3533     %addi(t0, t0, -1)
   3534     %stl(t0, count)
   3535     %b(&.slots)
   3536     :.done
   3537 })
   3538 
   3539 # Sweep the physical block chain, rebuild an address-ordered free list,
   3540 # coalesce adjacent garbage, clear survivor marks, and return a trailing
   3541 # free run to the unused tail.
   3542 %fn2(gc_sweep, {cursor end free_head list_last list_prev run_free allocated size}, {
   3543     %ld_global(t0, &heap_base)
   3544     %stl(t0, cursor)
   3545     %ld_global(t0, &heap_tail)
   3546     %stl(t0, end)
   3547     %li(t0, 0)
   3548     %stl(t0, free_head)
   3549     %stl(t0, list_last)
   3550     %stl(t0, list_prev)
   3551     %stl(t0, run_free)
   3552     %stl(t0, allocated)
   3553     :.loop
   3554     %ldl(t0, cursor)
   3555     %ldl(t1, end)
   3556     %beq(t0, t1, &.finish)
   3557     %ld(t1, t0, 0)
   3558     %shri(t2, t1, 8)
   3559     %li(a0, %GC_HEADER_BYTES)
   3560     %bltu(t2, a0, &.corrupt)
   3561     %andi(a0, t2, 7)
   3562     %bnez(a0, &.corrupt)
   3563     %add(a0, t0, t2)
   3564     %ldl(a1, end)
   3565     %bltu(a1, a0, &.corrupt)
   3566     %stl(t2, size)
   3567     %andi(a0, t1, %GC_KIND_MASK)
   3568     %beqz(a0, &.garbage)
   3569     %andi(a0, t1, %GC_MARK_BIT)
   3570     %beqz(a0, &.garbage)
   3571 
   3572     # Survivor: clear mark and break any adjacent-free run.
   3573     %li(a0, -129)
   3574     %and(t1, t1, a0)
   3575     %st(t1, t0, 0)
   3576     %st(t0, t0, 8)             ; restore allocated-block start canary
   3577     %ldl(a0, allocated)
   3578     %add(a0, a0, t2)
   3579     %stl(a0, allocated)
   3580     %li(a0, 0)
   3581     %stl(a0, run_free)
   3582     %b(&.advance)
   3583 
   3584     :.garbage
   3585     %ldl(a0, run_free)
   3586     %beqz(a0, &.new_run)
   3587     # Extend the preceding physical free run.
   3588     # Invalidate the absorbed block's old header so a stale exact slot
   3589     # cannot mistake this interior address for an allocated object start.
   3590     %li(a1, 0)
   3591     %st(a1, t0, 0)
   3592     %st(a1, t0, 8)
   3593     %ld(a1, a0, 0)
   3594     %shri(a1, a1, 8)
   3595     %add(a1, a1, t2)
   3596     %shli(a1, a1, 8)
   3597     %st(a1, a0, 0)
   3598     %b(&.advance)
   3599 
   3600     :.new_run
   3601     %shli(t1, t2, 8)          ; FREE kind, mark clear
   3602     %st(t1, t0, 0)
   3603     %li(t1, 0)
   3604     %st(t1, t0, 8)
   3605     %ldl(t1, list_last)
   3606     %stl(t1, list_prev)
   3607     %beqz(t1, &.first_free)
   3608     %st(t0, t1, 8)
   3609     %b(&.linked)
   3610     :.first_free
   3611     %stl(t0, free_head)
   3612     :.linked
   3613     %stl(t0, list_last)
   3614     %stl(t0, run_free)
   3615 
   3616     :.advance
   3617     %ldl(t0, cursor)
   3618     %ldl(t1, size)
   3619     %add(t0, t0, t1)
   3620     %stl(t0, cursor)
   3621     %b(&.loop)
   3622 
   3623     :.finish
   3624     # A final free run is outside the physical chain after trimming.
   3625     %ldl(t0, run_free)
   3626     %beqz(t0, &.publish)
   3627     %st_global(t0, &heap_tail, t1)
   3628     %ldl(t1, list_prev)
   3629     %beqz(t1, &.trim_only)
   3630     %li(t2, 0)
   3631     %st(t2, t1, 8)
   3632     %b(&.publish)
   3633     :.trim_only
   3634     %li(t1, 0)
   3635     %stl(t1, free_head)
   3636 
   3637     :.publish
   3638     %ldl(t0, free_head)
   3639     %st_global(t0, &gc_free_list, t1)
   3640     %ldl(t0, allocated)
   3641     %st_global(t0, &heap_allocated, t1)
   3642     %eret
   3643     :.corrupt
   3644     %die(msg_heap_corrupt)
   3645 })
   3646 
   3647 %fn(gc_collect, 0, {
   3648     %li(t0, 0)
   3649     %st_global(t0, &gc_mark_worklist, t1)
   3650     %call(&gc_mark_shadow_roots)
   3651     %call(&gc_mark_symbol_roots)
   3652     :.drain
   3653     %ld_global(t0, &gc_mark_worklist)
   3654     %beqz(t0, &.sweep)
   3655     %ld(t1, t0, 8)
   3656     %st_global(t1, &gc_mark_worklist, t2)
   3657     %mov(a0, t0)
   3658     %call(&gc_trace_header)
   3659     %b(&.drain)
   3660     :.sweep
   3661     %tail(&gc_sweep)
   3662 })
   3663 
   3664 # =========================================================================
   3665 # Managed heap allocation
   3666 # =========================================================================
   3667 #
   3668 # gc_alloc_try performs coalesced-free-list first fit, then uses the
   3669 # untouched heap tail.  It never collects and returns raw payload 0 on
   3670 # failure.  gc_alloc collects once and retries.  All managed payloads are
   3671 # 8-byte aligned; the two-word allocation header is not visible through
   3672 # existing Scheme object pointers.
   3673 
   3674 %fn2(gc_alloc_try, {total kind prev cur block_size next}, {
   3675     %alignup(a0, a0, 8, t0)
   3676     %addi(a0, a0, %GC_HEADER_BYTES)
   3677     %stl(a0, total)
   3678     %stl(a1, kind)
   3679     %li(t0, 0)
   3680     %stl(t0, prev)
   3681     %ld_global(t0, &gc_free_list)
   3682     %stl(t0, cur)
   3683 
   3684     :.free_loop
   3685     %ldl(t0, cur)
   3686     %beqz(t0, &.tail)
   3687     %ld(t1, t0, 0)
   3688     %shri(t1, t1, 8)
   3689     %stl(t1, block_size)
   3690     %ldl(t2, total)
   3691     %bltu(t1, t2, &.free_next)
   3692 
   3693     # Found first fit.  Split only when the remainder can hold a header
   3694     # plus at least one aligned payload word.
   3695     %ld(t1, t0, 8)
   3696     %stl(t1, next)
   3697     %ldl(t1, block_size)
   3698     %ldl(t2, total)
   3699     %sub(t1, t1, t2)            ; remainder bytes
   3700     %li(t2, (+ %GC_HEADER_BYTES 8))
   3701     %bltu(t1, t2, &.consume)
   3702 
   3703     # remainder_header = current + requested_total
   3704     %ldl(t0, cur)
   3705     %ldl(t2, total)
   3706     %add(t2, t0, t2)
   3707     %shli(t1, t1, 8)            ; FREE kind is zero
   3708     %st(t1, t2, 0)
   3709     %ldl(t1, next)
   3710     %st(t1, t2, 8)
   3711     %ldl(t1, prev)
   3712     %beqz(t1, &.split_head)
   3713     %st(t2, t1, 8)
   3714     %b(&.split_done)
   3715     :.split_head
   3716     %st_global(t2, &gc_free_list, t1)
   3717     :.split_done
   3718     %ldl(t1, total)
   3719     %stl(t1, block_size)
   3720     %b(&.prepare)
   3721 
   3722     :.consume
   3723     %ldl(t1, prev)
   3724     %ldl(t2, next)
   3725     %beqz(t1, &.consume_head)
   3726     %st(t2, t1, 8)
   3727     %b(&.prepare)
   3728     :.consume_head
   3729     %st_global(t2, &gc_free_list, t1)
   3730     %b(&.prepare)
   3731 
   3732     :.free_next
   3733     %ldl(t0, cur)
   3734     %stl(t0, prev)
   3735     %ld(t0, t0, 8)
   3736     %stl(t0, cur)
   3737     %b(&.free_loop)
   3738 
   3739     :.tail
   3740     %ld_global(t0, &heap_tail)
   3741     %ldl(t1, total)
   3742     %add(t2, t0, t1)
   3743     %ld_global(a3, &heap_end)
   3744     %bltu(a3, t2, &.fail)
   3745     %st_global(t2, &heap_tail, a3)
   3746     %stl(t0, cur)
   3747     %stl(t1, block_size)
   3748 
   3749     :.prepare
   3750     # Install the allocated header and account for the entire physical
   3751     # block (including header and any unsplittable tail fragment).
   3752     %ldl(t0, cur)
   3753     %ldl(t1, block_size)
   3754     %shli(t2, t1, 8)
   3755     %ldl(a1, kind)
   3756     %or(t2, t2, a1)
   3757     %st(t2, t0, 0)
   3758     %st(t0, t0, 8)             ; allocated-block start canary
   3759     %ld_global(a3, &heap_allocated)
   3760     %add(a3, a3, t1)
   3761     %st_global(a3, &heap_allocated, t2)
   3762 
   3763     # Clear traced payloads before publishing them.  This makes a
   3764     # partially constructed HEAP object safe if a later field-setting
   3765     # step allocates and triggers collection.
   3766     %ldl(a1, kind)
   3767     %li(t1, %GCKIND.RAW)
   3768     %beq(a1, t1, &.return)
   3769     %addi(t1, t0, %GC_HEADER_BYTES)
   3770     %ldl(t2, block_size)
   3771     %addi(t2, t2, (- %GC_HEADER_BYTES))
   3772     :.clear_loop
   3773     %beqz(t2, &.return)
   3774     %li(a0, 0)
   3775     %st(a0, t1, 0)
   3776     %addi(t1, t1, 8)
   3777     %addi(t2, t2, -8)
   3778     %b(&.clear_loop)
   3779 
   3780     :.return
   3781     %addi(a0, t0, %GC_HEADER_BYTES)
   3782     %eret
   3783 
   3784     :.fail
   3785     %li(a0, 0)
   3786 })
   3787 
   3788 %fn2(gc_alloc, {bytes kind}, {
   3789     %stl(a0, bytes)
   3790     %stl(a1, kind)
   3791     %call(&gc_alloc_try)
   3792     %bnez(a0, &.done)
   3793     %call(&gc_collect)
   3794     %ldl(a0, bytes)
   3795     %ldl(a1, kind)
   3796     %call(&gc_alloc_try)
   3797     %beqz(a0, &.oom)
   3798     :.done
   3799     %eret
   3800     :.oom
   3801     %die(msg_heap_full)
   3802 })
   3803 
   3804 # cons roots both arguments inside the allocator frame because gc_alloc
   3805 # may synchronously collect before returning a payload.
   3806 %gcfn2(cons, {car_value cdr_value}, 3, 0, {
   3807     %stl(a0, car_value)
   3808     %stl(a1, cdr_value)
   3809     %li(a0, %PAIR.SIZE)
   3810     %li(a1, %GCKIND.PAIR)
   3811     %call(&gc_alloc)
   3812     %ldl(t0, car_value)
   3813     %st(t0, a0, %PAIR.car)
   3814     %ldl(t0, cdr_value)
   3815     %st(t0, a0, %PAIR.cdr)
   3816     %addi(a0, a0, %TAG.PAIR)
   3817 })
   3818 
   3819 # alloc_hdr(bytes=a0, hdr_word=a1) -> tagged HEAP object.
   3820 %fn2(alloc_hdr, {bytes hdr_word}, {
   3821     %stl(a0, bytes)
   3822     %stl(a1, hdr_word)
   3823     %li(a1, %GCKIND.HEAP)
   3824     %call(&gc_alloc)
   3825     %ldl(t0, hdr_word)
   3826     %st(t0, a0, 0)
   3827     %addi(a0, a0, %TAG.HEAP)
   3828 })
   3829 
   3830 # env_find(sym=a0, env=a1) -> matching ENV node or 0 (a0). Leaf.
   3831 #
   3832 # Every ENV node carries a cumulative two-bit filter for all symbols in its
   3833 # parent chain. Missing either bit proves the symbol is global, avoiding the
   3834 # linear walk. A possible hit (including filter collisions) still walks and
   3835 # compares exact interned-symbol identities, preserving lexical shadowing.
   3836 :env_find
   3837 .scope
   3838     %if_nil(t0, a1, &.miss)
   3839     %env_sym_mask(t0, a0, t1, t2)
   3840     %heap_ld(t1, a1, %ENV.bloom)
   3841     %and(t1, t1, t0)
   3842     %bne(t1, t0, &.miss)
   3843 
   3844     :.walk
   3845     %heap_ld(t0, a1, %ENV.sym)
   3846     %beq(t0, a0, &.hit)
   3847     %heap_ld(a1, a1, %ENV.parent)
   3848     %if_nil(t0, a1, &.miss)
   3849     %b(&.walk)
   3850 
   3851     :.hit
   3852     %mov(a0, a1)
   3853     %ret
   3854 
   3855     :.miss
   3856     %li(a0, 0)
   3857     %ret
   3858 .endscope
   3859 
   3860 # env_extend(sym=a0, value=a1, parent=a2) -> new ENV head (a0).
   3861 # One 40-byte headered node replaces the prior binding-pair plus alist-cell
   3862 # pair (32 payload bytes across two managed allocations). Its bloom word is
   3863 # the parent's cumulative filter OR this symbol's two bits.
   3864 %gcfn2(env_extend, {sym value parent}, 7, 0, {
   3865     %stl(a0, sym)
   3866     %stl(a1, value)
   3867     %stl(a2, parent)
   3868 
   3869     %li(a0, %ENV.SIZE)
   3870     %li(a1, %HDR.ENV)
   3871     %call(&alloc_hdr)
   3872 
   3873     %ldl(t0, sym)
   3874     %heap_st(t0, a0, %ENV.sym)
   3875     %ldl(t0, value)
   3876     %heap_st(t0, a0, %ENV.value)
   3877     %ldl(t0, parent)
   3878     %heap_st(t0, a0, %ENV.parent)
   3879 
   3880     %if_nil(t1, t0, &.root)
   3881     %heap_ld(t0, t0, %ENV.bloom)
   3882     %b(&.have_parent_bloom)
   3883     :.root
   3884     %li(t0, 0)
   3885     :.have_parent_bloom
   3886     %ldl(t1, sym)
   3887     %env_sym_mask(t1, t1, t2, a3)
   3888     %or(t0, t0, t1)
   3889     %heap_st(t0, a0, %ENV.bloom)
   3890 })
   3891 
   3892 # list_length(list=a0) -> count (a0). Linear walk; clobbers a0 (used as
   3893 # the cursor). Callers that need the list afterward must save it first.
   3894 :list_length
   3895 .scope
   3896     %li(t0, 0)
   3897     :.loop
   3898         %if_nil(t1, a0, &.done)
   3899         %addi(t0, t0, 1)
   3900         %cdr(a0, a0)
   3901         %b(&.loop)
   3902     :.done
   3903     %mov(a0, t0)
   3904     %ret
   3905 .endscope
   3906 
   3907 # =========================================================================
   3908 # Multiple-values protocol
   3909 # =========================================================================
   3910 #
   3911 # An MV-pack is a HEAP-tagged object with header (count << 8) | HDR.MV
   3912 # followed by `count` slot words (raw +8, +16, ...). The R7RS protocol
   3913 # below treats single values and MV-packs uniformly: a 1-value yield is
   3914 # returned as the bare value, while 0 or 2+ values are materialized as
   3915 # an MV-pack. mv_to_list normalizes either form to a list so let-values /
   3916 # call-with-values can reuse the existing destructuring machinery.
   3917 
   3918 # list_to_mv(list=a0) -> tagged MV-pack (a0).
   3919 # Walks `list` to count it, allocates (count+1)*8 bytes with header
   3920 # (count<<8)|HDR.MV, then copies elements into consecutive slots in
   3921 # order. An empty list yields a 0-pack.
   3922 #
   3923 # Locals:
   3924 #   list   (preserved across list_length + alloc_hdr)
   3925 #   count
   3926 #   mv     (tagged MV-pack)
   3927 %gcfn2(list_to_mv, {list count mv pad}, 5, 0, {
   3928     %stl(a0, list)
   3929     %call(&list_length)         ; clobbers a0; returns count
   3930     %stl(a0, count)
   3931 
   3932     # alloc_hdr((count+1)*8, (count<<8)|HDR.MV)
   3933     %addi(a0, a0, 1)
   3934     %shli(a0, a0, 3)
   3935     %ldl(t0, count)
   3936     %shli(t0, t0, 8)
   3937     %ori(a1, t0, %HDR.MV)
   3938     %call(&alloc_hdr)
   3939     %stl(a0, mv)
   3940 
   3941     # Walk list, store at consecutive slots. The first slot's raw byte
   3942     # offset from a tagged HEAP pointer is +5 (= raw+8 - 3).
   3943     %ldl(t0, list)
   3944     %addi(t1, a0, 5)
   3945 
   3946     :.loop
   3947     %if_nil(t2, t0, &.done)
   3948     %car(t2, t0)
   3949     %st(t2, t1, 0)
   3950     %addi(t1, t1, 8)
   3951     %cdr(t0, t0)
   3952     %b(&.loop)
   3953 
   3954     :.done
   3955     %ldl(a0, mv)
   3956 })
   3957 
   3958 # mv_to_list(val=a0) -> list (a0).
   3959 # If val is HEAP-tagged with HDR.MV, build a fresh list of its slots in
   3960 # order. Any other value is wrapped as a single-element list, so callers
   3961 # can uniformly reuse list-shaped destructuring.
   3962 #
   3963 # Locals:
   3964 #   mv     original MV-pack, rooted while fresh list cells are allocated
   3965 #   ptr    (raw cursor into MV slots, walked backward)
   3966 #   count  (remaining slot count)
   3967 %gcfn2(mv_to_list, {mv ptr count}, 1, 0, {
   3968     %stl(a0, mv)
   3969     %tagof(t0, a0)
   3970     %bine(t0, %TAG.HEAP, &.single, t1)
   3971     %hdr_type(t0, a0)
   3972     %bine(t0, %HDR.MV,   &.single, t1)
   3973 
   3974     # MV-pack: count = (hdr >> 8); header sits at raw+0 = tagged-3.
   3975     %ld(t0, a0, -3)
   3976     %shri(t0, t0, 8)
   3977     %stl(t0, count)
   3978 
   3979     # Walk slots back-to-front so each cons prepends, yielding original
   3980     # left-to-right order. Cursor = (tagged+5) + (count-1)*8.
   3981     %addi(t1, a0, 5)
   3982     %shli(t2, t0, 3)
   3983     %add(t1, t1, t2)
   3984     %addi(t1, t1, -8)
   3985     %stl(t1, ptr)
   3986 
   3987     %li(a0, %imm_val(%IMM.NIL))
   3988 
   3989     :.loop
   3990     %ldl(t0, count)
   3991     %beqz(t0, &.done)
   3992 
   3993     %ldl(t1, ptr)
   3994     %ld(t2, t1, 0)
   3995     %mov(a1, a0)
   3996     %mov(a0, t2)
   3997     %call(&cons)
   3998 
   3999     %ldl(t1, ptr)
   4000     %addi(t1, t1, -8)
   4001     %stl(t1, ptr)
   4002     %ldl(t0, count)
   4003     %addi(t0, t0, -1)
   4004     %stl(t0, count)
   4005     %b(&.loop)
   4006 
   4007     :.done
   4008     %gceret
   4009 
   4010     :.single
   4011     # Non-MV: return (val . NIL).
   4012     %li(a1, %imm_val(%IMM.NIL))
   4013     %gctail(&cons)
   4014 })
   4015 
   4016 # =========================================================================
   4017 # Symbol intern -- linear scan, append on miss
   4018 # =========================================================================
   4019 #
   4020 # Locals:
   4021 #   name_ptr  (input)
   4022 #   name_len  (input)
   4023 #   idx  (loop counter / found index)
   4024 #   entry_ptr  (spilled across memcmp)
   4025 %gcfn2(intern, {name_ptr name_len idx entry_ptr}, 0, 1, {
   4026     %stl(a0, name_ptr)
   4027     %stl(a1, name_len)
   4028 
   4029     %li(t0, 0)
   4030     %stl(t0, idx)
   4031 
   4032     :.scan
   4033     # idx >= count? -> append
   4034     %ldl(t0, idx)
   4035     %ld_global(t1, &symtab_count)
   4036     %bltu(t0, t1, &.probe)
   4037     %b(&.append)
   4038 
   4039     :.probe
   4040     %symtab_entry(t1, t0, t2)
   4041     %stl(t1, entry_ptr)
   4042 
   4043     # entry.name_len == name_len ?
   4044     %ld(t2, t1, %SYMENT.name_len)
   4045     %ldl(a2, name_len)
   4046     %bne(t2, a2, &.next)
   4047 
   4048     # memcmp(entry.name_ptr, name_ptr, len)
   4049     %ld(a0, t1, %SYMENT.name_ptr)
   4050     %ldl(a1, name_ptr)
   4051     %ldl(a2, name_len)
   4052     %call(&memcmp)
   4053     %beqz(a0, &.found)
   4054 
   4055     :.next
   4056     %ldl(t0, idx)
   4057     %addi(t0, t0, 1)
   4058     %stl(t0, idx)
   4059     %b(&.scan)
   4060 
   4061     :.append
   4062     # Bounds check; on overflow exit 5 with a message.
   4063     %ldl(t0, idx)
   4064     %li(t1, %SYMTAB_CAP_SLOTS)
   4065     %bltu(t0, t1, &.append_ok)
   4066     %die(msg_symtab_full)
   4067 
   4068     :.append_ok
   4069     # Copy the name into a stable managed RAW buffer. The caller-provided
   4070     # ptr may live in readbuf_buf (parse_atom), so symtab names must
   4071     # outlive source-buffer reuse. The collector roots each name explicitly.
   4072     %ldl(a0, name_len)
   4073     %call(&alloc_bytes)
   4074     %ldl(a1, name_ptr)
   4075     %ldl(a2, name_len)
   4076     %call(&memcpy)              ; returns dst in a0 = stable copy
   4077 
   4078     %ldl(t0, idx)
   4079     %symtab_entry(t1, t0, t2)
   4080     %st(a0, t1, %SYMENT.name_ptr)   ; stable copy
   4081     %ldl(a0, name_len)
   4082     %st(a0, t1, %SYMENT.name_len)
   4083     %li(a0, %imm_val(%IMM.UNBOUND))
   4084     %st(a0, t1, %SYMENT.global_val)
   4085     %li(a0, 0)
   4086     %st(a0, t1, %SYMENT.pad)
   4087 
   4088     # symtab_count = idx + 1
   4089     %addi(a0, t0, 1)
   4090     %st_global(a0, &symtab_count, t2)
   4091 
   4092     # fall through with idx in t0 = sp[16]
   4093 
   4094     :.found
   4095     %ldl(t0, idx)
   4096     %shli(a0, t0, 3)
   4097     %ori(a0, a0, %TAG.SYM)
   4098 })
   4099 
   4100 # Lookup by sym_idx (untagged, in a0). Returns symtab[idx].global_val in a0.
   4101 # Leaf.
   4102 :sym_global
   4103     %ld_global(t0, &symtab_buf_ptr)
   4104     %ld_array(a0, t0, %SYMENT.SIZE, a0, %SYMENT.global_val, t1)
   4105     %ret
   4106 
   4107 # sym_set_global(idx=a0, val=a1). Leaf.
   4108 :sym_set_global
   4109     %ld_global(t0, &symtab_buf_ptr)
   4110     %st_array(a1, t0, %SYMENT.SIZE, a0, %SYMENT.global_val, t1)
   4111     %ret
   4112 
   4113 # =========================================================================
   4114 # Primitives
   4115 # =========================================================================
   4116 #
   4117 # PRIM objects live on the heap so the bump allocator's 8-byte alignment
   4118 # is what makes (heap_ptr & 7 == 0) hold; that's what lets `+3` encode
   4119 # the HEAP tag cleanly. (A static :prim_sys_exit emitted in the data
   4120 # section was at the mercy of preceding code length and could land at
   4121 # any 4-byte alignment, producing tag bits 5 or 7 instead of 3.)
   4122 #
   4123 # register_primitives walks prim_table at startup. Each table entry is
   4124 # 24 bytes: 8-byte name_ptr (4-byte label ref + 4 pad), 8-byte name_len,
   4125 # 8-byte entry_label (4 ref + 4 pad). For each entry we alloc a 16-byte
   4126 # PRIM, write the entry-label into the prim header's entry slot, intern
   4127 # the surface name, and bind the symbol's global slot to the HEAP-tagged
   4128 # prim pointer.
   4129 #
   4130 # Locals:
   4131 #   prim  ptr (HEAP-tagged; spilled across intern + sym_set_global)
   4132 #   walk  (current table cursor)
   4133 #   end  (table_end)
   4134 %gcfn2(register_primitives, {prim walk end}, 1, 0, {
   4135     %la(t0, &prim_table)
   4136     %stl(t0, walk)
   4137     %la(t0, &prim_table_end)
   4138     %stl(t0, end)
   4139 
   4140     :.loop
   4141     %ldl(t0, walk)
   4142     %ldl(t1, end)
   4143     %beq(t0, t1, &.done)
   4144 
   4145     # alloc_hdr(24, HDR.PRIM) -> HEAP-tagged a0. The third slot (offset 13
   4146     # from tagged) holds per-instance data and stays zero for the
   4147     # primitives registered here -- only parameterized prims (record
   4148     # ctor/predicate/accessor/mutator) read it.
   4149     %li(a0, 24)
   4150     %li(a1, %HDR.PRIM)
   4151     %call(&alloc_hdr)
   4152     %stl(a0, prim)
   4153 
   4154     # Write entry-label into prim's entry slot.
   4155     %ldl(t0, walk)
   4156     %ld(t1, t0, 16)
   4157     %ldl(t2, prim)
   4158     %heap_st(t1, t2, %PRIM.entry_w)
   4159 
   4160     # Intern surface name; bind global to prim ptr.
   4161     %ldl(t0, walk)
   4162     %ld(a0, t0, 0)
   4163     %ld(a1, t0, 8)
   4164     %call(&intern)
   4165     %untag_sym(a0, a0)
   4166     %ldl(a1, prim)
   4167     %call(&sym_set_global)
   4168 
   4169     %ldl(t0, walk)
   4170     %addi(t0, t0, 24)
   4171     %stl(t0, walk)
   4172     %b(&.loop)
   4173 
   4174     :.done
   4175 })
   4176 
   4177 %fn(register_globals, 0, {
   4178     # Bind `eof` as a direct global -> IMM.EOF value. (Predicate is `eof?`,
   4179     # registered via prim_table.) Cheaper and shorter than a 0-arg thunk.
   4180     %la(a0, &name_eof)
   4181     %li(a1, 3)
   4182     %call(&intern)
   4183     %untag_sym(a0, a0)
   4184     %li(a1, %imm_val(%IMM.EOF))
   4185     %call(&sym_set_global)
   4186 })
   4187 
   4188 # Each primitive is a leaf reached via apply's %tailr: args list is in a0,
   4189 # and the result goes back in a0. Most use no frame at all; the few that
   4190 # need recursion (apply) carry a small one via %fn.
   4191 #
   4192 # Arithmetic / compare / bitwise primitives on tagged fixnums take
   4193 # advantage of the (n << 3) representation: + / - / signed compare /
   4194 # bit-and / bit-or / bit-xor all work directly on the tagged words, so
   4195 # the variadic fold loop preserves the tag at every step. Only * has to
   4196 # untag each incoming operand to avoid a stray <<6.
   4197 
   4198 # (sys-exit code) -- libp1pp's sys_exit doesn't return; %b, not %call.
   4199 :prim_sys_exit_entry
   4200     %car(a0, a0)
   4201     %untag_fix(a0, a0)
   4202     %b(&sys_exit)
   4203 
   4204 # (cons a b) -> tagged pair.
   4205 :prim_cons_entry
   4206     %car(t0, a0)
   4207     %cdr(t1, a0)
   4208     %car(t1, t1)
   4209     %mov(a0, t0)
   4210     %mov(a1, t1)
   4211     %b(&cons)
   4212 
   4213 # (car p), (cdr p)
   4214 :prim_car_entry
   4215     %car(a0, a0)
   4216     %car(a0, a0)
   4217     %ret
   4218 
   4219 :prim_cdr_entry
   4220     %car(a0, a0)
   4221     %cdr(a0, a0)
   4222     %ret
   4223 
   4224 # Predicate primitives. Same shape: extract the arg, compare, return one
   4225 # of the two boolean immediates.
   4226 
   4227 :prim_nullq_entry
   4228 .scope
   4229     %car(t0, a0)
   4230     %li(a0, %imm_val(%IMM.TRUE))
   4231     %if_nil(t1, t0, &.end)
   4232     %li(a0, %imm_val(%IMM.FALSE))
   4233     :.end
   4234     %ret
   4235 .endscope
   4236 
   4237 :prim_pairq_entry
   4238 .scope
   4239     %car(t0, a0)
   4240     %tagof(t1, t0)
   4241     %li(t2, %TAG.PAIR)
   4242     %li(a0, %imm_val(%IMM.FALSE))
   4243     %bne(t1, t2, &.end)
   4244     %li(a0, %imm_val(%IMM.TRUE))
   4245     :.end
   4246     %ret
   4247 .endscope
   4248 
   4249 # (string? x) -- #t iff x is a HEAP-tagged HDR.STRING.
   4250 :prim_stringq_entry
   4251 .scope
   4252     %car(t0, a0)
   4253     %li(a0, %imm_val(%IMM.FALSE))
   4254     %tagof(t1, t0)
   4255     %li(t2, %TAG.HEAP)
   4256     %bne(t1, t2, &.end)
   4257     %hdr_type(t1, t0)
   4258     %li(t2, %HDR.STRING)
   4259     %bne(t1, t2, &.end)
   4260     %li(a0, %imm_val(%IMM.TRUE))
   4261     :.end
   4262     %ret
   4263 .endscope
   4264 
   4265 # (bytevector? x) -- #t iff x is a HEAP-tagged HDR.BV.
   4266 :prim_bytevectorq_entry
   4267 .scope
   4268     %car(t0, a0)
   4269     %li(a0, %imm_val(%IMM.FALSE))
   4270     %tagof(t1, t0)
   4271     %li(t2, %TAG.HEAP)
   4272     %bne(t1, t2, &.end)
   4273     %hdr_type(t1, t0)
   4274     %li(t2, %HDR.BV)
   4275     %bne(t1, t2, &.end)
   4276     %li(a0, %imm_val(%IMM.TRUE))
   4277     :.end
   4278     %ret
   4279 .endscope
   4280 
   4281 # (bytes? x) -- boot2 bridge predicate: string or bytevector.
   4282 :prim_bytesq_entry
   4283 .scope
   4284     %car(t0, a0)
   4285     %li(a0, %imm_val(%IMM.FALSE))
   4286     %tagof(t1, t0)
   4287     %li(t2, %TAG.HEAP)
   4288     %bne(t1, t2, &.end)
   4289     %hdr_type(t1, t0)
   4290     %li(t2, %HDR.BV)
   4291     %beq(t1, t2, &.true)
   4292     %li(t2, %HDR.STRING)
   4293     %bne(t1, t2, &.end)
   4294     :.true
   4295     %li(a0, %imm_val(%IMM.TRUE))
   4296     :.end
   4297     %ret
   4298 .endscope
   4299 
   4300 # (set-car! pair val) / (set-cdr! pair val) -- in-place pair mutation.
   4301 # No type check (matches car/cdr's lax stance); both return UNSPEC.
   4302 :prim_set_car_entry
   4303     %args2(t0, t1, a0)
   4304     %set_car(t1, t0)
   4305     %li(a0, %imm_val(%IMM.UNSPEC))
   4306     %ret
   4307 
   4308 :prim_set_cdr_entry
   4309     %args2(t0, t1, a0)
   4310     %set_cdr(t1, t0)
   4311     %li(a0, %imm_val(%IMM.UNSPEC))
   4312     %ret
   4313 
   4314 # (length xs) -- count of pairs in a proper list. Forwards to the
   4315 # list_length helper (which clobbers a0 as the cursor) and tags the
   4316 # resulting count as a fixnum. Needs a frame because %call(&list_length)
   4317 # would otherwise clobber lr and the trailing %ret would loop.
   4318 %fn(prim_length_entry, 0, {
   4319     %car(a0, a0)
   4320     %call(&list_length)
   4321     %mkfix(a0, a0)
   4322     %eret
   4323 })
   4324 
   4325 # (list-ref xs n) -- 0-indexed nth element. n is a fixnum; we untag,
   4326 # advance via cdr, then car. Out-of-range is undefined behavior, same
   4327 # as car/cdr on '().
   4328 :prim_list_ref_entry
   4329 .scope
   4330     %args2(t0, t1, a0)
   4331     %sari(t1, t1, 3)
   4332     :.loop
   4333     %beqz(t1, &.done)
   4334     %cdr(t0, t0)
   4335     %addi(t1, t1, -1)
   4336     %b(&.loop)
   4337     :.done
   4338     %car(a0, t0)
   4339     %ret
   4340 .endscope
   4341 
   4342 # (assq key alist) -> matching pair or #f. Walks alist, comparing
   4343 # car of each pair to key by identity (eq?); first match wins. Pure
   4344 # leaf -- no allocation, no calls. Replaces the interpreted prelude
   4345 # define so file-scope alist lookups (e.g. cc.scm scope-bind!'s
   4346 # redecl check) don't pay bind_params env-cons cost per step.
   4347 :prim_assq_entry
   4348 .scope
   4349     %args2(t0, t1, a0)         ; t0=key, t1=alist
   4350     :.loop
   4351     %if_nil(t2, t1, &.miss)
   4352     %car(t2, t1)               ; pair = (car alist)
   4353     %car(a0, t2)               ; (car pair)
   4354     %beq(a0, t0, &.hit)
   4355     %cdr(t1, t1)
   4356     %b(&.loop)
   4357     :.hit
   4358     %mov(a0, t2)
   4359     %ret
   4360     :.miss
   4361     %li(a0, %imm_val(%IMM.FALSE))
   4362     %ret
   4363 .endscope
   4364 
   4365 # (assoc key alist) -> matching pair or #f. Same shape as assq but
   4366 # the key compare goes through equal_recurse, which means we need a
   4367 # frame to preserve the key/cursor/current-pair across the call.
   4368 #
   4369 # Locals:
   4370 #   key
   4371 #   cursor
   4372 #   pair  (saved across equal_recurse so we can return it on hit)
   4373 %fn2(prim_assoc_entry, {key cursor pair}, {
   4374     %args2(t0, t1, a0)
   4375     %stl(t0, key)
   4376     %stl(t1, cursor)
   4377 
   4378     :.loop
   4379     %ldl(t1, cursor)
   4380     %if_nil(t2, t1, &.miss)
   4381     %car(t2, t1)               ; pair = (car cursor)
   4382     %stl(t2, pair)
   4383     %car(a0, t2)               ; (car pair)
   4384     %ldl(a1, key)
   4385     %call(&equal_recurse)
   4386     %bieq(a0, %imm_val(%IMM.FALSE), &.next, t0)
   4387     %ldl(a0, pair)
   4388     %eret
   4389 
   4390     :.next
   4391     %ldl(t1, cursor)
   4392     %cdr(t1, t1)
   4393     %stl(t1, cursor)
   4394     %b(&.loop)
   4395 
   4396     :.miss
   4397     %li(a0, %imm_val(%IMM.FALSE))
   4398 })
   4399 
   4400 # (reverse list) -> fresh reversed list. Walks the input forward,
   4401 # consing each element onto an accumulator; result is the accumulator.
   4402 # One fresh PAIR per input element, no intermediates. Frame needed
   4403 # because cons is a leaf and %call clobbers lr.
   4404 #
   4405 # Locals:
   4406 #   xs   (cursor; advanced each iteration)
   4407 #   acc
   4408 %gcfn2(prim_reverse_entry, {xs acc}, 3, 0, {
   4409     %car(t0, a0)               ; t0 = list arg
   4410     %stl(t0, xs)
   4411     %li(t0, %imm_val(%IMM.NIL))
   4412     %stl(t0, acc)
   4413 
   4414     :.loop
   4415     %ldl(t0, xs)
   4416     %if_nil(t1, t0, &.done)
   4417     %car(a0, t0)
   4418     %ldl(a1, acc)
   4419     %call(&cons)
   4420     %stl(a0, acc)
   4421     %ldl(t0, xs)
   4422     %cdr(t0, t0)
   4423     %stl(t0, xs)
   4424     %b(&.loop)
   4425 
   4426     :.done
   4427     %ldl(a0, acc)
   4428 })
   4429 
   4430 # (bytevector-append bv ...) -- variadic concatenation. Two passes:
   4431 # the first sums the bv lengths so we can size the result up front; the
   4432 # second walks the args again and memcpy's each src into the result.
   4433 # The args list head is saved at +0 because pass 1 walks a separate
   4434 # cursor (t1) and pass 2 needs to re-read the head. memcpy clobbers
   4435 # t-regs, so the running write offset and remaining-args cursor live
   4436 # in the frame across each call.
   4437 #
   4438 # Locals:
   4439 #   args  list head (re-read for pass 2; cursor during pass 2)
   4440 #   total  length (raw)
   4441 #   result  bv
   4442 #   write  offset (raw, into result.data)
   4443 %gcfn2(prim_bv_append_entry, {args total result write}, 5, 0, {
   4444     %stl(a0, args)
   4445 
   4446     %li(t0, 0)
   4447     %mov(t1, a0)
   4448     :.sum_loop
   4449         %if_nil(t2, t1, &.sum_done)
   4450         %car(t2, t1)
   4451         %heap_ld(a0, t2, %BV.hdr)
   4452         %shri(a0, a0, 8)
   4453         %add(t0, t0, a0)
   4454         %cdr(t1, t1)
   4455         %b(&.sum_loop)
   4456     :.sum_done
   4457     %stl(t0, total)
   4458 
   4459     %mov(a0, t0)
   4460     %call(&bv_alloc)
   4461     %stl(a0, result)
   4462 
   4463     %li(t0, 0)
   4464     %stl(t0, write)
   4465 
   4466     :.copy_loop
   4467         %ldl(t0, args)
   4468         %if_nil(t1, t0, &.copy_done)
   4469         %car(t1, t0)                ; src bv
   4470         %heap_ld(t2, t1, %BV.hdr)
   4471         %shri(t2, t2, 8)            ; src length
   4472 
   4473         %ldl(a0, result)
   4474         %heap_ld(a0, a0, %BV.data)  ; result.data
   4475         %ldl(a3, write)
   4476         %add(a0, a0, a3)            ; dst = result.data + offset
   4477         %heap_ld(a1, t1, %BV.data)  ; src.data
   4478         %mov(a2, t2)                ; count
   4479 
   4480         %add(a3, a3, t2)
   4481         %stl(a3, write)
   4482         %cdr(t0, t0)
   4483         %stl(t0, args)
   4484 
   4485         %call(&memcpy)
   4486         %b(&.copy_loop)
   4487     :.copy_done
   4488 
   4489     %ldl(a0, result)
   4490 })
   4491 
   4492 # (string->symbol bv) -- intern the bytes and return the SYM-tagged
   4493 # value. intern copies the name into stable heap storage if it has to
   4494 # append, so the bv's data buffer is safe to relocate afterwards.
   4495 :prim_string_to_symbol_entry
   4496     %car(t0, a0)
   4497     %heap_ld(a0, t0, %BV.data)
   4498     %heap_ld(a1, t0, %BV.hdr)
   4499     %shri(a1, a1, 8)            ; length
   4500     %b(&intern)
   4501 
   4502 # (symbol->string sym) -- fresh string copy of the symtab name. sym_name
   4503 # returns (ptr, len); string_alloc gives us a length-bearing wrapper;
   4504 # memcpy fills the data. Frame holds the (ptr, len) pair across
   4505 # str_alloc and the resulting bv across memcpy.
   4506 
   4507 %gcfn2(prim_symbol_to_string_entry, {ptr len bv}, 4, 0, {
   4508     %car(a0, a0)
   4509     %sari(a0, a0, 3)            ; raw sym idx
   4510     %call(&sym_name)            ; -> ptr (a0), len (a1)
   4511     %stl(a0, ptr)
   4512     %stl(a1, len)
   4513     %mov(a0, a1)
   4514     %call(&string_alloc)        ; tagged string in a0
   4515     %stl(a0, bv)
   4516     %ldl(a1, ptr)              ; src ptr
   4517     %ldl(a2, len)              ; len
   4518     %heap_ld(t0, a0, %BV.data)  ; dst = bv.data
   4519     %mov(a0, t0)
   4520     %call(&memcpy)
   4521     %ldl(a0, bv)
   4522 })
   4523 
   4524 # (number->string n [radix]) -- fresh string with the integer's text form.
   4525 # Radices other than the micro-supported 10 and 16 fail closed.
   4526 
   4527 %fn2(prim_number_to_string_entry, {value radix}, {
   4528     %car(t0, a0)
   4529     %sari(t0, t0, 3)            ; raw value
   4530     %stl(t0, value)
   4531 
   4532     # Default radix = 10. If a second arg is present, untag it.
   4533     %li(t0, 10)
   4534     %stl(t0, radix)
   4535     %cdr(t1, a0)
   4536     %if_nil(t0, t1, &.have_radix)
   4537     %car(t0, t1)
   4538     %sari(t0, t0, 3)
   4539     %stl(t0, radix)
   4540     :.have_radix
   4541 
   4542     %li(a0, 0)
   4543     %call(&str_alloc)
   4544     %ldl(a1, value)
   4545     %ldl(t0, radix)
   4546     %bieq(t0, 16, &.hex, t1)
   4547     %bine(t0, 10, &.bad_radix, t1)
   4548     %call(&str_putint)
   4549     %b(&.finish)
   4550     :.hex
   4551     %call(&str_puthex)
   4552     :.finish
   4553     # str_put* builds a NUL-bearing bytevector; retag only the heap header,
   4554     # preserving its logical length, data pointer, and capacity.
   4555     %heap_ld(t0, a0, %BV.hdr)
   4556     %ori(t0, t0, %HDR.STRING)
   4557     %heap_st(t0, a0, %BV.hdr)
   4558     %eret
   4559     :.bad_radix
   4560     %die(msg_bad_radix)
   4561 })
   4562 
   4563 # (string->number bv [radix]) -- decimal goes through parse_dec; radix
   4564 # 16 strips an optional leading '-' and calls parse_hex over the
   4565 # remainder, demanding it consume every byte. Returns #f on
   4566 # non-byte input, empty string, lone sign, or any non-recognized byte.
   4567 # Radices other than 10 and 16 fail closed.
   4568 %fn2(prim_string_to_number_entry, {args ptr len sign}, {
   4569     %stl(a0, args)
   4570 
   4571     %car(t2, a0)
   4572     %tagof(t0, t2)
   4573     %bine(t0, %TAG.HEAP, &.fail, t1)
   4574     %hdr_type(t0, t2)
   4575     %li(t1, %HDR.BV)
   4576     %beq(t0, t1, &.bytes_ok)
   4577     %li(t1, %HDR.STRING)
   4578     %bne(t0, t1, &.fail)
   4579     :.bytes_ok
   4580 
   4581     %heap_ld(t0, t2, %BV.data)
   4582     %heap_ld(t1, t2, %BV.hdr)
   4583     %shri(t1, t1, 8)            ; length
   4584     %stl(t0, ptr)
   4585     %stl(t1, len)
   4586 
   4587     # Inspect the optional radix arg.
   4588     %ldl(t0, args)
   4589     %cdr(t0, t0)
   4590     %if_nil(t1, t0, &.dec)
   4591     %car(t1, t0)
   4592     %sari(t1, t1, 3)
   4593     %bieq(t1, 16, &.hex, t2)
   4594     %bine(t1, 10, &.bad_radix, t2)
   4595 
   4596     :.dec
   4597     %ldl(a0, ptr)
   4598     %ldl(a1, len)
   4599     %beqz(a1, &.fail)
   4600     %lb(t0, a0, 0)
   4601     %bcne(t0, -43, &.dec_no_plus, t0)    ; '+'
   4602     %addi(a0, a0, 1)
   4603     %addi(a1, a1, -1)
   4604     %beqz(a1, &.fail)
   4605     :.dec_no_plus
   4606     %stl(a1, len)               ; save adjusted len
   4607     %call(&parse_exact_dec)     ; -> (raw_val=a0, consumed=a1)
   4608     %ldl(t0, len)
   4609     %bne(a1, t0, &.fail)       ; partial parse -> fail
   4610     %mov(t0, a0)
   4611     %mkfix(a0, t0)
   4612     %untag_fix(t1, a0)
   4613     %bne(t0, t1, &.overflow)
   4614     %b(&.end)
   4615 
   4616     :.hex
   4617     # Strip optional leading '+' / '-'.
   4618     %li(t0, 0)
   4619     %stl(t0, sign)
   4620     %ldl(t0, len)
   4621     %beqz(t0, &.fail)
   4622     %ldl(t1, ptr)
   4623     %lb(t2, t1, 0)
   4624     %addi(t0, t2, -45)          ; '-'
   4625     %beqz(t0, &.hex_neg)
   4626     %addi(t0, t2, -43)          ; '+'
   4627     %beqz(t0, &.hex_skip_sign)
   4628     %b(&.hex_parse)
   4629     :.hex_neg
   4630     %li(t0, 1)
   4631     %stl(t0, sign)
   4632     :.hex_skip_sign
   4633     %ldl(t0, ptr)
   4634     %addi(t0, t0, 1)
   4635     %stl(t0, ptr)
   4636     %ldl(t0, len)
   4637     %addi(t0, t0, -1)
   4638     %stl(t0, len)
   4639     %beqz(t0, &.fail)
   4640 
   4641     :.hex_parse
   4642     # Normalize leading zeroes and cap significant digits before calling
   4643     # the machine-word parser, then enforce the signed tagged range.
   4644     :.hex_trim_zero
   4645     %ldl(t0, len)
   4646     %li(t1, 1)
   4647     %beq(t0, t1, &.hex_count_check)
   4648     %ldl(t1, ptr)
   4649     %lb(t2, t1, 0)
   4650     %addi(t2, t2, -48)
   4651     %bnez(t2, &.hex_count_check)
   4652     %addi(t1, t1, 1)
   4653     %stl(t1, ptr)
   4654     %addi(t0, t0, -1)
   4655     %stl(t0, len)
   4656     %b(&.hex_trim_zero)
   4657     :.hex_count_check
   4658     %li(t1, (+ (/ %p1_word_bits 4) 1))
   4659     %bltu(t0, t1, &.hex_parse_go)
   4660     %b(&.overflow)
   4661     :.hex_parse_go
   4662     %ldl(a0, ptr)
   4663     %ldl(a1, len)
   4664     %call(&parse_hex)            ; -> (a0=value, a1=consumed)
   4665     %ldl(t0, len)
   4666     %bne(a1, t0, &.fail)        ; demand full consumption
   4667     %li(t0, -1)
   4668     %shri(t0, t0, 4)
   4669     %ldl(t1, sign)
   4670     %beqz(t1, &.hex_limit_ready)
   4671     %addi(t0, t0, 1)
   4672     :.hex_limit_ready
   4673     %bltu(t0, a0, &.overflow)
   4674     %ldl(t0, sign)
   4675     %beqz(t0, &.hex_pos)
   4676     %li(t1, 0)
   4677     %sub(a0, t1, a0)
   4678     :.hex_pos
   4679     %mov(t0, a0)
   4680     %mkfix(a0, t0)
   4681     %untag_fix(t1, a0)
   4682     %bne(t0, t1, &.overflow)
   4683     %b(&.end)
   4684 
   4685     :.bad_radix
   4686     %die(msg_bad_radix)
   4687 
   4688     :.overflow
   4689     %die(msg_integer_overflow)
   4690 
   4691     :.fail
   4692     %li(a0, %imm_val(%IMM.FALSE))
   4693     :.end
   4694 })
   4695 
   4696 # (boolean? x) -- #t iff x is the IMM.FALSE or IMM.TRUE singleton.
   4697 :prim_booleanq_entry
   4698 .scope
   4699     %car(t0, a0)
   4700     %li(a0, %imm_val(%IMM.TRUE))
   4701     %li(t1, %imm_val(%IMM.FALSE))
   4702     %beq(t0, t1, &.end)
   4703     %li(t1, %imm_val(%IMM.TRUE))
   4704     %beq(t0, t1, &.end)
   4705     %li(a0, %imm_val(%IMM.FALSE))
   4706     :.end
   4707     %ret
   4708 .endscope
   4709 
   4710 # (integer? x) -- #t iff x is a fixnum (low 3 tag bits == TAG.FIXNUM == 0).
   4711 :prim_integerq_entry
   4712 .scope
   4713     %car(t0, a0)
   4714     %tagof(t1, t0)
   4715     %li(a0, %imm_val(%IMM.FALSE))
   4716     %bnez(t1, &.end)
   4717     %li(a0, %imm_val(%IMM.TRUE))
   4718     :.end
   4719     %ret
   4720 .endscope
   4721 
   4722 # Characters are immediate byte values with their own tag, disjoint from
   4723 # exact integers.
   4724 :prim_charq_entry
   4725 .scope
   4726     %car(t0, a0)
   4727     %tagof(t1, t0)
   4728     %li(t2, %TAG.CHAR)
   4729     %li(a0, %imm_val(%IMM.FALSE))
   4730     %bne(t1, t2, &.end)
   4731     %li(a0, %imm_val(%IMM.TRUE))
   4732     :.end
   4733     %ret
   4734 .endscope
   4735 
   4736 :prim_char_to_integer_entry
   4737 .scope
   4738     %car(t0, a0)
   4739     %tagof(t1, t0)
   4740     %bine(t1, %TAG.CHAR, &.bad, t2)
   4741     %untag_char(t0, t0)
   4742     %mkfix(a0, t0)
   4743     %ret
   4744     :.bad
   4745     %die(msg_type)
   4746 .endscope
   4747 
   4748 :prim_integer_to_char_entry
   4749 .scope
   4750     %car(t0, a0)
   4751     %tagof(t1, t0)
   4752     %bine(t1, %TAG.FIXNUM, &.type, t2)
   4753     %untag_fix(t0, t0)
   4754     %bltz(t0, &.bad)
   4755     %li(t1, 256)
   4756     %bltu(t0, t1, &.ok)
   4757     :.bad
   4758     %die(msg_bad_char)
   4759     :.type
   4760     %die(msg_type)
   4761     :.ok
   4762     %mkchar(a0, t0)
   4763     %ret
   4764 .endscope
   4765 
   4766 # (symbol? x) -- #t iff x is TAG.SYM (interned symbol index, not a heap obj).
   4767 :prim_symbolq_entry
   4768 .scope
   4769     %car(t0, a0)
   4770     %tagof(t1, t0)
   4771     %li(t2, %TAG.SYM)
   4772     %li(a0, %imm_val(%IMM.FALSE))
   4773     %bne(t1, t2, &.end)
   4774     %li(a0, %imm_val(%IMM.TRUE))
   4775     :.end
   4776     %ret
   4777 .endscope
   4778 
   4779 # (procedure? x) -- #t iff x is HEAP-tagged with header HDR.CLOSURE or HDR.PRIM.
   4780 :prim_procedureq_entry
   4781 .scope
   4782     %car(t0, a0)
   4783     %tagof(t1, t0)
   4784     %li(t2, %TAG.HEAP)
   4785     %li(a0, %imm_val(%IMM.FALSE))
   4786     %bne(t1, t2, &.end)
   4787     %hdr_type(t1, t0)
   4788     %li(t2, %HDR.CLOSURE)
   4789     %beq(t1, t2, &.yes)
   4790     %li(t2, %HDR.PRIM)
   4791     %beq(t1, t2, &.yes)
   4792     %b(&.end)
   4793     :.yes
   4794     %li(a0, %imm_val(%IMM.TRUE))
   4795     :.end
   4796     %ret
   4797 .endscope
   4798 
   4799 :prim_zeroq_entry
   4800 .scope
   4801     %car(t0, a0)
   4802     %li(a0, %imm_val(%IMM.FALSE))
   4803     %bnez(t0, &.end)
   4804     %li(a0, %imm_val(%IMM.TRUE))
   4805     :.end
   4806     %ret
   4807 .endscope
   4808 
   4809 :prim_not_entry
   4810 .scope
   4811     %car(t0, a0)
   4812     %li(t1, %imm_val(%IMM.FALSE))
   4813     %li(a0, %imm_val(%IMM.FALSE))
   4814     %bne(t0, t1, &.end)
   4815     %li(a0, %imm_val(%IMM.TRUE))
   4816     :.end
   4817     %ret
   4818 .endscope
   4819 
   4820 :prim_eqq_entry
   4821 .scope
   4822     %car(t0, a0)
   4823     %cdr(t1, a0)
   4824     %car(t1, t1)
   4825     %li(a0, %imm_val(%IMM.FALSE))
   4826     %bne(t0, t1, &.end)
   4827     %li(a0, %imm_val(%IMM.TRUE))
   4828     :.end
   4829     %ret
   4830 .endscope
   4831 
   4832 # checked_mul_raw(x=a0, y=a1) -> raw exact result. Inputs are already in
   4833 # the tagged-fixnum range. Sign-quadrant bounds checks happen before the
   4834 # machine multiply, so host-word wraparound is never used as a Scheme result.
   4835 :checked_mul_raw
   4836 .scope
   4837     %beqz(a0, &.zero)
   4838     %beqz(a1, &.zero)
   4839 
   4840     # max/min derive from the target word's three tag bits.
   4841     %li(t0, -1)
   4842     %shri(t0, t0, 4)            ; max
   4843     %addi(t1, t0, 1)
   4844     %li(t2, 0)
   4845     %sub(t1, t2, t1)            ; min
   4846 
   4847     %bltz(a0, &.x_negative)
   4848     %bltz(a1, &.pos_neg)
   4849     # x > 0, y > 0: x <= max / y.
   4850     %div(t2, t0, a1)
   4851     %blt(t2, a0, &.overflow)
   4852     %b(&.multiply)
   4853 
   4854     :.pos_neg
   4855     # x > 0, y < 0: y >= min / x.
   4856     %div(t2, t1, a0)
   4857     %blt(a1, t2, &.overflow)
   4858     %b(&.multiply)
   4859 
   4860     :.x_negative
   4861     %bltz(a1, &.neg_neg)
   4862     # x < 0, y > 0: x >= min / y.
   4863     %div(t2, t1, a1)
   4864     %blt(a0, t2, &.overflow)
   4865     %b(&.multiply)
   4866 
   4867     :.neg_neg
   4868     # x < 0, y < 0: x >= max / y (both sides are negative).
   4869     %div(t2, t0, a1)
   4870     %blt(a0, t2, &.overflow)
   4871 
   4872     :.multiply
   4873     %mul(a0, a0, a1)
   4874     %ret
   4875 
   4876     :.zero
   4877     %li(a0, 0)
   4878     %ret
   4879 
   4880     :.overflow
   4881     %die(msg_integer_overflow)
   4882 .endscope
   4883 
   4884 # Variadic arithmetic. (+ ...) folds with identity 0; (* ...) folds with
   4885 # identity 1; (- x) is unary negate, (- x y z ...) folds left. Every
   4886 # intermediate result is checked against the tagged exact-integer range.
   4887 
   4888 :prim_plus_entry
   4889 .scope
   4890     %li(t0, 0)              ; raw accumulator
   4891     :.loop
   4892         %if_nil(t1, a0, &.done)
   4893         %car(t1, a0)
   4894         %untag_fix(t1, t1)
   4895         %add(t0, t0, t1)
   4896         %mkfix(t2, t0)
   4897         %untag_fix(a1, t2)
   4898         %bne(t0, a1, &.overflow)
   4899         %cdr(a0, a0)
   4900         %b(&.loop)
   4901     :.done
   4902     %mkfix(a0, t0)
   4903     %ret
   4904     :.overflow
   4905     %die(msg_integer_overflow)
   4906 .endscope
   4907 
   4908 # (- x) -> -x; (- x y ...) -> x - y - ... .  (-) is undefined behavior
   4909 # per the primitive-failure policy.
   4910 :prim_minus_entry
   4911 .scope
   4912     %if_nil(t0, a0, &.arity)
   4913     %car(t0, a0)
   4914     %untag_fix(t0, t0)      ; raw seed
   4915     %cdr(a0, a0)
   4916     %if_nil(t1, a0, &.neg)
   4917     :.loop
   4918         %if_nil(t1, a0, &.done)
   4919         %car(t1, a0)
   4920         %untag_fix(t1, t1)
   4921         %sub(t0, t0, t1)
   4922         %mkfix(t2, t0)
   4923         %untag_fix(a1, t2)
   4924         %bne(t0, a1, &.overflow)
   4925         %cdr(a0, a0)
   4926         %b(&.loop)
   4927     :.neg
   4928     %li(t1, 0)              ; unary: 0 - seed
   4929     %sub(t0, t1, t0)
   4930     %mkfix(t2, t0)
   4931     %untag_fix(a1, t2)
   4932     %bne(t0, a1, &.overflow)
   4933     :.done
   4934     %mkfix(a0, t0)
   4935     %ret
   4936     :.overflow
   4937     %die(msg_integer_overflow)
   4938     :.arity
   4939     %die(msg_arity)
   4940 .endscope
   4941 
   4942 # Multiply uses the checked raw helper because a full-width product
   4943 # can overflow the host word before a post-hoc tag check could observe it.
   4944 %fn2(prim_mult_entry, {args acc}, {
   4945     %stl(a0, args)
   4946     %li(t0, 1)
   4947     %stl(t0, acc)
   4948     :.loop
   4949         %ldl(t0, args)
   4950         %if_nil(t1, t0, &.done)
   4951         %car(t1, t0)
   4952         %untag_fix(t1, t1)
   4953         %ldl(a0, acc)
   4954         %mov(a1, t1)
   4955         %call(&checked_mul_raw)
   4956         %stl(a0, acc)
   4957         %advance_walk(args)
   4958         %b(&.loop)
   4959     :.done
   4960     %ldl(a0, acc)
   4961     %mkfix(a0, a0)
   4962 })
   4963 
   4964 # Variadic chained comparisons: (op a b c ...) ⇔ (a op b) ∧ (b op c) ∧ ...
   4965 # Walks the tail with a single live `prev` register; a0 is reused as the
   4966 # args cursor and finally as the result. <2 args is undefined behavior.
   4967 :prim_eq_entry
   4968 .scope
   4969     %if_nil(t0, a0, &.arity)
   4970     %car(t0, a0)            ; prev = first
   4971     %cdr(a0, a0)
   4972     %if_nil(t1, a0, &.arity)
   4973     :.loop
   4974         %if_nil(t1, a0, &.true)
   4975         %car(t1, a0)            ; curr
   4976         %bne(t0, t1, &.false)
   4977         %mov(t0, t1)
   4978         %cdr(a0, a0)
   4979         %b(&.loop)
   4980     :.true
   4981     %li(a0, %imm_val(%IMM.TRUE))
   4982     %ret
   4983     :.false
   4984     %li(a0, %imm_val(%IMM.FALSE))
   4985     %ret
   4986     :.arity
   4987     %die(msg_arity)
   4988 .endscope
   4989 
   4990 :prim_lt_entry
   4991 .scope
   4992     %if_nil(t0, a0, &.arity)
   4993     %car(t0, a0)
   4994     %cdr(a0, a0)
   4995     %if_nil(t1, a0, &.arity)
   4996     :.loop
   4997         %if_nil(t1, a0, &.true)
   4998         %car(t1, a0)
   4999         %blt(t0, t1, &.ok)     ; prev < curr -> continue
   5000         %li(a0, %imm_val(%IMM.FALSE))
   5001         %ret
   5002         :.ok
   5003         %mov(t0, t1)
   5004         %cdr(a0, a0)
   5005         %b(&.loop)
   5006     :.true
   5007     %li(a0, %imm_val(%IMM.TRUE))
   5008     %ret
   5009     :.arity
   5010     %die(msg_arity)
   5011 .endscope
   5012 
   5013 :prim_gt_entry
   5014 .scope
   5015     %if_nil(t0, a0, &.arity)
   5016     %car(t0, a0)
   5017     %cdr(a0, a0)
   5018     %if_nil(t1, a0, &.arity)
   5019     :.loop
   5020         %if_nil(t1, a0, &.true)
   5021         %car(t1, a0)
   5022         %blt(t1, t0, &.ok)     ; curr < prev <=> prev > curr -> continue
   5023         %li(a0, %imm_val(%IMM.FALSE))
   5024         %ret
   5025         :.ok
   5026         %mov(t0, t1)
   5027         %cdr(a0, a0)
   5028         %b(&.loop)
   5029     :.true
   5030     %li(a0, %imm_val(%IMM.TRUE))
   5031     %ret
   5032     :.arity
   5033     %die(msg_arity)
   5034 .endscope
   5035 
   5036 # (quotient x y) -- truncating integer division with explicit zero and
   5037 # representability checks.
   5038 :prim_quotient_entry
   5039 .scope
   5040     %args2(t0, t1, a0)
   5041     %untag_fix(t0, t0)
   5042     %untag_fix(t1, t1)
   5043     %beqz(t1, &.zero)
   5044     # Avoid the architecture-specific signed-divide overflow case before
   5045     # issuing the instruction (x86 traps for min / -1).
   5046     %li(t2, -1)
   5047     %bne(t1, t2, &.divide)
   5048     %li(a0, -1)
   5049     %shri(a0, a0, 4)
   5050     %addi(a0, a0, 1)
   5051     %li(a1, 0)
   5052     %sub(a0, a1, a0)           ; tagged-fixnum minimum, raw
   5053     %beq(t0, a0, &.overflow)
   5054     :.divide
   5055     %div(t0, t0, t1)
   5056     %mkfix(a0, t0)
   5057     %untag_fix(t1, a0)
   5058     %bne(t0, t1, &.overflow)
   5059     %ret
   5060     :.zero
   5061     %die(msg_divide_zero)
   5062     :.overflow
   5063     %die(msg_integer_overflow)
   5064 .endscope
   5065 
   5066 # (remainder x y) -- truncating remainder, sign of dividend. rem(tagged,
   5067 # tagged) = 8 * (real_x rem real_y), already in tagged form.
   5068 :prim_remainder_entry
   5069 .scope
   5070     %args2(t0, t1, a0)
   5071     %untag_fix(t0, t0)
   5072     %untag_fix(t1, t1)
   5073     %beqz(t1, &.zero)
   5074     # min % -1 is zero, but the underlying x86 divide instruction traps;
   5075     # answer it directly on every target.
   5076     %li(t2, -1)
   5077     %beq(t1, t2, &.result_zero)
   5078     %rem(a0, t0, t1)
   5079     %mkfix(a0, a0)
   5080     %ret
   5081     :.result_zero
   5082     %li(a0, 0)
   5083     %ret
   5084     :.zero
   5085     %die(msg_divide_zero)
   5086 .endscope
   5087 
   5088 # Variadic bitwise folds. Tagged fixnums have low 3 bits = 0, so AND/OR/
   5089 # XOR with another tagged fixnum preserves the tag in the accumulator.
   5090 # Identities: bit-and -> -1 (tagged -8), bit-or -> 0, bit-xor -> 0.
   5091 :prim_bit_and_entry
   5092 .scope
   5093     %li(t0, -8)             ; tagged -1; AND-identity preserves the tag
   5094     :.loop
   5095         %if_nil(t1, a0, &.done)
   5096         %car(t1, a0)
   5097         %and(t0, t0, t1)
   5098         %cdr(a0, a0)
   5099         %b(&.loop)
   5100     :.done
   5101     %mov(a0, t0)
   5102     %ret
   5103 .endscope
   5104 
   5105 :prim_bit_or_entry
   5106 .scope
   5107     %li(t0, 0)
   5108     :.loop
   5109         %if_nil(t1, a0, &.done)
   5110         %car(t1, a0)
   5111         %or(t0, t0, t1)
   5112         %cdr(a0, a0)
   5113         %b(&.loop)
   5114     :.done
   5115     %mov(a0, t0)
   5116     %ret
   5117 .endscope
   5118 
   5119 :prim_bit_xor_entry
   5120 .scope
   5121     %li(t0, 0)
   5122     :.loop
   5123         %if_nil(t1, a0, &.done)
   5124         %car(t1, a0)
   5125         %xor(t0, t0, t1)
   5126         %cdr(a0, a0)
   5127         %b(&.loop)
   5128     :.done
   5129     %mov(a0, t0)
   5130     %ret
   5131 .endscope
   5132 
   5133 # (bit-not n) -- bitwise complement. Untag, XOR with -1 (= ~n), retag.
   5134 # Can't XOR the tagged value directly: that would flip the low 3 tag bits.
   5135 :prim_bit_not_entry
   5136     %car(t0, a0)
   5137     %untag_fix(t0, t0)
   5138     %li(t1, -1)
   5139     %xor(t0, t0, t1)
   5140     %mkfix(a0, t0)
   5141     %ret
   5142 
   5143 # (arithmetic-shift n k): k > 0 means left shift; k < 0 means arith right.
   5144 # Untag both, branch on sign of k, retag.
   5145 :prim_arith_shift_entry
   5146 .scope
   5147     %car(t0, a0)
   5148     %cdr(t1, a0)
   5149     %car(t1, t1)
   5150     %untag_fix(t0, t0)
   5151     %untag_fix(t1, t1)
   5152     %bltz(t1, &.right)
   5153     %beqz(t0, &.zero)
   5154     %li(t2, (- %p1_word_bits 3))
   5155     %bltu(t1, t2, &.left_do)
   5156     %b(&.overflow)
   5157     :.left_do
   5158     %shl(t2, t0, t1)
   5159     %mkfix(a0, t2)
   5160     %untag_fix(a1, a0)
   5161     %bne(t2, a1, &.overflow)
   5162     %ret
   5163     :.right
   5164     %li(t2, 0)
   5165     %sub(t1, t2, t1)
   5166     %li(t2, %p1_word_bits)
   5167     %bltu(t1, t2, &.right_do)
   5168     %bltz(t0, &.minus_one)
   5169     :.zero
   5170     %li(a0, 0)
   5171     %ret
   5172     :.minus_one
   5173     %li(a0, -8)
   5174     %ret
   5175     :.right_do
   5176     %sar(a0, t0, t1)
   5177     %mkfix(a0, a0)
   5178     %ret
   5179     :.overflow
   5180     %die(msg_integer_overflow)
   5181 .endscope
   5182 
   5183 # Bytevectors are 24-byte HEAP-tagged wrappers pointing at a separately
   5184 # allocated data buffer; this gives them dynamic-array semantics — capacity
   5185 # can grow in place by reallocating just the data buffer (no need to find
   5186 # and patch every reference to the wrapper).
   5187 #
   5188 #   word 0  ::  (length << 8) | HDR.BV       ; length = hdr >> 8
   5189 #   word 1  ::  data_ptr (raw heap address)
   5190 #   word 2  ::  capacity in bytes
   5191 #
   5192 # Tagged-pointer offsets into the wrapper:
   5193 #   hdr      = ld(bv, -3)
   5194 #   data_ptr = ld(bv,  5)
   5195 #   capacity = ld(bv, 13)
   5196 #
   5197 # bv_capacity_for(n) returns the smallest power-of-two ≥ max(n, 16); bv_grow
   5198 # then doubles by repeatedly shifting until cap ≥ requested. Bytevectors
   5199 # are raw u8[] and need no headroom for a NUL terminator -- callers that
   5200 # build "strings" use the str_* writers, which reserve cap > len AND
   5201 # explicitly zero data[len] (reused GC blocks are not assumed zeroed).
   5202 
   5203 # alloc_bytes(size=a0) -> managed RAW payload address (a0).
   5204 :alloc_bytes
   5205     %li(a1, %GCKIND.RAW)
   5206     %b(&gc_alloc)
   5207 
   5208 # bv_capacity_for(n=a0) -> smallest power-of-two ≥ n, minimum 16. Pure
   5209 # bytevector sizing -- no NUL slack. Callers building "strings" call
   5210 # bv_capacity_for(raw_len + 1) to reserve room for the trailing NUL.
   5211 :bv_capacity_for
   5212 .scope
   5213     %li(t0, 16)
   5214     :.loop
   5215     %bltu(t0, a0, &.shift)         ; t0 < a0: keep doubling
   5216     %mov(a0, t0)                    ; t0 >= a0: done
   5217     %ret
   5218     :.shift
   5219     %shli(t0, t0, 1)
   5220     %b(&.loop)
   5221 .endscope
   5222 
   5223 # bv_alloc(raw_len=a0) -> tagged bv (a0). Length = raw_len, capacity from
   5224 # bv_capacity_for, data buffer uninitialized. data_ptr lives in a frame
   5225 # slot because alloc_hdr's alignup clobbers t-regs.
   5226 #
   5227 # Locals:
   5228 #   raw_len
   5229 #   capacity
   5230 #   data_ptr  (raw)
   5231 %gcfn2(bv_alloc, {raw_len capacity data_ptr}, 0, 4, {
   5232     %stl(a0, raw_len)
   5233 
   5234     %call(&bv_capacity_for)
   5235     %stl(a0, capacity)
   5236     %call(&alloc_bytes)
   5237     %stl(a0, data_ptr)
   5238 
   5239     %ldl(a1, raw_len)
   5240     %shli(a1, a1, 8)        ; hdr = (raw_len << 8) | HDR.BV (BV == 0)
   5241     %li(a0, 24)
   5242     %call(&alloc_hdr)
   5243 
   5244     %ldl(t0, data_ptr)
   5245     %heap_st(t0, a0, %BV.data)
   5246     %ldl(t1, capacity)
   5247     %st(t1, a0, 13)         ; bv.cap (raw offset 16; not in BV struct)
   5248 })
   5249 
   5250 # bv_grow(bv=a0, min_cap=a1) -> bv (a0). Doubles capacity until ≥ min_cap;
   5251 # allocates a fresh data buffer, copies the live bytes (length, not
   5252 # capacity), and patches the wrapper's data_ptr/capacity slots in place.
   5253 # A no-op when current capacity already satisfies min_cap.
   5254 #
   5255 # Locals:
   5256 #   bv
   5257 #   min_cap  (input) / new_cap (during loop)
   5258 #   new_data_ptr
   5259 #   raw  length
   5260 %gcfn2(bv_grow, {bv min_cap new_data_ptr raw}, 1, 4, {
   5261     %stl(a0, bv)
   5262     %stl(a1, min_cap)
   5263 
   5264     %ld(t0, a0, 13)         ; bv.cap (raw offset 16; not in BV struct)
   5265     %bltu(t0, a1, &.need)
   5266     %ldl(a0, bv)
   5267     %gceret
   5268 
   5269     :.need
   5270     :.loop
   5271     %shli(t0, t0, 1)
   5272     %ldl(t1, min_cap)
   5273     %bltu(t0, t1, &.loop)
   5274     %stl(t0, min_cap)
   5275 
   5276     %mov(a0, t0)
   5277     %call(&alloc_bytes)
   5278     %stl(a0, new_data_ptr)
   5279 
   5280     %ldl(t0, bv)
   5281     %heap_ld(t1, t0, %BV.hdr)
   5282     %shri(t1, t1, 8)        ; raw length
   5283     %stl(t1, raw)
   5284     %ldl(a0, new_data_ptr)
   5285     %heap_ld(a1, t0, %BV.data)  ; old data ptr
   5286     %ldl(a2, raw)
   5287     %call(&memcpy)
   5288 
   5289     %ldl(t0, bv)
   5290     %ldl(t1, new_data_ptr)
   5291     %heap_st(t1, t0, %BV.data)
   5292     %ldl(t1, min_cap)
   5293     %st(t1, t0, 13)         ; bv.cap (raw offset 16; not in BV struct)
   5294     %ldl(a0, bv)
   5295 })
   5296 
   5297 # (make-bytevector len) or (make-bytevector len fill)
   5298 
   5299 %gcfn2(prim_make_bytevector_entry, {args fill wrapper}, 5, 0, {
   5300     %stl(a0, args)
   5301 
   5302     %car(t0, a0)
   5303     %tagof(t1, t0)
   5304     %bine(t1, %TAG.FIXNUM, &.bad_type, t2)
   5305 
   5306     %li(t2, 0)
   5307     %cdr(t0, a0)
   5308     %if_nil(t1, t0, &.no_fill)
   5309     %car(t0, t0)
   5310     %tagof(t1, t0)
   5311     %bine(t1, %TAG.FIXNUM, &.bad_type, t2)
   5312     %untag_fix(t2, t0)
   5313     %bltz(t2, &.bad_fill)
   5314     %li(t1, 256)
   5315     %bltu(t2, t1, &.no_fill)
   5316     %b(&.bad_fill)
   5317     :.no_fill
   5318     %stl(t2, fill)
   5319 
   5320     %ldl(a0, args)
   5321     %car_fix(a0, a0)
   5322     %bltz(a0, &.bad_len)
   5323     %call(&bv_alloc)
   5324     %stl(a0, wrapper)
   5325 
   5326     %ldl(t0, args)
   5327     %car_fix(t0, t0)        ; raw_len
   5328     %ldl(t1, fill)          ; fill
   5329     %ldl(a1, wrapper)
   5330     %heap_ld(t2, a1, %BV.data)
   5331     %li(a1, 0)
   5332 
   5333     :.fill_loop
   5334         %beq(a1, t0, &.fill_done)
   5335         %sb(t1, t2, 0)
   5336         %addi(t2, t2, 1)
   5337         %addi(a1, a1, 1)
   5338         %b(&.fill_loop)
   5339     :.fill_done
   5340 
   5341     %ldl(a0, wrapper)
   5342     %gceret
   5343 
   5344     :.bad_len
   5345     %die(msg_bv_oob)
   5346     :.bad_fill
   5347     %die(msg_bad_byte)
   5348     :.bad_type
   5349     %die(msg_type)
   5350 })
   5351 
   5352 # (make-string len) or (make-string len fill-char). Strings have an explicit
   5353 # logical length and a spare NUL after it for the boot2 pathname bridge.
   5354 %gcfn2(prim_make_string_entry, {args fill wrapper}, 5, 0, {
   5355     %stl(a0, args)
   5356 
   5357     %car(t0, a0)
   5358     %tagof(t1, t0)
   5359     %bine(t1, %TAG.FIXNUM, &.bad_type, t2)
   5360 
   5361     %li(t2, 32)                 ; implementation-selected default: space
   5362     %cdr(t0, a0)
   5363     %if_nil(t1, t0, &.no_fill)
   5364     %car(t0, t0)
   5365     %tagof(t1, t0)
   5366     %bine(t1, %TAG.CHAR, &.bad_type, t2)
   5367     %untag_char(t2, t0)
   5368     :.no_fill
   5369     %stl(t2, fill)
   5370 
   5371     %ldl(a0, args)
   5372     %car_fix(a0, a0)
   5373     %bltz(a0, &.bad_len)
   5374     %call(&string_alloc)
   5375     %stl(a0, wrapper)
   5376 
   5377     %ldl(t0, args)
   5378     %car_fix(t0, t0)
   5379     %ldl(t1, fill)
   5380     %ldl(a0, wrapper)
   5381     %heap_ld(t2, a0, %BV.data)
   5382     %li(a1, 0)
   5383     :.fill_loop
   5384     %beq(a1, t0, &.fill_done)
   5385     %sb(t1, t2, 0)
   5386     %addi(t2, t2, 1)
   5387     %addi(a1, a1, 1)
   5388     %b(&.fill_loop)
   5389     :.fill_done
   5390     %ldl(a0, wrapper)
   5391     %gceret
   5392 
   5393     :.bad_len
   5394     %die(msg_string_oob)
   5395     :.bad_type
   5396     %die(msg_type)
   5397 })
   5398 
   5399 :prim_bv_length_entry
   5400 .scope
   5401     %car(t0, a0)
   5402     %tagof(t1, t0)
   5403     %bine(t1, %TAG.HEAP, &.bad, t2)
   5404     %hdr_type(t1, t0)
   5405     %li(t2, %HDR.BV)
   5406     %beq(t1, t2, &.ok)
   5407     %li(t2, %HDR.STRING)
   5408     %bne(t1, t2, &.bad)
   5409     :.ok
   5410     %heap_ld(t1, t0, %BV.hdr)
   5411     %shri(a0, t1, 5)
   5412     %ret
   5413     :.bad
   5414     %die(msg_type)
   5415 .endscope
   5416 
   5417 # (string-length s) -- explicit logical length from the string header.
   5418 :prim_string_length_entry
   5419 .scope
   5420     %car(t0, a0)
   5421     %tagof(t1, t0)
   5422     %bine(t1, %TAG.HEAP, &.bad, t2)
   5423     %hdr_type(t1, t0)
   5424     %bine(t1, %HDR.STRING, &.bad, t2)
   5425     %heap_ld(t1, t0, %BV.hdr)
   5426     %shri(a0, t1, 5)
   5427     %ret
   5428     :.bad
   5429     %die(msg_type)
   5430 .endscope
   5431 
   5432 # (string-ref s idx) -> disjoint character.
   5433 :prim_string_ref_entry
   5434 .scope
   5435     %args2(t0, t1, a0)
   5436     %tagof(t2, t0)
   5437     %bine(t2, %TAG.HEAP, &.type, a1)
   5438     %hdr_type(t2, t0)
   5439     %bine(t2, %HDR.STRING, &.type, a1)
   5440     %tagof(t2, t1)
   5441     %bine(t2, %TAG.FIXNUM, &.type, a1)
   5442     %sari(t1, t1, 3)
   5443     %bltz(t1, &.oob)
   5444     %heap_ld(a0, t0, %BV.hdr)
   5445     %shri(a0, a0, 8)
   5446     %bltu(t1, a0, &.ok)
   5447     :.oob
   5448     %die(msg_string_oob)
   5449     :.ok
   5450     %heap_ld(t2, t0, %BV.data)
   5451     %add(t2, t2, t1)
   5452     %lb(a0, t2, 0)
   5453     %mkchar(a0, a0)
   5454     %ret
   5455     :.type
   5456     %die(msg_type)
   5457 .endscope
   5458 
   5459 # (string-set! s idx char) -> unspecified.
   5460 :prim_string_set_entry
   5461 .scope
   5462     %args3(t0, t2, t1, a0)
   5463     %tagof(a1, t0)
   5464     %bine(a1, %TAG.HEAP, &.type, a2)
   5465     %hdr_type(a1, t0)
   5466     %bine(a1, %HDR.STRING, &.type, a2)
   5467     %tagof(a1, t2)
   5468     %bine(a1, %TAG.FIXNUM, &.type, a2)
   5469     %tagof(a1, t1)
   5470     %bine(a1, %TAG.CHAR, &.type, a2)
   5471     %sari(t2, t2, 3)
   5472     %untag_char(t1, t1)
   5473     %bltz(t2, &.oob)
   5474     %heap_ld(a0, t0, %BV.hdr)
   5475     %shri(a0, a0, 8)
   5476     %bltu(t2, a0, &.ok)
   5477     :.oob
   5478     %die(msg_string_oob)
   5479     :.ok
   5480     %heap_ld(a0, t0, %BV.data)
   5481     %add(a0, a0, t2)
   5482     %sb(t1, a0, 0)
   5483     %li(a0, %imm_val(%IMM.UNSPEC))
   5484     %ret
   5485     :.type
   5486     %die(msg_type)
   5487 .endscope
   5488 
   5489 :prim_bv_u8_ref_entry
   5490 .scope
   5491     %args2(t0, t1, a0)      ; bv, tagged idx
   5492     %tagof(t2, t0)
   5493     %bine(t2, %TAG.HEAP, &.type, a1)
   5494     %hdr_type(t2, t0)
   5495     %li(a1, %HDR.BV)
   5496     %beq(t2, a1, &.bytes)
   5497     %li(a1, %HDR.STRING)
   5498     %bne(t2, a1, &.type)
   5499     :.bytes
   5500     %tagof(t2, t1)
   5501     %bine(t2, %TAG.FIXNUM, &.type, a1)
   5502     %sari(t1, t1, 3)        ; raw idx
   5503     %bltz(t1, &.oob)
   5504     %heap_ld(a0, t0, %BV.hdr)
   5505     %shri(a0, a0, 8)        ; length
   5506     %bltu(t1, a0, &.ok)
   5507     :.oob
   5508     %die(msg_bv_oob)
   5509     :.ok
   5510     %heap_ld(t2, t0, %BV.data)
   5511     %add(t2, t2, t1)
   5512     %lb(a0, t2, 0)
   5513     %mkfix(a0, a0)
   5514     %ret
   5515     :.type
   5516     %die(msg_type)
   5517 .endscope
   5518 
   5519 :prim_bv_u8_set_entry
   5520 .scope
   5521     %args3(t0, t2, t1, a0)  ; bv, idx, val
   5522     %tagof(a1, t0)
   5523     %bine(a1, %TAG.HEAP, &.type, a2)
   5524     %hdr_type(a1, t0)
   5525     %li(a2, %HDR.BV)
   5526     %beq(a1, a2, &.bytes)
   5527     %li(a2, %HDR.STRING)
   5528     %bne(a1, a2, &.type)
   5529     :.bytes
   5530     %tagof(a1, t2)
   5531     %bine(a1, %TAG.FIXNUM, &.type, a2)
   5532     %tagof(a1, t1)
   5533     %bine(a1, %TAG.FIXNUM, &.type, a2)
   5534     %sari(t2, t2, 3)        ; raw idx
   5535     %sari(t1, t1, 3)        ; raw val
   5536     %bltz(t1, &.bad_byte)
   5537     %li(a1, 256)
   5538     %bltu(t1, a1, &.value_ok)
   5539     %b(&.bad_byte)
   5540     :.value_ok
   5541     %bltz(t2, &.oob)
   5542     %heap_ld(a0, t0, %BV.hdr)
   5543     %shri(a0, a0, 8)        ; length
   5544     %bltu(t2, a0, &.ok)
   5545     :.oob
   5546     %die(msg_bv_oob)
   5547     :.ok
   5548     %heap_ld(a0, t0, %BV.data)
   5549     %add(a0, a0, t2)
   5550     %sb(t1, a0, 0)
   5551     %li(a0, %imm_val(%IMM.UNSPEC))
   5552     %ret
   5553     :.bad_byte
   5554     %die(msg_bad_byte)
   5555     :.type
   5556     %die(msg_type)
   5557 .endscope
   5558 
   5559 # (bytevector-copy src start end) -> fresh bv of length end-start.
   5560 # Bounds: 0 <= start <= end <= src.length.
   5561 #
   5562 # Locals:
   5563 #   args
   5564 #   src  tagged
   5565 #   wrapper  (saved after bv_alloc)
   5566 %gcfn2(prim_bv_copy_entry, {args src wrapper}, 7, 0, {
   5567     %stl(a0, args)
   5568 
   5569     %args3(t0, t2, t1, a0)  ; src, start, end
   5570     %stl(t0, src)
   5571     %sari(t2, t2, 3)        ; raw start
   5572     %sari(t1, t1, 3)        ; raw end
   5573 
   5574     # Bounds: start >= 0; end >= start (signed catches negative end since
   5575     # start is now non-negative); src.length >= end.
   5576     %bltz(t2, &.oob)
   5577     %blt(t1, t2, &.oob)
   5578     %heap_ld(a0, t0, %BV.hdr)
   5579     %shri(a0, a0, 8)        ; src.length
   5580     %blt(a0, t1, &.oob)
   5581 
   5582     %sub(a0, t1, t2)        ; count
   5583     %call(&bv_alloc)
   5584     %stl(a0, wrapper)
   5585 
   5586     # Recompute src ptr at start; dst ptr at 0; count from new bv's hdr.
   5587     %ldl(t0, args)
   5588     %cdr(t0, t0)
   5589     %car_fix(t0, t0)        ; raw start
   5590     %ldl(t1, src)
   5591     %heap_ld(t2, t1, %BV.data)
   5592     %add(t2, t2, t0)        ; src ptr
   5593     %ldl(a3, wrapper)
   5594     %heap_ld(a2, a3, %BV.data)  ; dst ptr
   5595     %heap_ld(a1, a3, %BV.hdr)
   5596     %shri(a1, a1, 8)        ; count
   5597 
   5598     :.copy_loop
   5599     %beqz(a1, &.copy_done)
   5600     %lb(t0, t2, 0)
   5601     %sb(t0, a2, 0)
   5602     %addi(t2, t2, 1)
   5603     %addi(a2, a2, 1)
   5604     %addi(a1, a1, -1)
   5605     %b(&.copy_loop)
   5606 
   5607     :.copy_done
   5608     %ldl(a0, wrapper)
   5609     %gceret
   5610 
   5611     :.oob
   5612     %die(msg_bv_oob)
   5613 })
   5614 
   5615 # (bytevector-copy! dst dst-start src src-start src-end). Bounds:
   5616 # 0 <= src-start <= src-end <= src.length and
   5617 # 0 <= dst-start && dst-start + (src-end-src-start) <= dst.length.
   5618 #
   5619 # Locals:
   5620 #   dst  -start (raw)
   5621 #   dst_start
   5622 #   src  -end (raw)
   5623 #   src_start
   5624 #   src_end
   5625 %fn2(prim_bv_copy_bang_entry, {dst dst_start src src_start src_end}, {
   5626     %car(t0, a0)
   5627     %stl(t0, dst)              ; dst
   5628     %cdr(a0, a0)
   5629     %car(t0, a0)
   5630     %sari(t0, t0, 3)
   5631     %stl(t0, dst_start)              ; dst-start
   5632     %cdr(a0, a0)
   5633     %car(t0, a0)
   5634     %stl(t0, src)             ; src
   5635     %cdr(a0, a0)
   5636     %car(t0, a0)
   5637     %sari(t0, t0, 3)
   5638     %stl(t0, src_start)             ; src-start
   5639     %cdr(a0, a0)
   5640     %car(t0, a0)
   5641     %sari(t0, t0, 3)
   5642     %stl(t0, src_end)             ; src-end
   5643 
   5644     # src-start >= 0
   5645     %ldl(t0, src_start)
   5646     %bltz(t0, &.oob)
   5647     # src-end >= src-start (signed catches negative src-end)
   5648     %ldl(t1, src_end)
   5649     %blt(t1, t0, &.oob)
   5650     # src-end <= src.length
   5651     %ldl(t2, src)
   5652     %heap_ld(a0, t2, %BV.hdr)
   5653     %shri(a0, a0, 8)
   5654     %blt(a0, t1, &.oob)
   5655     # dst-start >= 0
   5656     %ldl(t2, dst_start)
   5657     %bltz(t2, &.oob)
   5658     # dst-start + count <= dst.length
   5659     %sub(a0, t1, t0)            ; count = src-end - src-start
   5660     %add(a0, a0, t2)            ; dst-start + count
   5661     %ldl(t1, dst)
   5662     %heap_ld(t2, t1, %BV.hdr)
   5663     %shri(t2, t2, 8)
   5664     %blt(t2, a0, &.oob)
   5665 
   5666     # Set up copy. dst ptr = dst.data + dst-start; src ptr = src.data +
   5667     # src-start; count = src-end - src-start.
   5668     %ldl(t0, dst)
   5669     %heap_ld(t0, t0, %BV.data)
   5670     %ldl(a1, dst_start)
   5671     %add(t0, t0, a1)            ; dst ptr
   5672     %ldl(a1, src)
   5673     %heap_ld(a1, a1, %BV.data)
   5674     %ldl(a2, src_start)
   5675     %add(a1, a1, a2)            ; src ptr
   5676     %ldl(a3, src_end)
   5677     %sub(a3, a3, a2)            ; count
   5678 
   5679     # memmove semantics: when destination starts inside the source range,
   5680     # copy from the end so an overlapping in-place move cannot trample bytes
   5681     # that have not been read yet.
   5682     %beqz(a3, &.done)
   5683     %bltu(a1, t0, &.dst_after_src)
   5684     %b(&.forward_loop)
   5685     :.dst_after_src
   5686     %add(t1, a1, a3)            ; source end
   5687     %bltu(t0, t1, &.backward_setup)
   5688 
   5689     :.forward_loop
   5690         %beqz(a3, &.done)
   5691         %lb(t1, a1, 0)
   5692         %sb(t1, t0, 0)
   5693         %addi(t0, t0, 1)
   5694         %addi(a1, a1, 1)
   5695         %addi(a3, a3, -1)
   5696     %b(&.forward_loop)
   5697 
   5698     :.backward_setup
   5699     %add(t0, t0, a3)
   5700     %add(a1, a1, a3)
   5701     :.backward_loop
   5702         %beqz(a3, &.done)
   5703         %addi(t0, t0, -1)
   5704         %addi(a1, a1, -1)
   5705         %lb(t1, a1, 0)
   5706         %sb(t1, t0, 0)
   5707         %addi(a3, a3, -1)
   5708     %b(&.backward_loop)
   5709 
   5710     :.done
   5711     %li(a0, %imm_val(%IMM.UNSPEC))
   5712     %eret
   5713 
   5714     :.oob
   5715     %die(msg_bv_oob)
   5716 })
   5717 
   5718 # bv_equal_check(a=a0, b=a1) -> a0 (IMM.TRUE / IMM.FALSE). Leaf. Both
   5719 # arguments are assumed to be HEAP-tagged HDR.BV values; callers do the
   5720 # type check (either bytevector=?'s prim entry or equal_recurse's BV
   5721 # branch). Compares lengths first, then walks bytes; %lb is zero-extending
   5722 # on every backend, so a single %bne is enough for the byte test.
   5723 :bv_equal_check
   5724 .scope
   5725     %heap_ld(t0, a0, %BV.hdr)
   5726     %shri(t0, t0, 8)            ; len_a
   5727     %heap_ld(t1, a1, %BV.hdr)
   5728     %shri(t1, t1, 8)            ; len_b
   5729     %bne(t0, t1, &.false)
   5730 
   5731     %heap_ld(a2, a0, %BV.data)
   5732     %heap_ld(a3, a1, %BV.data)
   5733 
   5734     :.loop
   5735         %beqz(t0, &.true)
   5736         %lb(t1, a2, 0)
   5737         %lb(t2, a3, 0)
   5738         %bne(t1, t2, &.false)
   5739         %addi(a2, a2, 1)
   5740         %addi(a3, a3, 1)
   5741         %addi(t0, t0, -1)
   5742     %b(&.loop)
   5743 
   5744     :.true
   5745     %li(a0, %imm_val(%IMM.TRUE))
   5746     %ret
   5747 
   5748     :.false
   5749     %li(a0, %imm_val(%IMM.FALSE))
   5750     %ret
   5751 .endscope
   5752 
   5753 # (bytevector=? a b) / (bytes=? a b) -- byte-oriented bridge equality.
   5754 # Each operand may be a string or bytevector; other inputs return #f.
   5755 :prim_bytevector_eq_entry
   5756 .scope
   5757     %args2(t0, t1, a0)
   5758     %tagof(t2, t0)
   5759     %li(a0, %TAG.HEAP)
   5760     %bne(t2, a0, &.false)
   5761     %tagof(t2, t1)
   5762     %bne(t2, a0, &.false)
   5763     %hdr_type(t2, t0)
   5764     %li(a0, %HDR.BV)
   5765     %beq(t2, a0, &.a_bytes)
   5766     %li(a0, %HDR.STRING)
   5767     %bne(t2, a0, &.false)
   5768     :.a_bytes
   5769     %hdr_type(t2, t1)
   5770     %li(a0, %HDR.BV)
   5771     %beq(t2, a0, &.both_bytes)
   5772     %li(a0, %HDR.STRING)
   5773     %bne(t2, a0, &.false)
   5774     :.both_bytes
   5775     %mov(a0, t0)
   5776     %mov(a1, t1)
   5777     %b(&bv_equal_check)
   5778     :.false
   5779     %li(a0, %imm_val(%IMM.FALSE))
   5780     %ret
   5781 .endscope
   5782 
   5783 # =========================================================================
   5784 # Private hash tables -- open addressing for compiler-sized maps
   5785 # =========================================================================
   5786 #
   5787 # These primitives deliberately use `%`-prefixed boot2 names rather than an
   5788 # R7RS spelling. Keys compare by byte content when both are strings or
   5789 # bytevectors (the same bridge rule as bytes=?); every other key compares by
   5790 # identity. HASH is a stable wrapper around a replaceable HASHDATA object, so
   5791 # growing a table never invalidates Scheme references to the table itself.
   5792 #
   5793 # HASHDATA uses power-of-two capacity and linear probing. Slot hash 0 is empty,
   5794 # 1 is a tombstone, and live hashes have bit 1 forced on (therefore >= 2).
   5795 # `used` counts live+tombstone slots, which lets insertion rebuild a table
   5796 # before tombstones can consume every terminating empty slot.
   5797 
   5798 # hash_key_hash(key=a0) -> raw machine-word hash (a0). Leaf. Byte-sequence
   5799 # hashing is content-only, so an equal string and bytevector land in the same
   5800 # probe chain. The xorshift fallback spreads sequential symbol/fixnum keys.
   5801 :hash_key_hash
   5802 .scope
   5803     %tagof(t0, a0)
   5804     %li(t1, %TAG.HEAP)
   5805     %bne(t0, t1, &.identity)
   5806     %hdr_type(t0, a0)
   5807     %li(t1, %HDR.BV)
   5808     %beq(t0, t1, &.bytes)
   5809     %li(t1, %HDR.STRING)
   5810     %bne(t0, t1, &.identity)
   5811 
   5812     :.bytes
   5813     %heap_ld(t0, a0, %BV.hdr)
   5814     %shri(t0, t0, 8)            ; remaining length
   5815     %heap_ld(t1, a0, %BV.data)
   5816     %li(a1, 5381)
   5817     :.bytes_loop
   5818     %beqz(t0, &.bytes_done)
   5819     %lb(t2, t1, 0)
   5820     %shli(a2, a1, 5)            ; hash * 33
   5821     %add(a1, a2, a1)
   5822     %xor(a1, a1, t2)
   5823     %addi(t1, t1, 1)
   5824     %addi(t0, t0, -1)
   5825     %b(&.bytes_loop)
   5826     :.bytes_done
   5827     %mov(a0, a1)
   5828     %ret
   5829 
   5830     :.identity
   5831     %shri(t0, a0, 3)
   5832     %shri(t1, t0, 17)
   5833     %xor(t0, t0, t1)
   5834     %shli(t1, t0, 13)
   5835     %xor(t0, t0, t1)
   5836     %shri(t1, t0, 29)
   5837     %xor(a0, t0, t1)
   5838     %ret
   5839 .endscope
   5840 
   5841 # hash_key_equal(a=a0, b=a1) -> Scheme boolean. Leaf. Identity is the
   5842 # overwhelmingly common path; the only structural case tails into bytes=?'s
   5843 # allocation-free worker.
   5844 :hash_key_equal
   5845 .scope
   5846     %beq(a0, a1, &.true)
   5847     %tagof(t0, a0)
   5848     %li(t1, %TAG.HEAP)
   5849     %bne(t0, t1, &.false)
   5850     %tagof(t0, a1)
   5851     %bne(t0, t1, &.false)
   5852     %hdr_type(t0, a0)
   5853     %li(t2, %HDR.BV)
   5854     %beq(t0, t2, &.a_bytes)
   5855     %li(t2, %HDR.STRING)
   5856     %bne(t0, t2, &.false)
   5857     :.a_bytes
   5858     %hdr_type(t0, a1)
   5859     %li(t2, %HDR.BV)
   5860     %beq(t0, t2, &.both_bytes)
   5861     %li(t2, %HDR.STRING)
   5862     %bne(t0, t2, &.false)
   5863     :.both_bytes
   5864     %b(&bv_equal_check)
   5865     :.true
   5866     %li(a0, %imm_val(%IMM.TRUE))
   5867     %ret
   5868     :.false
   5869     %li(a0, %imm_val(%IMM.FALSE))
   5870     %ret
   5871 .endscope
   5872 
   5873 # hash_data_alloc(capacity=a0 raw) -> HASHDATA object. Capacity is already a
   5874 # power of two. Traced allocation clears every slot hash to the empty marker.
   5875 %fn2(hash_data_alloc, {capacity}, {
   5876     %stl(a0, capacity)
   5877     %shli(t0, a0, 4)
   5878     %shli(t1, a0, 3)
   5879     %add(a0, t0, t1)            ; capacity * 24
   5880     %addi(a0, a0, %HASHDATA.SIZE)
   5881     %ldl(t0, capacity)
   5882     %shli(t0, t0, 8)
   5883     %ori(a1, t0, %HDR.HASHDATA)
   5884     %call(&alloc_hdr)
   5885     %li(t0, 0)
   5886     %heap_st(t0, a0, %HASHDATA.count)
   5887     %heap_st(t0, a0, %HASHDATA.used)
   5888 })
   5889 
   5890 # hash_make(requested_capacity=a0 raw) -> HASH wrapper. Round up to a
   5891 # power-of-two with a small floor so every table retains an empty probe slot.
   5892 %gcfn2(hash_make, {capacity table}, 2, 0, {
   5893     %li(t0, 8)
   5894     %stl(t0, capacity)
   5895     :.round
   5896     %ldl(t0, capacity)
   5897     %bltu(t0, a0, &.grow)
   5898     %b(&.rounded)
   5899     :.grow
   5900     %shli(t0, t0, 1)
   5901     %stl(t0, capacity)
   5902     %b(&.round)
   5903 
   5904     :.rounded
   5905     %li(a0, %HASH.SIZE)
   5906     %li(a1, %HDR.HASH)
   5907     %call(&alloc_hdr)
   5908     %stl(a0, table)
   5909     %ldl(a0, capacity)
   5910     %call(&hash_data_alloc)
   5911     %ldl(t0, table)
   5912     %heap_st(a0, t0, %HASH.data)
   5913     %mov(a0, t0)
   5914     %gceret
   5915 })
   5916 
   5917 # Insert a known-unique live entry into fresh HASHDATA. No equality checks or
   5918 # allocations; used and count both grow by one. Used only while rehashing.
   5919 %fn2(hash_data_insert_known, {data hash key value idx slot}, {
   5920     %stl(a0, data)
   5921     %stl(a1, hash)
   5922     %stl(a2, key)
   5923     %stl(a3, value)
   5924     %heap_ld(t0, a0, %HASHDATA.hdr)
   5925     %shri(t0, t0, 8)
   5926     %addi(t0, t0, -1)
   5927     %and(t0, t0, a1)
   5928     %stl(t0, idx)
   5929 
   5930     :.probe
   5931     %ldl(t0, data)
   5932     %ldl(t1, idx)
   5933     %hash_slot(t2, t0, t1, a0)
   5934     %stl(t2, slot)
   5935     %ld(a0, t2, 0)
   5936     %beqz(a0, &.insert)
   5937     %heap_ld(t0, t0, %HASHDATA.hdr)
   5938     %shri(t0, t0, 8)
   5939     %addi(t0, t0, -1)
   5940     %addi(t1, t1, 1)
   5941     %and(t1, t1, t0)
   5942     %stl(t1, idx)
   5943     %b(&.probe)
   5944 
   5945     :.insert
   5946     %ldl(t0, slot)
   5947     %ldl(t1, hash)
   5948     %st(t1, t0, 0)
   5949     %ldl(t1, key)
   5950     %st(t1, t0, 8)
   5951     %ldl(t1, value)
   5952     %st(t1, t0, 16)
   5953     %ldl(t0, data)
   5954     %heap_ld(t1, t0, %HASHDATA.count)
   5955     %addi(t1, t1, 1)
   5956     %heap_st(t1, t0, %HASHDATA.count)
   5957     %heap_ld(t1, t0, %HASHDATA.used)
   5958     %addi(t1, t1, 1)
   5959     %heap_st(t1, t0, %HASHDATA.used)
   5960 })
   5961 
   5962 # Rebuild a table and discard tombstones. Double when the live entries need
   5963 # the space; otherwise retain the capacity so repeated delete/insert churn
   5964 # cannot grow a mostly-empty table without bound. The wrapper continues
   5965 # pointing at the old data until the new allocation is rooted, so collection
   5966 # is safe at every construction point.
   5967 %gcfn2(hash_resize, {table old_data new_data capacity idx}, 7, 0, {
   5968     %stl(a0, table)
   5969     %heap_ld(t0, a0, %HASH.data)
   5970     %stl(t0, old_data)
   5971     %heap_ld(t1, t0, %HASHDATA.hdr)
   5972     %shri(t1, t1, 8)
   5973     %stl(t1, capacity)
   5974     %heap_ld(t0, t0, %HASHDATA.count)
   5975     %addi(t0, t0, 1)
   5976     %shli(t0, t0, 2)            ; (live + incoming) * 4
   5977     %shli(t2, t1, 1)
   5978     %add(t2, t2, t1)            ; capacity * 3
   5979     %bltu(t0, t2, &.alloc_same)
   5980     %shli(a0, t1, 1)
   5981     %b(&.alloc)
   5982     :.alloc_same
   5983     %mov(a0, t1)
   5984     :.alloc
   5985     %call(&hash_data_alloc)
   5986     %stl(a0, new_data)
   5987     %li(t0, 0)
   5988     %stl(t0, idx)
   5989 
   5990     :.loop
   5991     %ldl(t0, idx)
   5992     %ldl(t1, capacity)
   5993     %beq(t0, t1, &.publish)
   5994     %ldl(t1, old_data)
   5995     %hash_slot(t2, t1, t0, a0)
   5996     %ld(a1, t2, 0)
   5997     %li(a0, 2)
   5998     %bltu(a1, a0, &.next)
   5999     %ld(a2, t2, 8)
   6000     %ld(a3, t2, 16)
   6001     %ldl(a0, new_data)
   6002     %call(&hash_data_insert_known)
   6003 
   6004     :.next
   6005     %ldl(t0, idx)
   6006     %addi(t0, t0, 1)
   6007     %stl(t0, idx)
   6008     %b(&.loop)
   6009 
   6010     :.publish
   6011     %ldl(a0, table)
   6012     %ldl(t0, new_data)
   6013     %heap_st(t0, a0, %HASH.data)
   6014     %gceret
   6015 })
   6016 
   6017 # hash_ref(table=a0, key=a1) -> value or #f. Probe count is bounded by
   6018 # capacity as a defensive guarantee even if a corrupted table lacks empties.
   6019 %fn2(hash_ref, {table key data hash idx remaining slot}, {
   6020     %stl(a0, table)
   6021     %stl(a1, key)
   6022     %mov(a0, a1)
   6023     %call(&hash_key_hash)
   6024     %ori(a0, a0, 2)
   6025     %stl(a0, hash)
   6026     %ldl(t0, table)
   6027     %heap_ld(t0, t0, %HASH.data)
   6028     %stl(t0, data)
   6029     %heap_ld(t1, t0, %HASHDATA.hdr)
   6030     %shri(t1, t1, 8)
   6031     %stl(t1, remaining)
   6032     %addi(t1, t1, -1)
   6033     %and(t1, t1, a0)
   6034     %stl(t1, idx)
   6035 
   6036     :.probe
   6037     %ldl(t0, remaining)
   6038     %beqz(t0, &.miss)
   6039     %ldl(t0, data)
   6040     %ldl(t1, idx)
   6041     %hash_slot(t2, t0, t1, a0)
   6042     %stl(t2, slot)
   6043     %ld(t2, t2, 0)
   6044     %beqz(t2, &.miss)
   6045     %ldl(a0, hash)
   6046     %bne(t2, a0, &.next)
   6047     %ldl(t2, slot)
   6048     %ld(a0, t2, 8)
   6049     %ldl(a1, key)
   6050     %call(&hash_key_equal)
   6051     %bieq(a0, %imm_val(%IMM.FALSE), &.next, t0)
   6052     %ldl(t0, slot)
   6053     %ld(a0, t0, 16)
   6054     %eret
   6055 
   6056     :.next
   6057     %ldl(t0, data)
   6058     %heap_ld(t0, t0, %HASHDATA.hdr)
   6059     %shri(t0, t0, 8)
   6060     %addi(t0, t0, -1)
   6061     %ldl(t1, idx)
   6062     %addi(t1, t1, 1)
   6063     %and(t1, t1, t0)
   6064     %stl(t1, idx)
   6065     %ldl(t0, remaining)
   6066     %addi(t0, t0, -1)
   6067     %stl(t0, remaining)
   6068     %b(&.probe)
   6069 
   6070     :.miss
   6071     %li(a0, %imm_val(%IMM.FALSE))
   6072 })
   6073 
   6074 # hash_set(table=a0, key=a1, value=a2) -> table. Grow when live+tombstone
   6075 # occupancy reaches 3/4. The first tombstone in a probe chain is reused, but
   6076 # probing continues until an empty slot so an existing equal key still wins.
   6077 %gcfn2(hash_set, {table key value data hash idx remaining tomb slot}, 15, 0, {
   6078     %stl(a0, table)
   6079     %stl(a1, key)
   6080     %stl(a2, value)
   6081     %heap_ld(t0, a0, %HASH.data)
   6082     %stl(t0, data)
   6083     %heap_ld(t1, t0, %HASHDATA.used)
   6084     %addi(t1, t1, 1)
   6085     %shli(t1, t1, 2)
   6086     %heap_ld(t2, t0, %HASHDATA.hdr)
   6087     %shri(t2, t2, 8)
   6088     %shli(a0, t2, 1)
   6089     %add(a0, a0, t2)            ; capacity * 3
   6090     %bltu(t1, a0, &.ready)
   6091     %ldl(a0, table)
   6092     %call(&hash_resize)
   6093     %ldl(t0, table)
   6094     %heap_ld(t0, t0, %HASH.data)
   6095     %stl(t0, data)
   6096 
   6097     :.ready
   6098     %ldl(a0, key)
   6099     %call(&hash_key_hash)
   6100     %ori(a0, a0, 2)
   6101     %stl(a0, hash)
   6102     %ldl(t0, data)
   6103     %heap_ld(t1, t0, %HASHDATA.hdr)
   6104     %shri(t1, t1, 8)
   6105     %stl(t1, remaining)
   6106     %addi(t1, t1, -1)
   6107     %and(t1, t1, a0)
   6108     %stl(t1, idx)
   6109     %li(t0, 0)
   6110     %stl(t0, tomb)
   6111 
   6112     :.probe
   6113     %ldl(t0, remaining)
   6114     %beqz(t0, &.use_tomb)
   6115     %ldl(t0, data)
   6116     %ldl(t1, idx)
   6117     %hash_slot(t2, t0, t1, a0)
   6118     %stl(t2, slot)
   6119     %ld(t2, t2, 0)
   6120     %beqz(t2, &.empty)
   6121     %li(t0, 1)
   6122     %beq(t2, t0, &.remember_tomb)
   6123     %ldl(a0, hash)
   6124     %bne(t2, a0, &.next)
   6125     %ldl(t2, slot)
   6126     %ld(a0, t2, 8)
   6127     %ldl(a1, key)
   6128     %call(&hash_key_equal)
   6129     %bieq(a0, %imm_val(%IMM.FALSE), &.next, t0)
   6130     %ldl(t0, slot)
   6131     %ldl(t1, value)
   6132     %st(t1, t0, 16)
   6133     %b(&.done)
   6134 
   6135     :.remember_tomb
   6136     %ldl(t0, tomb)
   6137     %bnez(t0, &.next)
   6138     %ldl(t0, slot)
   6139     %stl(t0, tomb)
   6140     %b(&.next)
   6141 
   6142     :.empty
   6143     %ldl(t0, tomb)
   6144     %bnez(t0, &.insert_tomb)
   6145     %ldl(t0, slot)
   6146     %stl(t0, tomb)
   6147     # Inserting into a genuinely empty slot increases used.
   6148     %ldl(t1, data)
   6149     %heap_ld(t2, t1, %HASHDATA.used)
   6150     %addi(t2, t2, 1)
   6151     %heap_st(t2, t1, %HASHDATA.used)
   6152     %b(&.insert)
   6153 
   6154     :.insert_tomb
   6155     %stl(t0, tomb)
   6156     %b(&.insert)
   6157 
   6158     :.next
   6159     %ldl(t0, data)
   6160     %heap_ld(t0, t0, %HASHDATA.hdr)
   6161     %shri(t0, t0, 8)
   6162     %addi(t0, t0, -1)
   6163     %ldl(t1, idx)
   6164     %addi(t1, t1, 1)
   6165     %and(t1, t1, t0)
   6166     %stl(t1, idx)
   6167     %ldl(t0, remaining)
   6168     %addi(t0, t0, -1)
   6169     %stl(t0, remaining)
   6170     %b(&.probe)
   6171 
   6172     :.use_tomb
   6173     %ldl(t0, tomb)
   6174     %beqz(t0, &.full)
   6175 
   6176     :.insert
   6177     %ldl(t0, tomb)
   6178     %ldl(t1, hash)
   6179     %st(t1, t0, 0)
   6180     %ldl(t1, key)
   6181     %st(t1, t0, 8)
   6182     %ldl(t1, value)
   6183     %st(t1, t0, 16)
   6184     %ldl(t0, data)
   6185     %heap_ld(t1, t0, %HASHDATA.count)
   6186     %addi(t1, t1, 1)
   6187     %heap_st(t1, t0, %HASHDATA.count)
   6188 
   6189     :.done
   6190     %ldl(a0, table)
   6191     %gceret
   6192     :.full
   6193     %die(msg_hash_full)
   6194 })
   6195 
   6196 # hash_delete(table=a0, key=a1) -> Scheme boolean. Tombstoning preserves
   6197 # subsequent entries in the probe chain; key/value slots are cleared so the
   6198 # collector can reclaim them immediately.
   6199 %fn2(hash_delete, {table key data hash idx remaining slot}, {
   6200     %stl(a0, table)
   6201     %stl(a1, key)
   6202     %mov(a0, a1)
   6203     %call(&hash_key_hash)
   6204     %ori(a0, a0, 2)
   6205     %stl(a0, hash)
   6206     %ldl(t0, table)
   6207     %heap_ld(t0, t0, %HASH.data)
   6208     %stl(t0, data)
   6209     %heap_ld(t1, t0, %HASHDATA.hdr)
   6210     %shri(t1, t1, 8)
   6211     %stl(t1, remaining)
   6212     %addi(t1, t1, -1)
   6213     %and(t1, t1, a0)
   6214     %stl(t1, idx)
   6215 
   6216     :.probe
   6217     %ldl(t0, remaining)
   6218     %beqz(t0, &.miss)
   6219     %ldl(t0, data)
   6220     %ldl(t1, idx)
   6221     %hash_slot(t2, t0, t1, a0)
   6222     %stl(t2, slot)
   6223     %ld(t2, t2, 0)
   6224     %beqz(t2, &.miss)
   6225     %ldl(a0, hash)
   6226     %bne(t2, a0, &.next)
   6227     %ldl(t2, slot)
   6228     %ld(a0, t2, 8)
   6229     %ldl(a1, key)
   6230     %call(&hash_key_equal)
   6231     %bieq(a0, %imm_val(%IMM.FALSE), &.next, t0)
   6232     %ldl(t0, slot)
   6233     %li(t1, 1)
   6234     %st(t1, t0, 0)
   6235     %li(t1, 0)
   6236     %st(t1, t0, 8)
   6237     %st(t1, t0, 16)
   6238     %ldl(t0, data)
   6239     %heap_ld(t1, t0, %HASHDATA.count)
   6240     %addi(t1, t1, -1)
   6241     %heap_st(t1, t0, %HASHDATA.count)
   6242     %li(a0, %imm_val(%IMM.TRUE))
   6243     %eret
   6244 
   6245     :.next
   6246     %ldl(t0, data)
   6247     %heap_ld(t0, t0, %HASHDATA.hdr)
   6248     %shri(t0, t0, 8)
   6249     %addi(t0, t0, -1)
   6250     %ldl(t1, idx)
   6251     %addi(t1, t1, 1)
   6252     %and(t1, t1, t0)
   6253     %stl(t1, idx)
   6254     %ldl(t0, remaining)
   6255     %addi(t0, t0, -1)
   6256     %stl(t0, remaining)
   6257     %b(&.probe)
   6258 
   6259     :.miss
   6260     %li(a0, %imm_val(%IMM.FALSE))
   6261 })
   6262 
   6263 # Private Scheme entry points.
   6264 %fn(prim_make_hash_entry, 0, {
   6265     %car(t0, a0)
   6266     %tagof(t1, t0)
   6267     %bine(t1, %TAG.FIXNUM, &.bad, t2)
   6268     %untag_fix(a0, t0)
   6269     %bltz(a0, &.bad)
   6270     %call(&hash_make)
   6271     %eret
   6272     :.bad
   6273     %die(msg_type)
   6274 })
   6275 
   6276 %fn(prim_hash_ref_entry, 0, {
   6277     %args2(t0, t1, a0)
   6278     %mov(a0, t0)
   6279     %mov(a1, t1)
   6280     %call(&hash_ref)
   6281     %eret
   6282 })
   6283 
   6284 %fn(prim_hash_set_entry, 0, {
   6285     %args3(t0, t1, t2, a0)
   6286     %mov(a0, t0)
   6287     %mov(a1, t1)
   6288     %mov(a2, t2)
   6289     %call(&hash_set)
   6290     %li(a0, %imm_val(%IMM.UNSPEC))
   6291     %eret
   6292 })
   6293 
   6294 %fn(prim_hash_delete_entry, 0, {
   6295     %args2(t0, t1, a0)
   6296     %mov(a0, t0)
   6297     %mov(a1, t1)
   6298     %call(&hash_delete)
   6299     %eret
   6300 })
   6301 
   6302 :prim_hash_size_entry
   6303     %car(t0, a0)
   6304     %heap_ld(t0, t0, %HASH.data)
   6305     %heap_ld(a0, t0, %HASHDATA.count)
   6306     %mkfix(a0, a0)
   6307     %ret
   6308 
   6309 # equal_recurse(a=a0, b=a1) -> a0 (IMM.TRUE / IMM.FALSE). Identity covers
   6310 # fixnums, characters, symbols, immediates, and any case where both arguments
   6311 # are the same heap or pair pointer. For non-identical pair pointers we recurse
   6312 # into car then cdr. Same-type bytevectors and strings compare by content;
   6313 # records compare structurally; closures, prims, and TDs are identity-only.
   6314 # Tail-calls the cdr-side recursion and the byte-sequence/record checks.
   6315 #
   6316 # Locals:
   6317 #   a
   6318 #   b
   6319 %fn2(equal_recurse, {a b}, {
   6320     %stl(a0, a)
   6321     %stl(a1, b)
   6322 
   6323     %beq(a0, a1, &.true)
   6324 
   6325     %tagof(t0, a0)
   6326     %tagof(t1, a1)
   6327     %bne(t0, t1, &.false)
   6328 
   6329     %bieq(t0, %TAG.PAIR, &.pair, t1)
   6330     %bieq(t0, %TAG.HEAP, &.heap, t1)
   6331     %b(&.false)
   6332 
   6333     :.pair
   6334     %ldl(t0, a)
   6335     %ldl(t1, b)
   6336     %car(a0, t0)
   6337     %car(a1, t1)
   6338     %call(&equal_recurse)
   6339     %bieq(a0, %imm_val(%IMM.FALSE), &.done, t0)
   6340     %ldl(t0, a)
   6341     %ldl(t1, b)
   6342     %cdr(a0, t0)
   6343     %cdr(a1, t1)
   6344     %tail(&equal_recurse)
   6345 
   6346     :.heap
   6347     %ldl(t0, a)
   6348     %ldl(t1, b)
   6349     %hdr_type(t2, t0)
   6350     %hdr_type(a0, t1)
   6351     %bne(t2, a0, &.false)      ; differing heap classes -> #f
   6352     %li(a0, %HDR.BV)
   6353     %beq(t2, a0, &.heap_bv)
   6354     %li(a0, %HDR.STRING)
   6355     %beq(t2, a0, &.heap_bv)
   6356     %li(a0, %HDR.REC)
   6357     %beq(t2, a0, &.heap_rec)
   6358     %b(&.false)                 ; CLOSURE/PRIM/TD: identity-only
   6359 
   6360     :.heap_bv
   6361     %mov(a0, t0)
   6362     %mov(a1, t1)
   6363     %tail(&bv_equal_check)
   6364 
   6365     :.heap_rec
   6366     %mov(a0, t0)
   6367     %mov(a1, t1)
   6368     %tail(&rec_equal_check)
   6369 
   6370     :.true
   6371     %li(a0, %imm_val(%IMM.TRUE))
   6372     %b(&.done)
   6373 
   6374     :.false
   6375     %li(a0, %imm_val(%IMM.FALSE))
   6376 
   6377     :.done
   6378 })
   6379 
   6380 # rec_equal_check(a=a0, b=a1) -> a0 (IMM.TRUE / IMM.FALSE). Both args
   6381 # are HEAP-tagged HDR.REC. Records are equal iff their TDs are eq? and
   6382 # every field is equal? (recursing through equal_recurse). Field i sits
   6383 # at tagged + 13 + 8*i; nfields lives at the TD's offset 13 (raw).
   6384 #
   6385 # Locals:
   6386 #   a  (rec, tagged)
   6387 #   b  (rec, tagged)
   6388 #   i  (raw counter)
   6389 #   nfields  (raw)
   6390 %fn2(rec_equal_check, {a b i nfields}, {
   6391     %stl(a0, a)
   6392     %stl(a1, b)
   6393 
   6394     %heap_ld(t0, a0, %REC.td)   ; td_a
   6395     %heap_ld(t1, a1, %REC.td)   ; td_b
   6396     %bne(t0, t1, &.false)
   6397 
   6398     %heap_ld(t1, t0, %TD.nfields)
   6399     %stl(t1, nfields)
   6400     %li(t0, 0)
   6401     %stl(t0, i)              ; i = 0
   6402 
   6403     :.loop
   6404     %ldl(t0, i)
   6405     %ldl(t1, nfields)
   6406     %beq(t0, t1, &.true)
   6407 
   6408     %shli(t2, t0, 3)
   6409     %addi(t2, t2, 13)            ; field offset = 13 + 8*i
   6410     %ldl(t1, a)
   6411     %add(t1, t1, t2)
   6412     %ld(a0, t1, 0)               ; a's field i
   6413     %ldl(t1, b)
   6414     %add(t1, t1, t2)
   6415     %ld(a1, t1, 0)               ; b's field i
   6416     %call(&equal_recurse)
   6417     %bieq(a0, %imm_val(%IMM.FALSE), &.done, t0)
   6418 
   6419     %ldl(t0, i)
   6420     %addi(t0, t0, 1)
   6421     %stl(t0, i)
   6422     %b(&.loop)
   6423 
   6424     :.true
   6425     %li(a0, %imm_val(%IMM.TRUE))
   6426     %b(&.done)
   6427 
   6428     :.false
   6429     %li(a0, %imm_val(%IMM.FALSE))
   6430 
   6431     :.done
   6432 })
   6433 
   6434 # (equal? a b) -- thin prim wrapper that unpacks the args list and falls
   6435 # into equal_recurse. equal_recurse owns the frame; this entry stays a
   6436 # leaf so the prim-dispatch tailr lands directly into the frame setup.
   6437 :prim_equal_entry
   6438     %args2(t0, t1, a0)
   6439     %mov(a0, t0)
   6440     %mov(a1, t1)
   6441     %b(&equal_recurse)
   6442 
   6443 # (apply fn rest...)  -- the trailing element of `rest` is a list; any
   6444 # leading elements get prepended to it. apply_build_args walks `rest` and
   6445 # returns the assembled args list; prim_apply_entry then tail-calls apply.
   6446 #
   6447 # `apply` is itself a primitive, so on entry here a0 holds (fn . rest)
   6448 # and a1 holds the apply PRIM ptr (per the convention documented at
   6449 # `apply::prim`). a1 is dead from this primitive's point of view; we
   6450 # clobber it freely while assembling args, then tail-call apply, which
   6451 # re-derives a1 from the callee fn it dispatches on. Outer convention
   6452 # stays intact end-to-end.
   6453 
   6454 %gcfn2(prim_apply_entry, {args pad}, 1, 0, {
   6455     %stl(a0, args)
   6456     %cdr(a0, a0)
   6457     %call(&apply_build_args)
   6458     %mov(t0, a0)
   6459     %ldl(a0, args)
   6460     %car(a0, a0)
   6461     %mov(a1, t0)
   6462     %gctail(&apply)
   6463 })
   6464 
   6465 # apply_build_args(rest=a0) -> assembled args list.
   6466 # `rest` is (a1 a2 ... aN listargs); the trailing element is itself a list
   6467 # whose elements get appended after the leading aâ‚–'s. Iterative
   6468 # head/tail-cdr build: walk every cell whose cdr isn't NIL into a fresh
   6469 # (aâ‚– . NIL) cons; the final element (the trailing list) becomes the
   6470 # tail's cdr (or the result itself if there are no leading elements).
   6471 #
   6472 # Locals:
   6473 #   walk  (advances; current cell of rest)
   6474 #   head  (NIL until first leading arg appended)
   6475 #   tail  (most recent cell; set-cdr! target)
   6476 %gcfn2(apply_build_args, {walk head tail}, 7, 0, {
   6477     %stl(a0, walk)
   6478     %li(t0, %imm_val(%IMM.NIL))
   6479     %stl(t0, head)
   6480     %stl(t0, tail)
   6481 
   6482     :.loop
   6483         %ldl(t0, walk)
   6484         %cdr(t1, t0)
   6485         %if_nil(t2, t1, &.last)
   6486 
   6487         # cell = cons(car(walk), NIL); append to head/tail.
   6488         %car(a0, t0)
   6489         %li(a1, %imm_val(%IMM.NIL))
   6490         %call(&cons)
   6491 
   6492         %ldl(t0, head)
   6493         %if_nil(t1, t0, &.first)
   6494         %ldl(t0, tail)
   6495         %set_cdr(a0, t0)
   6496         %stl(a0, tail)
   6497         %b(&.advance)
   6498 
   6499         :.first
   6500         %stl(a0, head)
   6501         %stl(a0, tail)
   6502 
   6503         :.advance
   6504         %advance_walk(walk)
   6505         %b(&.loop)
   6506 
   6507     :.last
   6508     # car(walk) is the trailing list. If head is NIL there were no leading
   6509     # args -- return the trailing list directly. Otherwise splice it onto
   6510     # the tail and return head.
   6511     %car(a0, t0)
   6512     %ldl(t1, head)
   6513     %if_nil(t2, t1, &.done)
   6514     %ldl(t1, tail)
   6515     %set_cdr(a0, t1)
   6516     %ldl(a0, head)
   6517 
   6518     :.done
   6519 })
   6520 
   6521 # Records: TDs (type descriptors) and instances. A TD is a 24-byte heap
   6522 # object [HDR.TD][name_sym][nfields_raw]. A record is a variable-width
   6523 # heap object [HDR.REC][td][field_0]...[field_{n-1}], so field i lives at
   6524 # tagged + 13 + 8*i. define-record-type allocates one TD plus one
   6525 # parameterized PRIM per ctor/predicate/accessor/mutator, all pointing
   6526 # into the same TD via the prim's data slot.
   6527 
   6528 # make_param_prim(entry=a0, data=a1) -> prim (a0). Allocates a 24-byte
   6529 # PRIM and sets the entry label and data word. Generated primitives stay
   6530 # reachable through their global bindings.
   6531 
   6532 %gcfn2(make_param_prim, {entry data}, 2, 0, {
   6533     %stl(a0, entry)
   6534     %stl(a1, data)
   6535 
   6536     %li(a0, 24)
   6537     %li(a1, %HDR.PRIM)
   6538     %call(&alloc_hdr)
   6539 
   6540     %ldl(t0, entry)
   6541     %heap_st(t0, a0, %PRIM.entry_w)
   6542     %ldl(t1, data)
   6543     %heap_st(t1, a0, %PRIM.data)
   6544 })
   6545 
   6546 # Parameterized PRIM entries used by define-record-type. Each receives
   6547 # args in a0 and the prim itself in a1; the prim's data slot (offset 13
   6548 # from tagged) holds either the TD or a tagged field index. The
   6549 # constructor inlines record allocation; predicate / accessor / mutator
   6550 # inline what would otherwise be %record-is-a? / %record-ref /
   6551 # %record-set! bodies. None of these primitives are exposed at the
   6552 # user level — R7RS define-record-type binds only ctor / pred /
   6553 # accessor / mutator names.
   6554 
   6555 # ctor: prim.data = TD (HEAP); args = (f0 f1 ...). Inlines the
   6556 # %make-record body so we don't have to cons (TD . args) first.
   6557 
   6558 %gcfn2(prim_ctor_entry, {args td record}, 7, 0, {
   6559     %stl(a0, args)
   6560     %heap_ld(t0, a1, %PRIM.data)
   6561     %stl(t0, td)
   6562 
   6563     # Count = length(args).
   6564     %call(&list_length)
   6565     %ldl(t0, td)
   6566     %heap_ld(t1, t0, %TD.nfields)
   6567     %bne(a0, t1, &.bad_arity)
   6568     %shli(a0, a0, 3)
   6569     %addi(a0, a0, 16)
   6570     %li(a1, %HDR.REC)
   6571     %call(&alloc_hdr)
   6572     %stl(a0, record)
   6573 
   6574     %ldl(t0, td)
   6575     %heap_st(t0, a0, %REC.td)
   6576 
   6577     %ldl(t0, args)
   6578     %addi(t1, a0, 13)
   6579 
   6580     :.fill_loop
   6581         %if_nil(t2, t0, &.fill_done)
   6582         %car(t2, t0)
   6583         %st(t2, t1, 0)
   6584         %addi(t1, t1, 8)
   6585         %cdr(t0, t0)
   6586         %b(&.fill_loop)
   6587     :.fill_done
   6588 
   6589     %ldl(a0, record)
   6590     %gceret
   6591 
   6592     :.bad_arity
   6593     %die(msg_record_ctor_arity)
   6594 })
   6595 
   6596 # predicate: prim.data = TD; args = (rec).
   6597 :prim_predicate_entry
   6598 .scope
   6599     %car(t0, a0)
   6600     %heap_ld(t1, a1, %PRIM.data)
   6601     %tagof(t2, t0)
   6602     %li(a0, %imm_val(%IMM.FALSE))
   6603     %bine(t2, %TAG.HEAP, &.end, a2)
   6604     %hdr_type(t2, t0)
   6605     %bine(t2, %HDR.REC,  &.end, a2)
   6606     %heap_ld(t2, t0, %REC.td)
   6607     %bne(t2, t1, &.end)
   6608     %li(a0, %imm_val(%IMM.TRUE))
   6609     :.end
   6610     %ret
   6611 .endscope
   6612 
   6613 # accessor: prim.data = tagged field index; args = (rec).
   6614 :prim_accessor_entry
   6615     %car(t0, a0)
   6616     %heap_ld(t1, a1, %PRIM.data)
   6617     %addi(t1, t1, 13)
   6618     %add(t1, t1, t0)
   6619     %ld(a0, t1, 0)
   6620     %ret
   6621 
   6622 # mutator: prim.data = tagged field index; args = (rec val).
   6623 :prim_mutator_entry
   6624 .scope
   6625     %car(t0, a0)
   6626     %cdr(t1, a0)
   6627     %car(t1, t1)
   6628     %heap_ld(t2, a1, %PRIM.data)
   6629     %addi(t2, t2, 13)
   6630     %add(t2, t2, t0)
   6631     %st(t1, t2, 0)
   6632     %li(a0, %imm_val(%IMM.UNSPEC))
   6633     %ret
   6634 .endscope
   6635 
   6636 # eval_define_record_type(rest=a0, env=a1) -> UNSPEC.
   6637 # rest = (name (ctor f1 ...) pred clause1 clause2 ...)
   6638 # Each clause is (field-name accessor) or (field-name accessor mutator).
   6639 # Allocates one TD + one parameterized PRIM per name introduced (ctor,
   6640 # predicate, accessor, mutator) and binds each to the symbol's global.
   6641 # The TD also stores a list of field-name symbols in declaration order;
   6642 # pmatch's ($ pred (field pat) ...) record pattern uses this to map
   6643 # field names to indices at match time.
   6644 #
   6645 # Locals:
   6646 #   rest
   6647 #   env  (unused, but the dispatcher passes it)
   6648 #   td
   6649 #   walk  (clauses, advancing)
   6650 #   idx  (raw counter)
   6651 #   nfields
   6652 #   fl_head  (head of field-name list under construction)
   6653 #   fl_tail  (tail cell of field-name list under construction)
   6654 #   fl_cur   (cursor walking clauses for field-name pre-pass)
   6655 %gcfn2(eval_define_record_type, {rest env td walk idx nfields fl_head fl_tail fl_cur}, 463, 0, {
   6656     %stl(a0, rest)
   6657     %stl(a1, env)
   6658 
   6659     # clauses = cdddr(rest). Before allocating or publishing any bindings,
   6660     # validate the restricted micro constructor specification: its field
   6661     # list must name every declared field exactly once and in declaration
   6662     # order.
   6663     %ldl(a0, rest)
   6664     %cdr(a0, a0)
   6665     %cdr(a0, a0)
   6666     %cdr(a0, a0)
   6667     %stl(a0, walk)
   6668 
   6669     # fl_cur = cdr(cadr(rest)) -- constructor field cursor.
   6670     %ldl(t0, rest)
   6671     %cdr(t0, t0)
   6672     %car(t0, t0)
   6673     %cdr(t0, t0)
   6674     %stl(t0, fl_cur)
   6675 
   6676     :.validate_ctor
   6677     %ldl(t0, fl_cur)
   6678     %ldl(t1, walk)
   6679     %if_nil(t2, t0, &.ctor_fields_done)
   6680     %if_nil(t2, t1, &.bad_ctor_spec)
   6681     %car(t2, t0)                  ; constructor field name
   6682     %car(a0, t1)                  ; declaration clause
   6683     %car(a0, a0)                  ; declared field name
   6684     %bne(t2, a0, &.bad_ctor_spec)
   6685     %cdr(t0, t0)
   6686     %stl(t0, fl_cur)
   6687     %cdr(t1, t1)
   6688     %stl(t1, walk)
   6689     %b(&.validate_ctor)
   6690 
   6691     :.ctor_fields_done
   6692     %if_nil(t2, t1, &.ctor_valid)
   6693     %b(&.bad_ctor_spec)
   6694 
   6695     :.ctor_valid
   6696     # Restore the declaration cursor after validation and count fields.
   6697     %ldl(a0, rest)
   6698     %cdr(a0, a0)
   6699     %cdr(a0, a0)
   6700     %cdr(a0, a0)
   6701     %stl(a0, walk)
   6702     %call(&list_length)
   6703     %stl(a0, nfields)
   6704 
   6705     # td = alloc_hdr(TD.SIZE, HDR.TD); td.name = type-name;
   6706     # td.nfields = nfields; td.fields = NIL (filled below).
   6707     %li(a0, %TD.SIZE)
   6708     %li(a1, %HDR.TD)
   6709     %call(&alloc_hdr)
   6710     %stl(a0, td)
   6711     %ldl(t0, rest)
   6712     %car(t0, t0)
   6713     %heap_st(t0, a0, %TD.name)
   6714     %ldl(t1, nfields)
   6715     %heap_st(t1, a0, %TD.nfields)
   6716     %li(t1, %imm_val(%IMM.NIL))
   6717     %heap_st(t1, a0, %TD.fields)
   6718 
   6719     # Pre-pass: build (field-name-1 ... field-name-N) in declaration order
   6720     # via head/tail accumulator, then store at td.fields. Each clause's
   6721     # car is the field-name symbol. Uses fl_cur as a separate cursor so
   6722     # walk is left intact for the accessor-binding loop below.
   6723     %li(t0, %imm_val(%IMM.NIL))
   6724     %stl(t0, fl_head)
   6725     %stl(t0, fl_tail)
   6726     %ldl(t0, walk)
   6727     %stl(t0, fl_cur)
   6728 
   6729     :.fl_loop
   6730     %ldl(t0, fl_cur)
   6731     %if_nil(t1, t0, &.fl_done)
   6732     # cell = cons(car(car(fl_cur)), NIL)
   6733     %car(t1, t0)
   6734     %car(a0, t1)
   6735     %li(a1, %imm_val(%IMM.NIL))
   6736     %call(&cons)
   6737     # Splice into list: if head is NIL, head = tail = cell.
   6738     # Else set-cdr!(tail, cell); tail = cell.
   6739     %ldl(t1, fl_head)
   6740     %bine(t1, %imm_val(%IMM.NIL), &.fl_append, t2)
   6741     %stl(a0, fl_head)
   6742     %stl(a0, fl_tail)
   6743     %b(&.fl_next)
   6744     :.fl_append
   6745     %ldl(t1, fl_tail)
   6746     %set_cdr(a0, t1)
   6747     %stl(a0, fl_tail)
   6748     :.fl_next
   6749     %ldl(t0, fl_cur)
   6750     %cdr(t0, t0)
   6751     %stl(t0, fl_cur)
   6752     %b(&.fl_loop)
   6753 
   6754     :.fl_done
   6755     %ldl(t0, td)
   6756     %ldl(t1, fl_head)
   6757     %heap_st(t1, t0, %TD.fields)
   6758 
   6759     # ctor-prim = make_param_prim(prim_ctor_entry, td); bind ctor-name.
   6760     %la(a0, &prim_ctor_entry)
   6761     %ldl(a1, td)
   6762     %call(&make_param_prim)
   6763     %ldl(t0, rest)
   6764     %cdr(t0, t0)
   6765     %car(t0, t0)
   6766     %car(t0, t0)
   6767     %set_global(t0, a0)
   6768 
   6769     # pred-prim = make_param_prim(prim_predicate_entry, td); bind pred.
   6770     %la(a0, &prim_predicate_entry)
   6771     %ldl(a1, td)
   6772     %call(&make_param_prim)
   6773     %ldl(t0, rest)
   6774     %cdr(t0, t0)
   6775     %cdr(t0, t0)
   6776     %car(t0, t0)
   6777     %set_global(t0, a0)
   6778 
   6779     # Iterate clauses: bind accessor + optional mutator per clause.
   6780     %li(t0, 0)
   6781     %stl(t0, idx)
   6782 
   6783     :.clause_loop
   6784     %ldl(t0, walk)
   6785     %if_nil(t1, t0, &.done)
   6786 
   6787     # accessor-prim with data = tagged idx; bind cadr(clause).
   6788     %ldl(a1, idx)
   6789     %mkfix(a1, a1)
   6790     %la(a0, &prim_accessor_entry)
   6791     %call(&make_param_prim)
   6792 
   6793     %ldl(t0, walk)
   6794     %car(t0, t0)
   6795     %cdr(t0, t0)
   6796     %car(t0, t0)
   6797     %set_global(t0, a0)
   6798 
   6799     # Mutator? If cddr(clause) is a pair, bind it.
   6800     %ldl(t0, walk)
   6801     %car(t0, t0)
   6802     %cdr(t0, t0)
   6803     %cdr(t0, t0)
   6804     %if_nil(t1, t0, &.no_mutator)
   6805 
   6806     %ldl(a1, idx)
   6807     %mkfix(a1, a1)
   6808     %la(a0, &prim_mutator_entry)
   6809     %call(&make_param_prim)
   6810 
   6811     %ldl(t0, walk)
   6812     %car(t0, t0)
   6813     %cdr(t0, t0)
   6814     %cdr(t0, t0)
   6815     %car(t0, t0)
   6816     %set_global(t0, a0)
   6817 
   6818     :.no_mutator
   6819     %advance_walk(walk)
   6820     %ldl(t0, idx)
   6821     %addi(t0, t0, 1)
   6822     %stl(t0, idx)
   6823     %b(&.clause_loop)
   6824 
   6825     :.done
   6826     %li(a0, %imm_val(%IMM.UNSPEC))
   6827     %gceret
   6828 
   6829     :.bad_ctor_spec
   6830     %die(msg_record_ctor_spec)
   6831 })
   6832 
   6833 # =========================================================================
   6834 # Writer -- display, write, format, error
   6835 # =========================================================================
   6836 #
   6837 # All four entry points walk values through a single recursive writer
   6838 # that appends bytes into an output bytevector. display / write call the
   6839 # writer once, then sys_write the resulting bytes to stdout. error
   6840 # prepends `scheme1: error: `, joins irritants with spaces, and tails
   6841 # into runtime_error so the prefix stays consistent with every other
   6842 # abort path. format walks a template bv, emitting raw bytes verbatim
   6843 # and dispatching ~a (display), ~s (write), ~d (decimal), ~% (newline),
   6844 # and ~~ (literal '~') against successive args.
   6845 #
   6846 # Mode flag for write_to_bv: 0 = display (strings and bytevectors emit raw),
   6847 # 1 = write (strings use an escaped quoted spelling and bytevectors use
   6848 # #u8(...)).
   6849 #
   6850 # bv_putn / bv_putc / bv_putint append raw bytes to a bv and return the
   6851 # (same wrapper, possibly-grown) bv. They do NOT maintain a trailing NUL
   6852 # -- callers building "strings" must use the str_* family below.
   6853 # bv_grow patches data_ptr/capacity in place, so the wrapper pointer
   6854 # never changes -- callers can keep a stable handle in a single frame
   6855 # slot.
   6856 
   6857 # bv_putn(bv=a0, src=a1, n=a2) -> bv (a0). Append n bytes from src to bv,
   6858 # growing the data buffer when capacity falls short. Raw u8[] semantics:
   6859 # the byte at index `length` after append is unspecified.
   6860 
   6861 %gcfn2(bv_putn, {bv src n old_len}, 1, 2, {
   6862     %stl(a0, bv)
   6863     %stl(a1, src)
   6864     %stl(a2, n)
   6865 
   6866     %heap_ld(t0, a0, %BV.hdr)
   6867     %shri(t0, t0, 8)            ; old_len
   6868     %stl(t0, old_len)
   6869 
   6870     # bv_grow ensures cap >= old_len + n.
   6871     %add(a1, t0, a2)
   6872     %call(&bv_grow)
   6873 
   6874     %ldl(t0, bv)
   6875     %heap_ld(a0, t0, %BV.data)
   6876     %ldl(t1, old_len)
   6877     %add(a0, a0, t1)            ; dst = data + old_len
   6878     %ldl(a1, src)
   6879     %ldl(a2, n)
   6880     %call(&memcpy)
   6881 
   6882     # hdr = (old_len + n) << 8 | HDR.BV. HDR.BV is 0.
   6883     %ldl(t0, old_len)
   6884     %ldl(t1, n)
   6885     %add(t0, t0, t1)
   6886     %shli(t0, t0, 8)
   6887     %ldl(t1, bv)
   6888     %heap_st(t0, t1, %BV.hdr)
   6889 
   6890     %ldl(a0, bv)
   6891 })
   6892 
   6893 # bv_putc(bv=a0, byte=a1) -> bv (a0). Append a single byte (low 8 bits
   6894 # of a1). Same growth + length-update protocol as bv_putn; no NUL.
   6895 
   6896 %gcfn2(bv_putc, {bv byte}, 1, 0, {
   6897     %stl(a0, bv)
   6898     %stl(a1, byte)
   6899 
   6900     %heap_ld(t0, a0, %BV.hdr)
   6901     %shri(t0, t0, 8)            ; old_len
   6902     %addi(a1, t0, 1)             ; min_cap = old_len + 1
   6903     %call(&bv_grow)
   6904 
   6905     %ldl(t0, bv)
   6906     %heap_ld(t1, t0, %BV.hdr)
   6907     %shri(t1, t1, 8)            ; old_len (re-read after grow)
   6908     %heap_ld(t2, t0, %BV.data)
   6909     %add(t2, t2, t1)
   6910     %ldl(a0, byte)
   6911     %sb(a0, t2, 0)
   6912 
   6913     %addi(t1, t1, 1)
   6914     %shli(t1, t1, 8)
   6915     %heap_st(t1, t0, %BV.hdr)
   6916 
   6917     %ldl(a0, bv)
   6918 })
   6919 
   6920 # bv_putint(bv=a0, value=a1) -> bv (a0). Append decimal repr of (raw,
   6921 # untagged) value. Uses :writer_num_buf as a 24-byte scratch buffer
   6922 # (fmt_dec writes at most 20 bytes for a 64-bit signed integer).
   6923 
   6924 %gcfn2(bv_putint, {bv pad}, 1, 0, {
   6925     %stl(a0, bv)
   6926 
   6927     %la(a0, &writer_num_buf)
   6928     %call(&fmt_dec)              ; n_bytes (a0)
   6929 
   6930     %mov(a2, a0)
   6931     %la(a1, &writer_num_buf)
   6932     %ldl(a0, bv)
   6933     %gctail(&bv_putn)
   6934 })
   6935 
   6936 # String writers: identical to bv_putn / bv_putc / bv_putint except they
   6937 # guarantee cap > length AND data[length] == 0 on return. Required for
   6938 # any bv whose data_ptr is later read as a C string (syscall paths,
   6939 # runtime_error). The explicit zero is necessary because a fresh data
   6940 # buffer may come from a reused block carrying stale bytes.
   6941 
   6942 # str_alloc(raw_len=a0) -> tagged bv (a0). Like bv_alloc, but cap >
   6943 # raw_len and data[raw_len] = 0.
   6944 %gcfn2(str_alloc, {raw_len bv}, 2, 0, {
   6945     %stl(a0, raw_len)
   6946     %addi(a0, a0, 1)             ; reserve a NUL slot
   6947     %call(&bv_alloc)
   6948     %stl(a0, bv)
   6949 
   6950     # Patch hdr length back down to raw_len.
   6951     %ldl(t0, raw_len)
   6952     %shli(t0, t0, 8)             ; HDR.BV is 0
   6953     %heap_st(t0, a0, %BV.hdr)
   6954 
   6955     # Zero data[raw_len].
   6956     %heap_ld(t1, a0, %BV.data)
   6957     %ldl(t2, raw_len)
   6958     %add(t1, t1, t2)
   6959     %li(t0, 0)
   6960     %sb(t0, t1, 0)
   6961 
   6962     %ldl(a0, bv)
   6963 })
   6964 
   6965 # string_alloc(raw_len=a0) -> tagged HDR.STRING. Strings share the
   6966 # length/data/capacity layout with bytevectors so boot2's byte bridge and
   6967 # syscall wrappers can use one compact set of walkers, but the distinct
   6968 # header keeps the Scheme types disjoint. A spare trailing NUL is maintained
   6969 # for pathname interoperability; it is not part of the logical length.
   6970 %gcfn2(string_alloc, {raw_len string}, 2, 0, {
   6971     %stl(a0, raw_len)
   6972     %addi(a0, a0, 1)
   6973     %call(&bv_alloc)
   6974     %stl(a0, string)
   6975 
   6976     %ldl(t0, raw_len)
   6977     %shli(t0, t0, 8)
   6978     %ori(t0, t0, %HDR.STRING)
   6979     %heap_st(t0, a0, %BV.hdr)
   6980 
   6981     %heap_ld(t1, a0, %BV.data)
   6982     %ldl(t2, raw_len)
   6983     %add(t1, t1, t2)
   6984     %li(t0, 0)
   6985     %sb(t0, t1, 0)
   6986 
   6987     %ldl(a0, string)
   6988 })
   6989 
   6990 # str_putn(bv=a0, src=a1, n=a2) -> bv (a0). Append n bytes; on return
   6991 # cap > new_len and data[new_len] == 0.
   6992 %gcfn2(str_putn, {bv src n}, 1, 2, {
   6993     %stl(a0, bv)
   6994     %stl(a1, src)
   6995     %stl(a2, n)
   6996 
   6997     # Pre-grow so the post-append buffer has a NUL slot.
   6998     %heap_ld(t0, a0, %BV.hdr)
   6999     %shri(t0, t0, 8)             ; old_len
   7000     %add(a1, t0, a2)
   7001     %addi(a1, a1, 1)             ; min_cap = old_len + n + 1
   7002     %call(&bv_grow)
   7003 
   7004     %ldl(a0, bv)
   7005     %ldl(a1, src)
   7006     %ldl(a2, n)
   7007     %call(&bv_putn)              ; appends + updates length
   7008 
   7009     # Zero data[new_len]. bv_putn left cap and data_ptr alone, so the
   7010     # NUL slot reserved above is still ours.
   7011     %heap_ld(t0, a0, %BV.hdr)
   7012     %shri(t0, t0, 8)             ; new_len
   7013     %heap_ld(t1, a0, %BV.data)
   7014     %add(t1, t1, t0)
   7015     %li(t2, 0)
   7016     %sb(t2, t1, 0)
   7017 })
   7018 
   7019 # str_putc(bv=a0, byte=a1) -> bv (a0). Append one byte; cap > new_len
   7020 # and data[new_len] == 0 on return.
   7021 %gcfn2(str_putc, {bv byte}, 1, 0, {
   7022     %stl(a0, bv)
   7023     %stl(a1, byte)
   7024 
   7025     %heap_ld(t0, a0, %BV.hdr)
   7026     %shri(t0, t0, 8)             ; old_len
   7027     %addi(a1, t0, 2)             ; min_cap = old_len + 1 + 1
   7028     %call(&bv_grow)
   7029 
   7030     %ldl(a0, bv)
   7031     %ldl(a1, byte)
   7032     %call(&bv_putc)
   7033 
   7034     %heap_ld(t0, a0, %BV.hdr)
   7035     %shri(t0, t0, 8)             ; new_len
   7036     %heap_ld(t1, a0, %BV.data)
   7037     %add(t1, t1, t0)
   7038     %li(t2, 0)
   7039     %sb(t2, t1, 0)
   7040 })
   7041 
   7042 # str_putint(bv=a0, value=a1) -> bv (a0). Like bv_putint but tails into
   7043 # str_putn, so the result is NUL-terminated.
   7044 %gcfn2(str_putint, {bv pad}, 1, 0, {
   7045     %stl(a0, bv)
   7046 
   7047     %la(a0, &writer_num_buf)
   7048     %call(&fmt_dec)              ; n_bytes (a0)
   7049 
   7050     %mov(a2, a0)
   7051     %la(a1, &writer_num_buf)
   7052     %ldl(a0, bv)
   7053     %gctail(&str_putn)
   7054 })
   7055 
   7056 # str_puthex(bv=a0, value=a1) -> bv (a0). Signed hex: emits a leading
   7057 # '-' for negatives, then unsigned hex of |value| via fmt_hex. The bv
   7058 # wrapper pointer is stable across str_putc / str_putn (only the
   7059 # internal data buffer can move), so we reload it from the local.
   7060 %gcfn2(str_puthex, {bv value}, 1, 0, {
   7061     %stl(a0, bv)
   7062     %stl(a1, value)
   7063 
   7064     %bltz(a1, &.neg)
   7065     %b(&.pos)
   7066 
   7067     :.neg
   7068     %ldl(a0, bv)
   7069     %li(a1, 45)                  ; '-'
   7070     %call(&str_putc)
   7071     %ldl(t0, value)
   7072     %li(t1, 0)
   7073     %sub(t0, t1, t0)
   7074     %stl(t0, value)
   7075 
   7076     :.pos
   7077     %la(a0, &writer_num_buf)
   7078     %ldl(a1, value)
   7079     %call(&fmt_hex)              ; n_bytes (a0)
   7080 
   7081     %mov(a2, a0)
   7082     %la(a1, &writer_num_buf)
   7083     %ldl(a0, bv)
   7084     %gctail(&str_putn)
   7085 })
   7086 
   7087 # sym_name(idx=a0) -> (ptr=a0, len=a1). Leaf. idx is the untagged sym
   7088 # slot index; both fields come straight out of the symtab entry.
   7089 :sym_name
   7090     %ld_global(t0, &symtab_buf_ptr)
   7091     %lda_array(a1, t1, t0, %SYMENT.SIZE, a0, %SYMENT.name_len)
   7092     %ld(a0, t1, %SYMENT.name_ptr)
   7093     %ret
   7094 
   7095 # hex_digit_ascii(nibble=a0) -> lowercase ASCII byte.
   7096 :hex_digit_ascii
   7097 .scope
   7098     %li(t0, 10)
   7099     %bltu(a0, t0, &.digit)
   7100     %addi(a0, a0, 87)           ; 10 -> 'a'
   7101     %ret
   7102     :.digit
   7103     %addi(a0, a0, 48)
   7104     %ret
   7105 .endscope
   7106 
   7107 # write_string_to_bv(string=a0, bv=a1, mode=a2) -> bv. Display mode emits
   7108 # the logical bytes. Write mode emits a reader-compatible quoted spelling
   7109 # with the micro escape set, including embedded NUL bytes.
   7110 %gcfn2(write_string_to_bv, {string bv index length byte}, 3, 0, {
   7111     %stl(a0, string)
   7112     %stl(a1, bv)
   7113 
   7114     %heap_ld(t0, a0, %BV.hdr)
   7115     %shri(t0, t0, 8)
   7116     %stl(t0, length)
   7117     %beqz(a2, &.raw)
   7118 
   7119     %ldl(a0, bv)
   7120     %li(a1, 34)                  ; opening quote
   7121     %call(&str_putc)
   7122     %li(t0, 0)
   7123     %stl(t0, index)
   7124 
   7125     :.loop
   7126     %ldl(t0, index)
   7127     %ldl(t1, length)
   7128     %beq(t0, t1, &.close)
   7129     %ldl(t1, string)
   7130     %heap_ld(t2, t1, %BV.data)
   7131     %add(t2, t2, t0)
   7132     %lb(t2, t2, 0)
   7133     %stl(t2, byte)
   7134 
   7135     %bieq(t2, 10, &.escape_n, t0)
   7136     %bieq(t2, 9,  &.escape_t, t0)
   7137     %bieq(t2, 13, &.escape_r, t0)
   7138     %bieq(t2, 92, &.escape_slash, t0)
   7139     %bieq(t2, 34, &.escape_quote, t0)
   7140     %li(t0, 32)
   7141     %bltu(t2, t0, &.escape_hex)
   7142     %li(t0, 127)
   7143     %bltu(t2, t0, &.plain)
   7144     %b(&.escape_hex)
   7145 
   7146     :.plain
   7147     %ldl(a0, bv)
   7148     %mov(a1, t2)
   7149     %call(&str_putc)
   7150     %b(&.advance)
   7151 
   7152     :.escape_n
   7153     %li(t0, 110)                 ; 'n'
   7154     %b(&.escape_two)
   7155     :.escape_t
   7156     %li(t0, 116)                 ; 't'
   7157     %b(&.escape_two)
   7158     :.escape_r
   7159     %li(t0, 114)                 ; 'r'
   7160     %b(&.escape_two)
   7161     :.escape_slash
   7162     %li(t0, 92)
   7163     %b(&.escape_two)
   7164     :.escape_quote
   7165     %li(t0, 34)
   7166     :.escape_two
   7167     %stl(t0, byte)
   7168     %ldl(a0, bv)
   7169     %li(a1, 92)
   7170     %call(&str_putc)
   7171     %ldl(a0, bv)
   7172     %ldl(a1, byte)
   7173     %call(&str_putc)
   7174     %b(&.advance)
   7175 
   7176     :.escape_hex
   7177     %ldl(a0, bv)
   7178     %li(a1, 92)
   7179     %call(&str_putc)
   7180     %ldl(a0, bv)
   7181     %li(a1, 120)                 ; 'x'
   7182     %call(&str_putc)
   7183     %ldl(t0, byte)
   7184     %shri(a0, t0, 4)
   7185     %call(&hex_digit_ascii)
   7186     %mov(a1, a0)
   7187     %ldl(a0, bv)
   7188     %call(&str_putc)
   7189     %ldl(t0, byte)
   7190     %andi(a0, t0, 15)
   7191     %call(&hex_digit_ascii)
   7192     %mov(a1, a0)
   7193     %ldl(a0, bv)
   7194     %call(&str_putc)
   7195     %ldl(a0, bv)
   7196     %li(a1, 59)                  ; ';'
   7197     %call(&str_putc)
   7198 
   7199     :.advance
   7200     %ldl(t0, index)
   7201     %addi(t0, t0, 1)
   7202     %stl(t0, index)
   7203     %b(&.loop)
   7204 
   7205     :.close
   7206     %ldl(a0, bv)
   7207     %li(a1, 34)
   7208     %gctail(&str_putc)
   7209 
   7210     :.raw
   7211     %ldl(t0, string)
   7212     %heap_ld(a1, t0, %BV.data)
   7213     %ldl(a2, length)
   7214     %ldl(a0, bv)
   7215     %gctail(&str_putn)
   7216 })
   7217 
   7218 # write_bytevector_to_bv(bytes=a0, bv=a1, mode=a2) -> bv. Display mode
   7219 # retains boot2's raw-byte diagnostic convention; write mode uses #u8(...).
   7220 %gcfn2(write_bytevector_to_bv, {bytes bv index length}, 3, 0, {
   7221     %stl(a0, bytes)
   7222     %stl(a1, bv)
   7223     %heap_ld(t0, a0, %BV.hdr)
   7224     %shri(t0, t0, 8)
   7225     %stl(t0, length)
   7226     %beqz(a2, &.raw)
   7227 
   7228     %ldl(a0, bv)
   7229     %la(a1, &str_u8_open)
   7230     %li(a2, 4)
   7231     %call(&str_putn)
   7232     %li(t0, 0)
   7233     %stl(t0, index)
   7234     :.loop
   7235     %ldl(t0, index)
   7236     %ldl(t1, length)
   7237     %beq(t0, t1, &.close)
   7238     %beqz(t0, &.value)
   7239     %ldl(a0, bv)
   7240     %li(a1, 32)
   7241     %call(&str_putc)
   7242     :.value
   7243     %ldl(t0, bytes)
   7244     %heap_ld(t1, t0, %BV.data)
   7245     %ldl(t0, index)
   7246     %add(t1, t1, t0)
   7247     %lb(a1, t1, 0)
   7248     %ldl(a0, bv)
   7249     %call(&str_putint)
   7250     %ldl(t0, index)
   7251     %addi(t0, t0, 1)
   7252     %stl(t0, index)
   7253     %b(&.loop)
   7254     :.close
   7255     %ldl(a0, bv)
   7256     %li(a1, 41)
   7257     %gctail(&str_putc)
   7258 
   7259     :.raw
   7260     %ldl(t0, bytes)
   7261     %heap_ld(a1, t0, %BV.data)
   7262     %ldl(a2, length)
   7263     %ldl(a0, bv)
   7264     %gctail(&str_putn)
   7265 })
   7266 
   7267 # write_char_to_bv(char=a0, bv=a1, mode=a2) -> bv. Display emits the
   7268 # character byte; write emits a micro character literal.
   7269 %gcfn2(write_char_to_bv, {bv value}, 1, 0, {
   7270     %stl(a1, bv)
   7271     %untag_char(t0, a0)
   7272     %stl(t0, value)
   7273     %beqz(a2, &.display)
   7274 
   7275     %ldl(a0, bv)
   7276     %la(a1, &str_char_open)
   7277     %li(a2, 2)
   7278     %call(&str_putn)
   7279     %ldl(t0, value)
   7280     %bieq(t0, 32, &.space, t1)
   7281     %bieq(t0, 10, &.newline, t1)
   7282     %bieq(t0, 9,  &.tab, t1)
   7283     %bieq(t0, 13, &.return, t1)
   7284     %beqz(t0, &.null)
   7285     %li(t1, 32)
   7286     %bltu(t0, t1, &.hex)
   7287     %li(t1, 127)
   7288     %bltu(t0, t1, &.direct)
   7289     :.hex
   7290     %ldl(a0, bv)
   7291     %li(a1, 120)                 ; 'x'
   7292     %call(&str_putc)
   7293     %ldl(a0, bv)
   7294     %ldl(a1, value)
   7295     %gctail(&str_puthex)
   7296     :.direct
   7297     %ldl(a0, bv)
   7298     %mov(a1, t0)
   7299     %gctail(&str_putc)
   7300     :.space
   7301     %la(a1, &name_ch_space)
   7302     %li(a2, 5)
   7303     %b(&.named)
   7304     :.newline
   7305     %la(a1, &name_ch_newline)
   7306     %li(a2, 7)
   7307     %b(&.named)
   7308     :.tab
   7309     %la(a1, &name_ch_tab)
   7310     %li(a2, 3)
   7311     %b(&.named)
   7312     :.return
   7313     %la(a1, &name_ch_return)
   7314     %li(a2, 6)
   7315     %b(&.named)
   7316     :.null
   7317     %la(a1, &name_ch_null)
   7318     %li(a2, 4)
   7319     :.named
   7320     %ldl(a0, bv)
   7321     %gctail(&str_putn)
   7322 
   7323     :.display
   7324     %ldl(a0, bv)
   7325     %mov(a1, t0)
   7326     %gctail(&str_putc)
   7327 })
   7328 
   7329 # write_to_bv(val=a0, bv=a1, mode=a2) -> bv (a0). Recursively appends
   7330 # val's printed representation to bv. Strings and characters use their
   7331 # R7RS display/write spellings. Boot2 display keeps bytevectors raw for
   7332 # compiler diagnostics, while write emits #u8(...). Pairs are delegated
   7333 # to write_pair_to_bv so recursion through PAIR has its own frame.
   7334 #
   7335 # Output is treated as a string by callers (display / write / error /
   7336 # format), so all internal append calls go through the str_* family --
   7337 # the result has cap > length and a trailing NUL.
   7338 
   7339 %gcfn2(write_to_bv, {val bv mode pad}, 3, 0, {
   7340     %stl(a0, val)
   7341     %stl(a1, bv)
   7342     %stl(a2, mode)
   7343 
   7344     %tagof(t0, a0)
   7345     %bieq(t0, %TAG.PAIR, &.pair, t1)
   7346     %bieq(t0, %TAG.SYM,  &.sym,  t1)
   7347     %bieq(t0, %TAG.HEAP, &.heap, t1)
   7348     %bieq(t0, %TAG.IMM,  &.imm,  t1)
   7349     %bieq(t0, %TAG.CHAR, &.char, t1)
   7350 
   7351     # Fall-through: FIXNUM (the only remaining tag).
   7352     %ldl(a0, bv)
   7353     %ldl(a1, val)
   7354     %sari(a1, a1, 3)
   7355     %gctail(&str_putint)
   7356 
   7357     :.sym
   7358     %ldl(a0, val)
   7359     %sari(a0, a0, 3)
   7360     %call(&sym_name)
   7361     %mov(a2, a1)
   7362     %mov(a1, a0)
   7363     %ldl(a0, bv)
   7364     %gctail(&str_putn)
   7365 
   7366     :.pair
   7367     %ldl(a0, val)
   7368     %ldl(a1, bv)
   7369     %ldl(a2, mode)
   7370     %gctail(&write_pair_to_bv)
   7371 
   7372     :.char
   7373     %ldl(a0, val)
   7374     %ldl(a1, bv)
   7375     %ldl(a2, mode)
   7376     %gctail(&write_char_to_bv)
   7377 
   7378     :.heap
   7379     %hdr_type(t0, a0)
   7380     %bieq(t0, %HDR.BV,      &.heap_bv,      t1)
   7381     %bieq(t0, %HDR.STRING,  &.heap_string,  t1)
   7382     %bieq(t0, %HDR.CLOSURE, &.heap_closure, t1)
   7383     %bieq(t0, %HDR.PRIM,    &.heap_prim,    t1)
   7384     %bieq(t0, %HDR.TD,      &.heap_td,      t1)
   7385     %bieq(t0, %HDR.REC,     &.heap_rec,     t1)
   7386     %b(&.heap_unknown)
   7387 
   7388     :.heap_bv
   7389     %ldl(a0, val)
   7390     %ldl(a1, bv)
   7391     %ldl(a2, mode)
   7392     %gctail(&write_bytevector_to_bv)
   7393 
   7394     :.heap_string
   7395     %ldl(a0, val)
   7396     %ldl(a1, bv)
   7397     %ldl(a2, mode)
   7398     %gctail(&write_string_to_bv)
   7399 
   7400     :.heap_closure
   7401     %la(a1, &str_closure)
   7402     %li(a2, 10)
   7403     %ldl(a0, bv)
   7404     %gctail(&str_putn)
   7405 
   7406     :.heap_prim
   7407     %la(a1, &str_prim)
   7408     %li(a2, 7)
   7409     %ldl(a0, bv)
   7410     %gctail(&str_putn)
   7411 
   7412     :.heap_td
   7413     %la(a1, &str_td)
   7414     %li(a2, 11)
   7415     %ldl(a0, bv)
   7416     %gctail(&str_putn)
   7417 
   7418     :.heap_rec
   7419     %la(a1, &str_rec)
   7420     %li(a2, 9)
   7421     %ldl(a0, bv)
   7422     %gctail(&str_putn)
   7423 
   7424     :.heap_unknown
   7425     %la(a1, &str_unknown)
   7426     %li(a2, 10)
   7427     %ldl(a0, bv)
   7428     %gctail(&str_putn)
   7429 
   7430     :.imm
   7431     %ldl(a0, val)
   7432     %sari(a0, a0, 3)
   7433     %beqz(a0, &.imm_false)
   7434     %addi(t0, a0, -1)
   7435     %beqz(t0, &.imm_true)
   7436     %addi(t0, a0, -2)
   7437     %beqz(t0, &.imm_nil)
   7438     %addi(t0, a0, -3)
   7439     %beqz(t0, &.imm_unspec)
   7440     %addi(t0, a0, -4)
   7441     %beqz(t0, &.imm_unbound)
   7442     # EOF (idx == 5) is the only remaining IMM.
   7443     %la(a1, &str_eof)
   7444     %li(a2, 5)
   7445     %ldl(a0, bv)
   7446     %gctail(&str_putn)
   7447 
   7448     :.imm_false
   7449     %la(a1, &str_false)
   7450     %li(a2, 2)
   7451     %ldl(a0, bv)
   7452     %gctail(&str_putn)
   7453 
   7454     :.imm_true
   7455     %la(a1, &str_true)
   7456     %li(a2, 2)
   7457     %ldl(a0, bv)
   7458     %gctail(&str_putn)
   7459 
   7460     :.imm_nil
   7461     %la(a1, &str_nil)
   7462     %li(a2, 2)
   7463     %ldl(a0, bv)
   7464     %gctail(&str_putn)
   7465 
   7466     :.imm_unspec
   7467     %la(a1, &str_unspec)
   7468     %li(a2, 8)
   7469     %ldl(a0, bv)
   7470     %gctail(&str_putn)
   7471 
   7472     :.imm_unbound
   7473     %la(a1, &str_unbound)
   7474     %li(a2, 9)
   7475     %ldl(a0, bv)
   7476     %gctail(&str_putn)
   7477 })
   7478 
   7479 # write_pair_to_bv(pair=a0, bv=a1, mode=a2) -> bv (a0). Emits `(elt elt
   7480 # ...)` form, with `( . )` for non-list cdrs (dotted pair). The walker
   7481 # advances `pair` along the spine; cdr's tag determines whether we emit
   7482 # a separator and continue, emit ` . val)` for a dotted tail, or just
   7483 # emit `)` for a proper-list NIL.
   7484 #
   7485 # Locals:
   7486 #   pair  walk
   7487 #   bv  (stable wrapper; reused across recursive calls)
   7488 #   mode
   7489 #   pad
   7490 %gcfn2(write_pair_to_bv, {pair bv mode pad}, 3, 0, {
   7491     %stl(a0, pair)
   7492     %stl(a1, bv)
   7493     %stl(a2, mode)
   7494 
   7495     %ldl(a0, bv)
   7496     %li(a1, 40)
   7497     %call(&str_putc)
   7498 
   7499     :.loop
   7500     %ldl(t0, pair)
   7501     %car(a0, t0)
   7502     %ldl(a1, bv)
   7503     %ldl(a2, mode)
   7504     %call(&write_to_bv)
   7505 
   7506     %ldl(t0, pair)
   7507     %cdr(t0, t0)
   7508     %stl(t0, pair)
   7509 
   7510     %if_nil(t1, t0, &.done)
   7511     %tagof(t1, t0)
   7512     %li(t2, %TAG.PAIR)
   7513     %beq(t1, t2, &.cont)
   7514 
   7515     # Dotted tail: emit ` . ` then write_to_bv(cdr).
   7516     %ldl(a0, bv)
   7517     %li(a1, 32)
   7518     %call(&str_putc)
   7519     %ldl(a0, bv)
   7520     %li(a1, 46)
   7521     %call(&str_putc)
   7522     %ldl(a0, bv)
   7523     %li(a1, 32)
   7524     %call(&str_putc)
   7525     %ldl(a0, pair)
   7526     %ldl(a1, bv)
   7527     %ldl(a2, mode)
   7528     %call(&write_to_bv)
   7529     %b(&.done)
   7530 
   7531     :.cont
   7532     %ldl(a0, bv)
   7533     %li(a1, 32)
   7534     %call(&str_putc)
   7535     %b(&.loop)
   7536 
   7537     :.done
   7538     %ldl(a0, bv)
   7539     %li(a1, 41)
   7540     %gctail(&str_putc)
   7541 })
   7542 
   7543 # value_to_bv(val=a0, mode=a1) -> bv (a0). Allocate an empty NUL-
   7544 # terminated bv and delegate to write_to_bv; helper for display / write
   7545 # / error / format. write_to_bv internally uses str_*, so the result
   7546 # has cap > length and a trailing NUL -- safe to hand to syscalls or
   7547 # runtime_error as a C string.
   7548 
   7549 %gcfn2(value_to_bv, {val mode}, 1, 0, {
   7550     %stl(a0, val)
   7551     %stl(a1, mode)
   7552     %li(a0, 0)
   7553     %call(&str_alloc)
   7554     %mov(a1, a0)
   7555     %ldl(a0, val)
   7556     %ldl(a2, mode)
   7557     %gctail(&write_to_bv)
   7558 })
   7559 
   7560 # (display val) and (write val): build the printed representation in a
   7561 # fresh bv, sys_write the raw bytes to fd 1, return UNSPEC. Partial
   7562 # writes are not retried -- libp1pp's wrapper streams its own buffer
   7563 # but the kernel may chunk a giant single write; in practice
   7564 # scheme1 outputs are short and we accept the simple path.
   7565 %fn(prim_display_entry, 0, {
   7566     %car(a0, a0)
   7567     %li(a1, 0)
   7568     %call(&value_to_bv)
   7569     %heap_ld(a1, a0, %BV.data)
   7570     %heap_ld(a2, a0, %BV.hdr)
   7571     %shri(a2, a2, 8)
   7572     %li(a0, 1)
   7573     %call(&sys_write)
   7574     %li(a0, %imm_val(%IMM.UNSPEC))
   7575 })
   7576 
   7577 %fn(prim_write_entry, 0, {
   7578     %car(a0, a0)
   7579     %li(a1, 1)
   7580     %call(&value_to_bv)
   7581     %heap_ld(a1, a0, %BV.data)
   7582     %heap_ld(a2, a0, %BV.hdr)
   7583     %shri(a2, a2, 8)
   7584     %li(a0, 1)
   7585     %call(&sys_write)
   7586     %li(a0, %imm_val(%IMM.UNSPEC))
   7587 })
   7588 
   7589 # (error msg-bv irritant ...). Builds `scheme1: error: <msg> <irr> ...`
   7590 # in a string-bv (irritants joined by single spaces, all rendered with
   7591 # display semantics) and tails into runtime_error. str_alloc + str_*
   7592 # guarantee cap > length and a trailing NUL, making the bv's data_ptr
   7593 # a valid C string for panic's eprint_cstr.
   7594 #
   7595 # Locals:
   7596 #   walk  (initially args; advances over irritants)
   7597 #   bv
   7598 %gcfn2(prim_error_entry, {walk bv}, 3, 0, {
   7599     %stl(a0, walk)
   7600 
   7601     %li(a0, 0)
   7602     %call(&str_alloc)
   7603     %stl(a0, bv)
   7604 
   7605     %la(a1, &str_error_prefix)
   7606     %li(a2, 16)
   7607     %ldl(a0, bv)
   7608     %call(&str_putn)
   7609 
   7610     # First arg (the message) goes through write_to_bv with display mode.
   7611     %ldl(t0, walk)
   7612     %car(a0, t0)
   7613     %ldl(a1, bv)
   7614     %li(a2, 0)
   7615     %call(&write_to_bv)
   7616 
   7617     %ldl(t0, walk)
   7618     %cdr(t0, t0)
   7619     %stl(t0, walk)
   7620 
   7621     :.loop
   7622     %ldl(t0, walk)
   7623     %if_nil(t1, t0, &.done)
   7624 
   7625     %ldl(a0, bv)
   7626     %li(a1, 32)
   7627     %call(&str_putc)
   7628 
   7629     %ldl(t0, walk)
   7630     %car(a0, t0)
   7631     %ldl(a1, bv)
   7632     %li(a2, 0)
   7633     %call(&write_to_bv)
   7634 
   7635     %ldl(t0, walk)
   7636     %cdr(t0, t0)
   7637     %stl(t0, walk)
   7638     %b(&.loop)
   7639 
   7640     :.done
   7641     %ldl(t0, bv)
   7642     %heap_ld(a0, t0, %BV.data)
   7643     %gctail(&runtime_error)
   7644 })
   7645 
   7646 # (format template-bv arg ...). Walks the template bv byte by byte;
   7647 # `~X` consumes the next byte as a directive: a (display), s (write),
   7648 # d (decimal fixnum), x (lowercase hex fixnum, signed), % (newline),
   7649 # ~ (literal tilde). Unknown specs pass through verbatim. Returns the
   7650 # assembled bv; the caller decides how to consume it (e.g.
   7651 # (display (format ...))).
   7652 #
   7653 # Locals:
   7654 #   out  bv
   7655 #   template  bv
   7656 #   args  walk
   7657 #   idx  (current byte offset into template)
   7658 %gcfn2(prim_format_entry, {out template args idx}, 7, 0, {
   7659     %stl(a0, args)             ; spill incoming args while we set up
   7660 
   7661     %li(a0, 0)
   7662     %call(&str_alloc)
   7663     %stl(a0, out)
   7664 
   7665     %ldl(t0, args)
   7666     %car(t1, t0)
   7667     %stl(t1, template)
   7668     %cdr(t0, t0)
   7669     %stl(t0, args)
   7670 
   7671     %li(t0, 0)
   7672     %stl(t0, idx)
   7673 
   7674     :.loop
   7675     %ldl(t1, template)
   7676     %heap_ld(t2, t1, %BV.hdr)
   7677     %shri(t2, t2, 8)            ; template length
   7678     %ldl(t0, idx)
   7679     %beq(t0, t2, &.done)
   7680 
   7681     %heap_ld(a3, t1, %BV.data)
   7682     %add(a3, a3, t0)
   7683     %lb(a3, a3, 0)               ; byte = template.data[idx]
   7684 
   7685     %addi(t1, a3, -126)         ; '~'
   7686     %beqz(t1, &.tilde)
   7687 
   7688     # Plain byte: emit and advance.
   7689     %ldl(a0, out)
   7690     %mov(a1, a3)
   7691     %call(&str_putc)
   7692     %ldl(t0, idx)
   7693     %addi(t0, t0, 1)
   7694     %stl(t0, idx)
   7695     %b(&.loop)
   7696 
   7697     :.tilde
   7698     %ldl(t0, idx)
   7699     %addi(t0, t0, 1)
   7700     %ldl(t1, template)
   7701     %heap_ld(t2, t1, %BV.hdr)
   7702     %shri(t2, t2, 8)
   7703     %beq(t0, t2, &.tilde_lit)
   7704 
   7705     %heap_ld(t1, t1, %BV.data)
   7706     %add(t1, t1, t0)
   7707     %lb(a3, t1, 0)               ; spec
   7708 
   7709     %addi(t0, t0, 1)             ; advance past spec
   7710     %stl(t0, idx)
   7711 
   7712     %addi(t1, a3, -97)          ; 'a'
   7713     %beqz(t1, &.spec_a)
   7714     %addi(t1, a3, -115)         ; 's'
   7715     %beqz(t1, &.spec_s)
   7716     %addi(t1, a3, -100)         ; 'd'
   7717     %beqz(t1, &.spec_d)
   7718     %addi(t1, a3, -120)         ; 'x'
   7719     %beqz(t1, &.spec_x)
   7720     %addi(t1, a3, -37)          ; '%'
   7721     %beqz(t1, &.spec_pct)
   7722     %addi(t1, a3, -126)         ; '~'
   7723     %beqz(t1, &.spec_tilde)
   7724 
   7725     # Unknown directive: emit `~` then the spec byte verbatim. Re-read
   7726     # the spec byte from the template since str_putc may clobber a3.
   7727     %ldl(a0, out)
   7728     %li(a1, 126)
   7729     %call(&str_putc)
   7730     %ldl(t0, template)
   7731     %heap_ld(t1, t0, %BV.data)
   7732     %ldl(t0, idx)
   7733     %addi(t0, t0, -1)
   7734     %add(t1, t1, t0)
   7735     %lb(a1, t1, 0)
   7736     %ldl(a0, out)
   7737     %call(&str_putc)
   7738     %b(&.loop)
   7739 
   7740     :.tilde_lit
   7741     # `~` at end of template: emit literal `~` and finish next iter.
   7742     %ldl(a0, out)
   7743     %li(a1, 126)
   7744     %call(&str_putc)
   7745     %ldl(t0, idx)
   7746     %addi(t0, t0, 1)
   7747     %stl(t0, idx)
   7748     %b(&.loop)
   7749 
   7750     :.spec_a
   7751     %ldl(t0, args)
   7752     %car(a0, t0)
   7753     %cdr(t0, t0)
   7754     %stl(t0, args)
   7755     %ldl(a1, out)
   7756     %li(a2, 0)
   7757     %call(&write_to_bv)
   7758     %b(&.loop)
   7759 
   7760     :.spec_s
   7761     %ldl(t0, args)
   7762     %car(a0, t0)
   7763     %cdr(t0, t0)
   7764     %stl(t0, args)
   7765     %ldl(a1, out)
   7766     %li(a2, 1)
   7767     %call(&write_to_bv)
   7768     %b(&.loop)
   7769 
   7770     :.spec_d
   7771     %ldl(t0, args)
   7772     %car(t1, t0)
   7773     %cdr(t0, t0)
   7774     %stl(t0, args)
   7775     %sari(a1, t1, 3)
   7776     %ldl(a0, out)
   7777     %call(&str_putint)
   7778     %b(&.loop)
   7779 
   7780     :.spec_x
   7781     %ldl(t0, args)
   7782     %car(t1, t0)
   7783     %cdr(t0, t0)
   7784     %stl(t0, args)
   7785     %sari(a1, t1, 3)
   7786     %ldl(a0, out)
   7787     %call(&str_puthex)
   7788     %b(&.loop)
   7789 
   7790     :.spec_pct
   7791     %ldl(a0, out)
   7792     %li(a1, 10)
   7793     %call(&str_putc)
   7794     %b(&.loop)
   7795 
   7796     :.spec_tilde
   7797     %ldl(a0, out)
   7798     %li(a1, 126)
   7799     %call(&str_putc)
   7800     %b(&.loop)
   7801 
   7802     :.done
   7803     %ldl(a0, out)
   7804 })
   7805 
   7806 # =========================================================================
   7807 # Syscall primitives
   7808 # =========================================================================
   7809 #
   7810 # Each syscall primitive untags the args list, calls a thin libp1pp- or
   7811 # scheme1-local syscall wrapper, and routes the raw return through
   7812 # wrap_syscall_result: r >= 0 -> (#t . r), r < 0 -> (#f . -r).
   7813 #
   7814 # Bytevector args (paths, buffers) are passed by their raw data_ptr (slot
   7815 # +5 from the tagged wrapper). For syscalls that read data_ptr as a C
   7816 # string (paths, argv elements), the caller must produce the bv via the
   7817 # str_* family so cap > length and data[length] == 0. Callers that only
   7818 # expose the bv as a (data_ptr, count) pair (sys-read, sys-write buffers)
   7819 # can pass plain bytevectors -- no NUL needed.
   7820 
   7821 # wrap_syscall_result(raw=a0) -> (#t . r) or (#f . errno).
   7822 
   7823 %fn2(wrap_syscall_result, {raw pad}, {
   7824     %stl(a0, raw)
   7825     %bltz(a0, &.err)
   7826     %mkfix(a1, a0)
   7827     %li(a0, %imm_val(%IMM.TRUE))
   7828     %tail(&cons)
   7829 
   7830     :.err
   7831     %ldl(t0, raw)
   7832     %li(t1, 0)
   7833     %sub(t0, t1, t0)
   7834     %mkfix(a1, t0)
   7835     %li(a0, %imm_val(%IMM.FALSE))
   7836     %tail(&cons)
   7837 })
   7838 
   7839 # sys_openat(dirfd=a0, path=a1, flags=a2, mode=a3) -> r (a0). Leaf.
   7840 :sys_openat
   7841     %mov(t0, a3)
   7842     %mov(a3, a2)
   7843     %mov(a2, a1)
   7844     %mov(a1, a0)
   7845     %li(a0, %p1_sys_openat)
   7846     %syscall
   7847     %ret
   7848 
   7849 # sys_clone() -> r (a0). Linux clone(SIGCHLD, 0, 0, 0, 0) -- fork-style.
   7850 # Saves and restores s0 around the syscall because %p1_syscall reads s0
   7851 # as the 5th OS-syscall argument.
   7852 
   7853 %fn2(sys_clone, {saved_s0 pad}, {
   7854     %stl(s0, saved_s0)
   7855     %li(s0, 0)
   7856 
   7857     %li(a1, 17)
   7858     %li(a2, 0)
   7859     %li(a3, 0)
   7860     %li(t0, 0)
   7861     %li(a0, %p1_sys_clone)
   7862     %syscall
   7863 
   7864     %ldl(s0, saved_s0)
   7865 })
   7866 
   7867 # sys_execve(path=a0, argv=a1, envp=a2) -> -errno (a0). Only returns on
   7868 # failure; on success the new image takes over.
   7869 :sys_execve
   7870     %mov(a3, a2)
   7871     %mov(a2, a1)
   7872     %mov(a1, a0)
   7873     %li(a0, %p1_sys_execve)
   7874     %syscall
   7875     %ret
   7876 
   7877 # sys_spawn(path=a0, argv=a1) -> r (a0). Atomic clone+execve, single
   7878 # syscall: kernel saves parent state, swaps user pool with no copy,
   7879 # loads the ELF, builds the user stack, and erets into the child. The
   7880 # parent's spawn() returns child_pid only after the child exit_groups.
   7881 # Provided by the seed kernel (private syscall 1024). On Linux this
   7882 # number is unmapped so the kernel returns -ENOSYS, which the prelude
   7883 # uses to detect environment and fall back to sys_clone+sys_execve.
   7884 :sys_spawn
   7885     %mov(a2, a1)
   7886     %mov(a1, a0)
   7887     %li(a0, %p1_sys_spawn)
   7888     %syscall
   7889     %ret
   7890 
   7891 # sys_waitid(idtype=a0, id=a1, infop=a2, options=a3) -> r (a0). Leaf.
   7892 :sys_waitid
   7893     %mov(t0, a3)
   7894     %mov(a3, a2)
   7895     %mov(a2, a1)
   7896     %mov(a1, a0)
   7897     %li(a0, %p1_sys_waitid)
   7898     %syscall
   7899     # Canonicalize RV32's 4-byte-aligned siginfo child-status field to the
   7900     # offset used by the shared Scheme prelude (24 on the 64-bit ABIs).
   7901     %lb(t0, a3, %p1_waitid_status_off)
   7902     %sb(t0, a3, 24)
   7903     %ret
   7904 
   7905 # build_execve_argv(list=a0) -> raw NULL-terminated array (a0).
   7906 # Walks `list` (cons-list of bytevectors), allocates a native pointer array,
   7907 # writes each bv's data_ptr, terminates with NULL.
   7908 #
   7909 # Locals:
   7910 #   list
   7911 #   count
   7912 #   array  ptr (raw)
   7913 %gcfn2(build_execve_argv, {list count array}, 1, 0, {
   7914     %stl(a0, list)
   7915     %call(&list_length)     ; clobbers a0 -> count
   7916     %stl(a0, count)
   7917 
   7918     %addi(a0, a0, 1)
   7919     %shli(a0, a0, %p1_word_shift)
   7920     %call(&alloc_bytes)
   7921     %stl(a0, array)
   7922 
   7923     %ldl(t0, list)
   7924     %ldl(t1, array)
   7925 
   7926     :.fill_loop
   7927     %if_nil(t2, t0, &.fill_done)
   7928     %car(a3, t0)
   7929     %heap_ld(a2, a3, %BV.data)
   7930     %st(a2, t1, 0)
   7931     %addi(t1, t1, %p1_word_bytes)
   7932     %cdr(t0, t0)
   7933     %b(&.fill_loop)
   7934 
   7935     :.fill_done
   7936     %li(t2, 0)
   7937     %st(t2, t1, 0)
   7938 
   7939     %ldl(a0, array)
   7940 })
   7941 
   7942 # (sys-read fd buf offset count). Passes (buf.data_ptr + offset) to the
   7943 # kernel; offset lets callers read into the middle of a bv without first
   7944 # slicing/copying.
   7945 %fn(prim_sys_read_entry, 0, {
   7946     %args4(t0, t1, t2, a3, a0)
   7947     %sari(t0, t0, 3)        ; fd
   7948     %heap_ld(t1, t1, %BV.data)  ; buf data ptr
   7949     %sari(t2, t2, 3)        ; offset
   7950     %add(t1, t1, t2)        ; data_ptr + offset
   7951     %sari(t2, a3, 3)        ; count
   7952     %mov(a0, t0)
   7953     %mov(a1, t1)
   7954     %mov(a2, t2)
   7955     %call(&sys_read)
   7956     %tail(&wrap_syscall_result)
   7957 })
   7958 
   7959 # (sys-write fd buf offset count). Passes (buf.data_ptr + offset) to the
   7960 # kernel; offset lets callers retry the unwritten tail of a partial
   7961 # write without bytevector-copy.
   7962 %fn(prim_sys_write_entry, 0, {
   7963     %args4(t0, t1, t2, a3, a0)
   7964     %sari(t0, t0, 3)        ; fd
   7965     %heap_ld(t1, t1, %BV.data)  ; buf data ptr
   7966     %sari(t2, t2, 3)        ; offset
   7967     %add(t1, t1, t2)        ; data_ptr + offset
   7968     %sari(t2, a3, 3)        ; count
   7969     %mov(a0, t0)
   7970     %mov(a1, t1)
   7971     %mov(a2, t2)
   7972     %call(&sys_write)
   7973     %tail(&wrap_syscall_result)
   7974 })
   7975 
   7976 # (sys-close fd)
   7977 %fn(prim_sys_close_entry, 0, {
   7978     %car_fix(a0, a0)
   7979     %call(&sys_close)
   7980     %tail(&wrap_syscall_result)
   7981 })
   7982 
   7983 # (sys-openat dirfd path flags mode)
   7984 %fn(prim_sys_openat_entry, 0, {
   7985     %args4(t0, t1, t2, a3, a0)
   7986     %sari(t0, t0, 3)        ; dirfd
   7987     %heap_ld(t1, t1, %BV.data)  ; path data_ptr
   7988     %sari(t2, t2, 3)        ; flags
   7989     %sari(a3, a3, 3)        ; mode
   7990     %mov(a0, t0)
   7991     %mov(a1, t1)
   7992     %mov(a2, t2)
   7993     %call(&sys_openat)
   7994     %tail(&wrap_syscall_result)
   7995 })
   7996 
   7997 # (sys-clone). Linux POSIX-style fork; only used as a fallback path on
   7998 # Linux since the seed kernel doesn't implement clone (it offers
   7999 # sys-spawn instead).
   8000 %fn(prim_sys_clone_entry, 0, {
   8001     %call(&sys_clone)
   8002     %tail(&wrap_syscall_result)
   8003 })
   8004 
   8005 # (sys-execve path argv-list)
   8006 
   8007 %gcfn2(prim_sys_execve_entry, {path pad}, 1, 0, {
   8008     %args2(t0, a0, a0)      ; t0 = path bv, a0 = argv-list
   8009     %stl(t0, path)
   8010     %call(&build_execve_argv)
   8011     %mov(a1, a0)
   8012     %ldl(a0, path)
   8013     %heap_ld(a0, a0, %BV.data)  ; path data ptr
   8014     %li(a2, 0)
   8015     %call(&sys_execve)
   8016     %gctail(&wrap_syscall_result)
   8017 })
   8018 
   8019 # (sys-spawn path argv-list). Same calling convention as sys-execve, but
   8020 # wraps the seed kernel's atomic spawn syscall: returns (#t . child-pid)
   8021 # after the child has exit_grouped (the kernel suspends the parent for
   8022 # the lifetime of the child), or (#f . -errno) on failure (notably
   8023 # -ENOSYS=38 on Linux, which the prelude probes for at init time).
   8024 %gcfn2(prim_sys_spawn_entry, {path pad}, 1, 0, {
   8025     %args2(t0, a0, a0)      ; t0 = path bv, a0 = argv-list
   8026     %stl(t0, path)
   8027     %call(&build_execve_argv)
   8028     %mov(a1, a0)
   8029     %ldl(a0, path)
   8030     %heap_ld(a0, a0, %BV.data)  ; path data ptr
   8031     %call(&sys_spawn)
   8032     %gctail(&wrap_syscall_result)
   8033 })
   8034 
   8035 # (sys-waitid idtype id infop options)
   8036 %fn(prim_sys_waitid_entry, 0, {
   8037     %args4(t0, t1, t2, a3, a0)
   8038     %sari(t0, t0, 3)        ; idtype
   8039     %sari(t1, t1, 3)        ; id
   8040     %heap_ld(t2, t2, %BV.data)  ; infop bv data ptr
   8041     %sari(a3, a3, 3)        ; options
   8042     %mov(a0, t0)
   8043     %mov(a1, t1)
   8044     %mov(a2, t2)
   8045     %call(&sys_waitid)
   8046     %tail(&wrap_syscall_result)
   8047 })
   8048 
   8049 # (sys-argv) -> list of bytevectors. Walks saved_argv, strlen-ing each
   8050 # NUL-terminated entry into a fresh bytevector and consing them in order
   8051 # via the head/tail trick.
   8052 #
   8053 # Locals:
   8054 #   argv  ptr (advancing one native pointer per iteration)
   8055 #   count  remaining (decrementing from saved_argc)
   8056 #   head
   8057 #   tail
   8058 #   bv
   8059 %gcfn2(prim_sys_argv_entry, {argv count head tail bv}, 28, 0, {
   8060     %ld_global(t0, &saved_argv)
   8061     %stl(t0, argv)
   8062     %ld_global(t0, &saved_argc)
   8063     %stl(t0, count)
   8064     %li(t0, %imm_val(%IMM.NIL))
   8065     %stl(t0, head)
   8066     %stl(t0, tail)
   8067 
   8068     :.loop
   8069     %ldl(t0, count)
   8070     %beqz(t0, &.done)
   8071 
   8072     # len = strlen(*argv)
   8073     %ldl(t0, argv)
   8074     %ld(a0, t0, 0)
   8075     %call(&libp1pp__strlen)
   8076 
   8077     # bv = str_alloc(len). argv entries flow into syscalls (sys-openat,
   8078     # sys-execve) that read data_ptr as a C string, so the trailing NUL
   8079     # is required.
   8080     %call(&str_alloc)
   8081     %stl(a0, bv)
   8082 
   8083     # memcpy(bv.data_ptr, *argv, len-from-bv-hdr).
   8084     %ldl(t0, bv)
   8085     %heap_ld(a0, t0, %BV.data)
   8086     %ldl(t1, argv)
   8087     %ld(a1, t1, 0)
   8088     %heap_ld(t1, t0, %BV.hdr)
   8089     %shri(a2, t1, 8)
   8090     %call(&memcpy)
   8091 
   8092     # cell = cons(bv, NIL); append to list head/tail.
   8093     %ldl(a0, bv)
   8094     %li(a1, %imm_val(%IMM.NIL))
   8095     %call(&cons)
   8096 
   8097     %ldl(t0, head)
   8098     %if_nil(t1, t0, &.first)
   8099     %ldl(t0, tail)
   8100     %set_cdr(a0, t0)
   8101     %stl(a0, tail)
   8102     %b(&.advance)
   8103 
   8104     :.first
   8105     %stl(a0, head)
   8106     %stl(a0, tail)
   8107 
   8108     :.advance
   8109     %ldl(t0, argv)
   8110     %addi(t0, t0, %p1_word_bytes)
   8111     %stl(t0, argv)
   8112     %ldl(t0, count)
   8113     %addi(t0, t0, -1)
   8114     %stl(t0, count)
   8115     %b(&.loop)
   8116 
   8117     :.done
   8118     %ldl(a0, head)
   8119 })
   8120 
   8121 # (eof? x). The `eof` value itself is bound at startup in p1_main as a
   8122 # direct global -> IMM.EOF, not via a primitive thunk.
   8123 :prim_eofq_entry
   8124 .scope
   8125     %car(t0, a0)
   8126     %li(t1, %imm_val(%IMM.EOF))
   8127     %li(a0, %imm_val(%IMM.FALSE))
   8128     %bne(t0, t1, &.end)
   8129     %li(a0, %imm_val(%IMM.TRUE))
   8130     :.end
   8131     %ret
   8132 .endscope
   8133 
   8134 # (heap-usage) -> tagged fixnum: currently allocated managed bytes,
   8135 # including the 16-byte header of every live allocation.
   8136 :prim_heap_usage_entry
   8137     %ld_global(a0, &heap_allocated)
   8138     %mkfix(a0, a0)
   8139     %ret
   8140 
   8141 # (target-word-bytes) / (target-word-bits) expose the active P1 data model
   8142 # to portable Scheme-hosted tools such as cc.scm. Arguments are ignored,
   8143 # matching the other zero-argument introspection primitives.
   8144 :prim_target_word_bytes_entry
   8145     %li(a0, %p1_word_bytes)
   8146     %mkfix(a0, a0)
   8147     %ret
   8148 
   8149 :prim_target_word_bits_entry
   8150     %li(a0, %p1_word_bits)
   8151     %mkfix(a0, a0)
   8152     %ret
   8153 
   8154 # (collect-garbage) -> unspecified.  The primitive's argument list is
   8155 # intentionally ignored, so it does not retain otherwise unreachable data.
   8156 %fn(prim_collect_garbage_entry, 0, {
   8157     %call(&gc_collect)
   8158     %li(a0, %imm_val(%IMM.UNSPEC))
   8159 })
   8160 
   8161 # Record introspection. Surfaces the unsafe %record-* helpers (heap
   8162 # layout: [HDR.REC][td][f0..fN-1], field i at tagged + 13 + 8*i;
   8163 # nfields lives at TD's offset 13 raw). All primitives below trust
   8164 # their inputs -- no bounds check, no kind check on record-ref /
   8165 # record-set! / record-td.
   8166 
   8167 # (record? obj) -> bool. True iff obj is HEAP-tagged with HDR.REC.
   8168 :prim_recordq_entry
   8169 .scope
   8170     %car(t0, a0)
   8171     %li(a0, %imm_val(%IMM.FALSE))
   8172     %tagof(t1, t0)
   8173     %li(t2, %TAG.HEAP)
   8174     %bne(t1, t2, &.end)
   8175     %hdr_type(t1, t0)
   8176     %li(t2, %HDR.REC)
   8177     %bne(t1, t2, &.end)
   8178     %li(a0, %imm_val(%IMM.TRUE))
   8179     :.end
   8180     %ret
   8181 .endscope
   8182 
   8183 # (record-td rec) -> td. Reads the TD slot from the record header. No
   8184 # kind check; caller is expected to gate with record? if needed.
   8185 :prim_record_td_entry
   8186     %car(t0, a0)
   8187     %heap_ld(a0, t0, %REC.td)
   8188     %ret
   8189 
   8190 # (record-ref rec idx) -> field value. idx is a tagged fixnum; since
   8191 # tagged_fixnum = raw_idx * 8 (fixnum tag bits are 0), the byte offset
   8192 # is exactly idx + 13 from the tagged record pointer. No bounds check.
   8193 :prim_record_ref_entry
   8194     %args2(t0, t1, a0)         ; t0=rec, t1=idx (tagged fixnum = raw*8)
   8195     %addi(t0, t0, 13)
   8196     %add(t0, t0, t1)
   8197     %ld(a0, t0, 0)
   8198     %ret
   8199 
   8200 # (record-set! rec idx val) -> unspec. In-place store at slot idx.
   8201 :prim_record_set_bang_entry
   8202 .scope
   8203     %car(t0, a0)               ; rec
   8204     %cdr(a0, a0)
   8205     %car(t1, a0)               ; idx (tagged fixnum)
   8206     %cdr(a0, a0)
   8207     %car(t2, a0)               ; val
   8208     %addi(t0, t0, 13)
   8209     %add(t0, t0, t1)
   8210     %st(t2, t0, 0)
   8211     %li(a0, %imm_val(%IMM.UNSPEC))
   8212     %ret
   8213 .endscope
   8214 
   8215 # (make-record/td td) -> fresh record allocated in the current heap.
   8216 # Reads td.nfields, allocates 16 + nfields*8 bytes with HDR.REC, sets
   8217 # the td slot, and zero-fills field slots to IMM.UNSPEC. Mirrors
   8218 # eval_define_record_type's ctor allocation but driven by the TD's
   8219 # nfields rather than a runtime args list. Used by deep-copy as a
   8220 # pre-fill stand-in before recursive slot promotion.
   8221 #
   8222 # Locals:
   8223 #   td      (the TD pointer; saved across alloc_hdr)
   8224 #   record  (the new record pointer)
   8225 %gcfn2(prim_make_record_td_entry, {td record}, 3, 0, {
   8226     %car(t0, a0)               ; td
   8227     %stl(t0, td)
   8228 
   8229     %heap_ld(a0, t0, %TD.nfields)  ; raw nfields
   8230     %shli(a0, a0, 3)               ; nfields * 8
   8231     %addi(a0, a0, 16)              ; + REC header (hdr + td slot)
   8232     %li(a1, %HDR.REC)
   8233     %call(&alloc_hdr)
   8234     %stl(a0, record)
   8235 
   8236     %ldl(t0, td)
   8237     %heap_st(t0, a0, %REC.td)
   8238 
   8239     # Zero-fill field slots to IMM.UNSPEC. Cursor starts at first slot
   8240     # (tagged + 13); count = nfields read again from the TD.
   8241     %heap_ld(t0, t0, %TD.nfields)
   8242     %addi(t1, a0, 13)
   8243     %li(t2, %imm_val(%IMM.UNSPEC))
   8244 
   8245     :.fill_loop
   8246     %beqz(t0, &.fill_done)
   8247     %st(t2, t1, 0)
   8248     %addi(t1, t1, 8)
   8249     %addi(t0, t0, -1)
   8250     %b(&.fill_loop)
   8251 
   8252     :.fill_done
   8253     %ldl(a0, record)
   8254 })
   8255 
   8256 # (td-nfields td) -> tagged fixnum count of fields.
   8257 :prim_td_nfields_entry
   8258     %car(t0, a0)
   8259     %heap_ld(a0, t0, %TD.nfields)  ; raw count
   8260     %mkfix(a0, a0)
   8261     %ret
   8262 
   8263 # (td-name td) -> symbol bound at define-record-type time.
   8264 :prim_td_name_entry
   8265     %car(t0, a0)
   8266     %heap_ld(a0, t0, %TD.name)
   8267     %ret
   8268 
   8269 # Debug primitives. UNSAFE: peek-memory-u8 dereferences arbitrary addresses.
   8270 # Intended for diagnosing heap-layout bugs from scheme1 user code; not
   8271 # part of the surface contract.
   8272 
   8273 # (tagged-value obj) -> fixnum. Returns the raw byte address of obj
   8274 # with tag bits masked off, encoded as a tagged fixnum so format /
   8275 # display can print it. Pass the result back into peek-memory-u8 to read raw
   8276 # bytes. For non-pointer values (fixnums, immediates, syms) the masked
   8277 # value is small but still encodable; the result is meaningful only for
   8278 # heap-tagged inputs.
   8279 :prim_tagged_value_entry
   8280     %car(t0, a0)
   8281     %li(t1, -8)
   8282     %and(t0, t0, t1)
   8283     %mkfix(a0, t0)
   8284     %ret
   8285 
   8286 # (peek-memory-u8 addr) -> fixnum. Reads one byte at the given raw byte
   8287 # address (tagged fixnum input, untagged inside). UNSAFE: no bounds
   8288 # check; a wild address segfaults the process.
   8289 :prim_peek_memory_u8_entry
   8290     %car(t0, a0)
   8291     %sari(t0, t0, 3)
   8292     %lb(a0, t0, 0)
   8293     %mkfix(a0, a0)
   8294     %ret
   8295 
   8296 # (values . xs) -- multiple-values producer. Single-arg case returns the
   8297 # arg unchanged so (values x) is interchangeable with x in any 1-value
   8298 # context; 0 or 2+ args materialize an MV-pack.
   8299 :prim_values_entry
   8300 .scope
   8301     %if_nil(t0, a0, &.pack)
   8302     %cdr(t0, a0)
   8303     %if_nil(t1, t0, &.single)
   8304     :.pack
   8305     %b(&list_to_mv)
   8306     :.single
   8307     %car(a0, a0)
   8308     %ret
   8309 .endscope
   8310 
   8311 # (call-with-values producer consumer) -- apply producer to no args, then
   8312 # normalize its result (via mv_to_list) and tail-apply the consumer to the
   8313 # resulting argument list.
   8314 #
   8315 # Locals:
   8316 #   consumer  (saved across apply(producer) and mv_to_list)
   8317 %gcfn2(prim_call_with_values_entry, {consumer pad}, 1, 0, {
   8318     %args2(t0, t1, a0)              ; t0 = producer, t1 = consumer
   8319     %stl(t1, consumer)
   8320 
   8321     %mov(a0, t0)
   8322     %li(a1, %imm_val(%IMM.NIL))
   8323     %call(&apply)
   8324 
   8325     %call(&mv_to_list)
   8326 
   8327     %mov(a1, a0)
   8328     %ldl(a0, consumer)
   8329     %gctail(&apply)
   8330 })
   8331 
   8332 # =========================================================================
   8333 # Startup -- heap_init
   8334 # =========================================================================
   8335 
   8336 # heap_init() -> none. Initializes the physical heap chain, free list,
   8337 # accounting, and bounded exact-root stack. Leaf.
   8338 :heap_init
   8339     %ld_global(t0, &heap_buf_ptr)
   8340     %alignup(t0, t0, 8, t1)
   8341     %st_global(t0, &heap_base, t1)
   8342     %st_global(t0, &heap_tail, t1)
   8343 
   8344     %ld_global(t0, &heap_buf_ptr)
   8345     %li(t1, %HEAP_CAP_BYTES)
   8346     %add(t0, t0, t1)
   8347     %st_global(t0, &heap_end, t1)
   8348 
   8349     %li(t0, 0)
   8350     %st_global(t0, &gc_free_list, t1)
   8351     %st_global(t0, &gc_mark_worklist, t1)
   8352     %st_global(t0, &heap_allocated, t1)
   8353 
   8354     %ld_global(t0, &gc_root_buf_ptr)
   8355     %st_global(t0, &gc_root_next, t1)
   8356     %li(t1, (* %GC_ROOT_CAP_FRAMES %GC_ROOT_FRAME_BYTES))
   8357     %add(t0, t0, t1)
   8358     %st_global(t0, &gc_root_end, t1)
   8359 
   8360     %ret
   8361 
   8362 # Sentinel: marks the boundary between executable text and rodata.
   8363 # Read by scripts/disasm-elf.sh (via scripts/m1-symbols.py) to bound
   8364 # disassembly so trailing strings don't decode as bogus instructions.
   8365 :_text_end
   8366 
   8367 .align 8
   8368 
   8369 # Primitive surface names.
   8370 :name_sys_exit    %cstr8("sys-exit")
   8371 :name_cons        %cstr8("cons")
   8372 :name_car         %cstr8("car")
   8373 :name_cdr         %cstr8("cdr")
   8374 :name_nullq       %cstr8("null?")
   8375 :name_pairq       %cstr8("pair?")
   8376 :name_stringq     %cstr8("string?")
   8377 :name_bytevectorq %cstr8("bytevector?")
   8378 :name_bytesq      %cstr8("bytes?")
   8379 :name_set_car     %cstr8("set-car!")
   8380 :name_set_cdr     %cstr8("set-cdr!")
   8381 :name_length      %cstr8("length")
   8382 :name_list_ref    %cstr8("list-ref")
   8383 :name_assq        %cstr8("assq")
   8384 :name_assoc       %cstr8("assoc")
   8385 :name_reverse     %cstr8("reverse")
   8386 :name_str_to_sym  %cstr8("string->symbol")
   8387 :name_sym_to_str  %cstr8("symbol->string")
   8388 :name_num_to_str  %cstr8("number->string")
   8389 :name_str_to_num  %cstr8("string->number")
   8390 :name_bv_append   %cstr8("bytevector-append")
   8391 :name_booleanq    %cstr8("boolean?")
   8392 :name_integerq    %cstr8("integer?")
   8393 :name_charq       %cstr8("char?")
   8394 :name_char_to_integer %cstr8("char->integer")
   8395 :name_integer_to_char %cstr8("integer->char")
   8396 :name_symbolq     %cstr8("symbol?")
   8397 :name_procedureq  %cstr8("procedure?")
   8398 :name_zeroq       %cstr8("zero?")
   8399 :name_not         %cstr8("not")
   8400 :name_eqq         %cstr8("eq?")
   8401 :name_equal       %cstr8("equal?")
   8402 :name_plus        %cstr8("+")
   8403 :name_minus       %cstr8("-")
   8404 :name_mult        %cstr8("*")
   8405 :name_eq          %cstr8("=")
   8406 :name_lt          %cstr8("<")
   8407 :name_gt          %cstr8(">")
   8408 :name_quotient    %cstr8("quotient")
   8409 :name_remainder   %cstr8("remainder")
   8410 :name_bit_and     %cstr8("bit-and")
   8411 :name_bit_or      %cstr8("bit-or")
   8412 :name_bit_xor     %cstr8("bit-xor")
   8413 :name_bit_not     %cstr8("bit-not")
   8414 :name_arith_shift %cstr8("arithmetic-shift")
   8415 :name_apply       %cstr8("apply")
   8416 :name_make_bv     %cstr8("make-bytevector")
   8417 :name_make_string %cstr8("make-string")
   8418 :name_bv_length   %cstr8("bytevector-length")
   8419 :name_string_length %cstr8("string-length")
   8420 :name_string_ref  %cstr8("string-ref")
   8421 :name_string_set  %cstr8("string-set!")
   8422 :name_bv_u8_ref   %cstr8("bytevector-u8-ref")
   8423 :name_bv_u8_set   %cstr8("bytevector-u8-set!")
   8424 :name_bv_copy     %cstr8("bytevector-copy")
   8425 :name_bv_copy_b   %cstr8("bytevector-copy!")
   8426 :name_bv_eq       %cstr8("bytevector=?")
   8427 :name_bytes_eq    %cstr8("bytes=?")
   8428 :name_make_hash   %cstr8("%make-hash-table")
   8429 :name_hash_ref    %cstr8("%hash-ref")
   8430 :name_hash_set    %cstr8("%hash-set!")
   8431 :name_hash_delete %cstr8("%hash-delete!")
   8432 :name_hash_size   %cstr8("%hash-size")
   8433 
   8434 :name_sys_read    %cstr8("sys-read")
   8435 :name_sys_write   %cstr8("sys-write")
   8436 :name_sys_close   %cstr8("sys-close")
   8437 :name_sys_openat  %cstr8("sys-openat")
   8438 :name_sys_clone   %cstr8("sys-clone")
   8439 :name_sys_execve  %cstr8("sys-execve")
   8440 :name_sys_spawn   %cstr8("sys-spawn")
   8441 :name_sys_waitid  %cstr8("sys-waitid")
   8442 :name_sys_argv    %cstr8("sys-argv")
   8443 :name_eof         %cstr8("eof")
   8444 :name_eofq        %cstr8("eof?")
   8445 :name_values      %cstr8("values")
   8446 :name_call_with_values %cstr8("call-with-values")
   8447 :name_display     %cstr8("display")
   8448 :name_write       %cstr8("write")
   8449 :name_error       %cstr8("error")
   8450 :name_format      %cstr8("format")
   8451 :name_heap_usage  %cstr8("heap-usage")
   8452 :name_target_word_bytes %cstr8("target-word-bytes")
   8453 :name_target_word_bits  %cstr8("target-word-bits")
   8454 :name_collect_garbage %cstr8("collect-garbage")
   8455 :name_recordq               %cstr8("record?")
   8456 :name_record_td             %cstr8("record-td")
   8457 :name_record_ref            %cstr8("record-ref")
   8458 :name_record_set_bang       %cstr8("record-set!")
   8459 :name_make_record_td        %cstr8("make-record/td")
   8460 :name_td_nfields            %cstr8("td-nfields")
   8461 :name_td_name               %cstr8("td-name")
   8462 :name_tagged_value          %cstr8("tagged-value")
   8463 :name_peek_memory_u8        %cstr8("peek-memory-u8")
   8464 
   8465 # Writer string constants. Lengths are hard-coded at the str_putn call
   8466 # sites (write_to_bv branches). They are emitted through cstr8 so the
   8467 # labels remain aligned and are also safe as C strings if reused later.
   8468 :str_false        %cstr8("#f")
   8469 :str_true         %cstr8("#t")
   8470 :str_nil          %cstr8("()")
   8471 :str_unspec       %cstr8("#!unspec")
   8472 :str_unbound      %cstr8("#!unbound")
   8473 :str_eof          %cstr8("#!eof")
   8474 :str_closure      %cstr8("#<closure>")
   8475 :str_prim         %cstr8("#<prim>")
   8476 :str_td           %cstr8("#<rec-type>")
   8477 :str_rec          %cstr8("#<record>")
   8478 :str_unknown      %cstr8("#<unknown>")
   8479 :str_error_prefix %cstr8("scheme1: error: ")
   8480 :str_u8_open      %cstr8("#u8(")
   8481 :str_char_open    %cstr8("#\\")
   8482 
   8483 # Primitive registration table. Each entry: 8-byte name_ptr (4-byte label
   8484 # ref + 4 pad), 8-byte name_len, 8-byte entry_label (4 ref + 4 pad).
   8485 :prim_table
   8486 &name_sys_exit    %(0)  $(8)   &prim_sys_exit_entry     %(0)
   8487 &name_cons        %(0)  $(4)   &prim_cons_entry         %(0)
   8488 &name_car         %(0)  $(3)   &prim_car_entry          %(0)
   8489 &name_cdr         %(0)  $(3)   &prim_cdr_entry          %(0)
   8490 &name_nullq       %(0)  $(5)   &prim_nullq_entry        %(0)
   8491 &name_pairq       %(0)  $(5)   &prim_pairq_entry        %(0)
   8492 &name_stringq     %(0)  $(7)   &prim_stringq_entry      %(0)
   8493 &name_bytevectorq %(0)  $(11)  &prim_bytevectorq_entry  %(0)
   8494 &name_bytesq      %(0)  $(6)   &prim_bytesq_entry       %(0)
   8495 &name_set_car     %(0)  $(8)   &prim_set_car_entry      %(0)
   8496 &name_set_cdr     %(0)  $(8)   &prim_set_cdr_entry      %(0)
   8497 &name_length      %(0)  $(6)   &prim_length_entry       %(0)
   8498 &name_list_ref    %(0)  $(8)   &prim_list_ref_entry     %(0)
   8499 &name_assq        %(0)  $(4)   &prim_assq_entry         %(0)
   8500 &name_assoc       %(0)  $(5)   &prim_assoc_entry        %(0)
   8501 &name_reverse     %(0)  $(7)   &prim_reverse_entry      %(0)
   8502 &name_str_to_sym  %(0)  $(14)  &prim_string_to_symbol_entry %(0)
   8503 &name_sym_to_str  %(0)  $(14)  &prim_symbol_to_string_entry %(0)
   8504 &name_num_to_str  %(0)  $(14)  &prim_number_to_string_entry %(0)
   8505 &name_str_to_num  %(0)  $(14)  &prim_string_to_number_entry %(0)
   8506 &name_bv_append   %(0)  $(17)  &prim_bv_append_entry    %(0)
   8507 &name_booleanq    %(0)  $(8)   &prim_booleanq_entry     %(0)
   8508 &name_integerq    %(0)  $(8)   &prim_integerq_entry     %(0)
   8509 &name_charq       %(0)  $(5)   &prim_charq_entry        %(0)
   8510 &name_char_to_integer %(0) $(13) &prim_char_to_integer_entry %(0)
   8511 &name_integer_to_char %(0) $(13) &prim_integer_to_char_entry %(0)
   8512 &name_symbolq     %(0)  $(7)   &prim_symbolq_entry      %(0)
   8513 &name_procedureq  %(0)  $(10)  &prim_procedureq_entry   %(0)
   8514 &name_zeroq       %(0)  $(5)   &prim_zeroq_entry        %(0)
   8515 &name_not         %(0)  $(3)   &prim_not_entry          %(0)
   8516 &name_eqq         %(0)  $(3)   &prim_eqq_entry          %(0)
   8517 &name_equal       %(0)  $(6)   &prim_equal_entry        %(0)
   8518 &name_plus        %(0)  $(1)   &prim_plus_entry         %(0)
   8519 &name_minus       %(0)  $(1)   &prim_minus_entry        %(0)
   8520 &name_mult        %(0)  $(1)   &prim_mult_entry         %(0)
   8521 &name_eq          %(0)  $(1)   &prim_eq_entry           %(0)
   8522 &name_lt          %(0)  $(1)   &prim_lt_entry           %(0)
   8523 &name_gt          %(0)  $(1)   &prim_gt_entry           %(0)
   8524 &name_quotient    %(0)  $(8)   &prim_quotient_entry     %(0)
   8525 &name_remainder   %(0)  $(9)   &prim_remainder_entry    %(0)
   8526 &name_bit_and     %(0)  $(7)   &prim_bit_and_entry      %(0)
   8527 &name_bit_or      %(0)  $(6)   &prim_bit_or_entry       %(0)
   8528 &name_bit_xor     %(0)  $(7)   &prim_bit_xor_entry      %(0)
   8529 &name_bit_not     %(0)  $(7)   &prim_bit_not_entry      %(0)
   8530 &name_arith_shift %(0)  $(16)  &prim_arith_shift_entry  %(0)
   8531 &name_apply       %(0)  $(5)   &prim_apply_entry        %(0)
   8532 &name_make_bv     %(0)  $(15)  &prim_make_bytevector_entry %(0)
   8533 &name_make_string %(0)  $(11)  &prim_make_string_entry  %(0)
   8534 &name_bv_length   %(0)  $(17)  &prim_bv_length_entry    %(0)
   8535 &name_string_length %(0) $(13) &prim_string_length_entry %(0)
   8536 &name_string_ref  %(0)  $(10)  &prim_string_ref_entry   %(0)
   8537 &name_string_set  %(0)  $(11)  &prim_string_set_entry   %(0)
   8538 &name_bv_u8_ref   %(0)  $(17)  &prim_bv_u8_ref_entry    %(0)
   8539 &name_bv_u8_set   %(0)  $(18)  &prim_bv_u8_set_entry    %(0)
   8540 &name_bv_copy     %(0)  $(15)  &prim_bv_copy_entry      %(0)
   8541 &name_bv_copy_b   %(0)  $(16)  &prim_bv_copy_bang_entry %(0)
   8542 &name_bv_eq       %(0)  $(12)  &prim_bytevector_eq_entry %(0)
   8543 &name_bytes_eq    %(0)  $(7)   &prim_bytevector_eq_entry %(0)
   8544 &name_make_hash   %(0)  $(16)  &prim_make_hash_entry     %(0)
   8545 &name_hash_ref    %(0)  $(9)   &prim_hash_ref_entry      %(0)
   8546 &name_hash_set    %(0)  $(10)  &prim_hash_set_entry      %(0)
   8547 &name_hash_delete %(0)  $(13)  &prim_hash_delete_entry   %(0)
   8548 &name_hash_size   %(0)  $(10)  &prim_hash_size_entry     %(0)
   8549 &name_sys_read    %(0)  $(8)   &prim_sys_read_entry     %(0)
   8550 &name_sys_write   %(0)  $(9)   &prim_sys_write_entry    %(0)
   8551 &name_sys_close   %(0)  $(9)   &prim_sys_close_entry    %(0)
   8552 &name_sys_openat  %(0)  $(10)  &prim_sys_openat_entry   %(0)
   8553 &name_sys_clone   %(0)  $(9)   &prim_sys_clone_entry    %(0)
   8554 &name_sys_execve  %(0)  $(10)  &prim_sys_execve_entry   %(0)
   8555 &name_sys_spawn   %(0)  $(9)   &prim_sys_spawn_entry    %(0)
   8556 &name_sys_waitid  %(0)  $(10)  &prim_sys_waitid_entry   %(0)
   8557 &name_sys_argv    %(0)  $(8)   &prim_sys_argv_entry     %(0)
   8558 &name_eofq        %(0)  $(4)   &prim_eofq_entry         %(0)
   8559 &name_display     %(0)  $(7)   &prim_display_entry      %(0)
   8560 &name_write       %(0)  $(5)   &prim_write_entry        %(0)
   8561 &name_error       %(0)  $(5)   &prim_error_entry        %(0)
   8562 &name_format      %(0)  $(6)   &prim_format_entry       %(0)
   8563 &name_heap_usage  %(0)  $(10)  &prim_heap_usage_entry   %(0)
   8564 &name_target_word_bytes %(0) $(17) &prim_target_word_bytes_entry %(0)
   8565 &name_target_word_bits  %(0) $(16) &prim_target_word_bits_entry  %(0)
   8566 &name_collect_garbage %(0) $(15) &prim_collect_garbage_entry %(0)
   8567 &name_recordq         %(0) $(7)  &prim_recordq_entry         %(0)
   8568 &name_record_td       %(0) $(9)  &prim_record_td_entry       %(0)
   8569 &name_record_ref      %(0) $(10) &prim_record_ref_entry      %(0)
   8570 &name_record_set_bang %(0) $(11) &prim_record_set_bang_entry %(0)
   8571 &name_make_record_td  %(0) $(14) &prim_make_record_td_entry  %(0)
   8572 &name_td_nfields      %(0) $(10) &prim_td_nfields_entry      %(0)
   8573 &name_td_name         %(0) $(7)  &prim_td_name_entry         %(0)
   8574 &name_tagged_value    %(0) $(12) &prim_tagged_value_entry    %(0)
   8575 &name_peek_memory_u8  %(0) $(14) &prim_peek_memory_u8_entry  %(0)
   8576 &name_values      %(0)  $(6)   &prim_values_entry       %(0)
   8577 &name_call_with_values %(0) $(16) &prim_call_with_values_entry %(0)
   8578 :prim_table_end
   8579 
   8580 ;; Error messages are NUL-terminated C strings. The embedded newline
   8581 ;; keeps the old stderr formatting; runtime_error's panic path appends
   8582 ;; another newline, which shell command substitution trims in tests.
   8583 :msg_usage          %cstr8("scheme1: usage: scheme1 SOURCE.scm\n")
   8584 :msg_load_fail      %cstr8("scheme1: failed to read source\n")
   8585 :msg_symtab_full    %cstr8("scheme1: symbol table full\n")
   8586 :msg_unexp_rparen   %cstr8("scheme1: unexpected ')'\n")
   8587 :msg_bad_hash       %cstr8("scheme1: bad #-syntax\n")
   8588 :msg_unexp_eof      %cstr8("scheme1: unexpected EOF in form\n")
   8589 :msg_unterm_list    %cstr8("scheme1: unterminated list\n")
   8590 :msg_unbound        %cstr8("scheme1: unbound variable\n")
   8591 :msg_unbound_set    %cstr8("scheme1: set!: unbound variable\n")
   8592 :msg_not_proc       %cstr8("scheme1: not a procedure\n")
   8593 :msg_type           %cstr8("scheme1: type error\n")
   8594 :msg_arity          %cstr8("scheme1: wrong number of arguments\n")
   8595 :msg_heap_full      %cstr8("scheme1: heap exhausted\n")
   8596 :msg_heap_corrupt   %cstr8("scheme1: corrupt managed heap\n")
   8597 :msg_gc_roots_full  %cstr8("scheme1: shadow root stack overflow\n")
   8598 :msg_readbuf_full   %cstr8("scheme1: source buffer overflow\n")
   8599 :msg_bv_oob         %cstr8("scheme1: bytevector index out of range\n")
   8600 :msg_string_oob     %cstr8("scheme1: string index out of range\n")
   8601 :msg_bad_byte       %cstr8("scheme1: byte value out of range\n")
   8602 :msg_unterm_string  %cstr8("scheme1: unterminated string literal\n")
   8603 :msg_bad_escape     %cstr8("scheme1: bad string escape\n")
   8604 :msg_bad_char       %cstr8("scheme1: bad #\\ character literal\n")
   8605 :msg_bad_number     %cstr8("scheme1: bad number literal\n")
   8606 :msg_bad_radix      %cstr8("scheme1: unsupported radix\n")
   8607 :msg_integer_overflow %cstr8("scheme1: exact integer overflow\n")
   8608 :msg_divide_zero    %cstr8("scheme1: division by zero\n")
   8609 :msg_bad_ident      %cstr8("scheme1: bad identifier\n")
   8610 :msg_unsupported_syntax %cstr8("scheme1: unsupported reader syntax\n")
   8611 :msg_internal_define %cstr8("scheme1: internal define is not supported\n")
   8612 :msg_cond_else_not_final %cstr8("scheme1: cond: else clause is not final\n")
   8613 :msg_record_ctor_spec %cstr8("scheme1: unsupported record constructor specification\n")
   8614 :msg_record_ctor_arity %cstr8("scheme1: record constructor arity mismatch\n")
   8615 :msg_pmatch_no_match %cstr8("scheme1: pmatch: no clause matched\n")
   8616 :msg_bad_unquote_pattern %cstr8("scheme1: pmatch: malformed ,-pattern\n")
   8617 :msg_hash_full      %cstr8("scheme1: hash table probe exhausted\n")
   8618 
   8619 :name_ch_tab      %cstr8("tab")
   8620 :name_ch_null     %cstr8("null")
   8621 :name_ch_space    %cstr8("space")
   8622 :name_ch_return   %cstr8("return")
   8623 :name_ch_newline  %cstr8("newline")
   8624 
   8625 # =========================================================================
   8626 # BSS arena table
   8627 # =========================================================================
   8628 #
   8629 # (slot, size) rows for libp1pp's init_arenas, walked once at startup.
   8630 # init_arenas threads a running offset, so each arena starts where the
   8631 # previous one ended.
   8632 :arena_table
   8633 %arena_entry(&readbuf_buf_ptr, %READBUF_CAP_BYTES)
   8634 %arena_entry(&symtab_buf_ptr,  (* %SYMTAB_CAP_SLOTS %SYMENT.SIZE))
   8635 %arena_entry(&gc_root_buf_ptr, (* %GC_ROOT_CAP_FRAMES %GC_ROOT_FRAME_BYTES))
   8636 %arena_entry(&heap_buf_ptr,    %HEAP_CAP_BYTES)
   8637 :arena_table_end
   8638 
   8639 # =========================================================================
   8640 # Scalar BSS (file-resident, zero-initialized)
   8641 # =========================================================================
   8642 
   8643 # Managed-heap physical chain and accounting.
   8644 :heap_base        $(0)
   8645 :heap_tail        $(0)
   8646 :heap_end         $(0)
   8647 :heap_allocated   $(0)
   8648 :gc_free_list     $(0)
   8649 :gc_mark_worklist $(0)
   8650 
   8651 # Exact shadow-root frame stack.
   8652 :gc_root_next     $(0)
   8653 :gc_root_end      $(0)
   8654 
   8655 # Source-buffer cursor and slurped length.
   8656 :readbuf_pos      $(0)
   8657 :readbuf_len      $(0)
   8658 
   8659 # Symbol table count (number of entries used).
   8660 :symtab_count     $(0)
   8661 
   8662 # Cached tagged-symbol values for special forms (filled by
   8663 # intern_special_forms at startup).
   8664 :sym_quote        $(0)
   8665 :sym_if           $(0)
   8666 :sym_lambda       $(0)
   8667 :sym_define       $(0)
   8668 :sym_begin        $(0)
   8669 :sym_cond         $(0)
   8670 :sym_else         $(0)
   8671 :sym_arrow        $(0)
   8672 :sym_let          $(0)
   8673 :sym_letstar      $(0)
   8674 :sym_let_values   $(0)
   8675 :sym_letstar_values $(0)
   8676 :sym_and          $(0)
   8677 :sym_or           $(0)
   8678 :sym_when         $(0)
   8679 :sym_case         $(0)
   8680 :sym_setbang     $(0)
   8681 :sym_define_record_type $(0)
   8682 :sym_pmatch       $(0)
   8683 :sym_do           $(0)
   8684 :sym_unquote      $(0)
   8685 :sym_guard        $(0)
   8686 :sym_underscore   $(0)
   8687 :sym_dollar       $(0)
   8688 
   8689 # Process startup state, captured by p1_main and read by sys-argv.
   8690 :saved_argc       $(0)
   8691 :saved_argv       $(0)
   8692 
   8693 # Scratch buffer for bv_putint / str_putint -> fmt_dec. fmt_dec writes
   8694 # at most 20 bytes for a 64-bit signed integer; 24 bytes (three words)
   8695 # is comfortable room and keeps following slots word-aligned.
   8696 :writer_num_buf   $(0) $(0) $(0)
   8697 
   8698 # Pointer slots for the past-:ELF_end arenas.
   8699 :readbuf_buf_ptr  $(0)
   8700 :heap_buf_ptr     $(0)
   8701 :symtab_buf_ptr   $(0)
   8702 :gc_root_buf_ptr  $(0)
   8703 
   8704 :ELF_end