commit 49216446b528bf525f65f87772edea67dbe3d5d6
parent 9507a3668cd1218bae1aa0f171e7991cc8768afd
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 23 Apr 2026 15:35:15 -0700
Track C: macro expansion (parse_args, find_macro/param, expand_*)
Diffstat:
| M | m1pp/m1pp.M1 | | | 658 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
1 file changed, 648 insertions(+), 10 deletions(-)
diff --git a/m1pp/m1pp.M1 b/m1pp/m1pp.M1
@@ -1253,7 +1253,171 @@ DEFINE EXPR_INVALID 1100000000000000
## Fatal on: > 16 args, reaching limit without matching RPAREN.
## Oracle: m1pp.c:parse_args.
:parse_args
- la_br &err_not_implemented
+ # tok = lparen + 1; arg_start = tok; depth = 1; arg_index = 0
+ addi_a0,a0,24
+ la_a2 &pa_pos
+ st_a0,a2,0
+ la_a2 &pa_arg_start
+ st_a0,a2,0
+ la_a2 &pa_limit
+ st_a1,a2,0
+ li_a2 %1 %0
+ la_a3 &pa_depth
+ st_a2,a3,0
+ li_a2 %0 %0
+ la_a3 &pa_arg_index
+ st_a2,a3,0
+
+:pa_loop
+ # if (tok >= limit) fatal unterminated
+ la_a0 &pa_pos
+ ld_t0,a0,0
+ la_a1 &pa_limit
+ ld_t1,a1,0
+ la_br &err_unterminated_macro
+ beq_t0,t1
+
+ # kind = tok->kind
+ ld_a2,t0,0
+
+ # if (kind == TOK_LPAREN) { depth++; tok++; loop }
+ li_a3 TOK_LPAREN
+ la_br &pa_lparen
+ beq_a2,a3
+ li_a3 TOK_RPAREN
+ la_br &pa_rparen
+ beq_a2,a3
+ li_a3 TOK_COMMA
+ la_br &pa_maybe_comma
+ beq_a2,a3
+
+ # default: tok++
+ addi_t0,t0,24
+ la_a0 &pa_pos
+ st_t0,a0,0
+ la_br &pa_loop
+ b
+
+:pa_lparen
+ la_a0 &pa_depth
+ ld_t1,a0,0
+ addi_t1,t1,1
+ st_t1,a0,0
+ addi_t0,t0,24
+ la_a0 &pa_pos
+ st_t0,a0,0
+ la_br &pa_loop
+ b
+
+:pa_rparen
+ # depth--
+ la_a0 &pa_depth
+ ld_t1,a0,0
+ addi_t1,t1,-1
+ st_t1,a0,0
+ # if (depth != 0) tok++; loop
+ la_br &pa_rparen_close
+ beqz_t1
+ addi_t0,t0,24
+ la_a0 &pa_pos
+ st_t0,a0,0
+ la_br &pa_loop
+ b
+
+:pa_rparen_close
+ # depth == 0: close out the call.
+ # arg_start (BSS), arg_index (BSS), tok = current pos.
+ la_a0 &pa_arg_start
+ ld_a1,a0,0
+ la_a0 &pa_arg_index
+ ld_a2,a0,0
+
+ # if (arg_start == tok && arg_index == 0) -> arg_count = 0
+ la_br &pa_close_with_arg
+ bne_a1,t0
+ la_br &pa_close_with_arg
+ bnez_a2
+
+ # empty (): arg_count = 0
+ li_a3 %0 %0
+ la_a0 &arg_count
+ st_a3,a0,0
+ la_br &pa_finish
+ b
+
+:pa_close_with_arg
+ # if (arg_index >= 16) fatal: branch to ok only if arg_index < 16
+ li_a3 M1PP_MAX_PARAMS
+ la_br &pa_close_with_arg_ok
+ blt_a2,a3
+ la_br &err_bad_macro_header
+ b
+:pa_close_with_arg_ok
+ # arg_starts[arg_index] = arg_start; arg_ends[arg_index] = tok
+ la_a3 &arg_starts
+ shli_t1,a2,3
+ add_a3,a3,t1
+ st_a1,a3,0
+ la_a3 &arg_ends
+ add_a3,a3,t1
+ st_t0,a3,0
+ # arg_count = arg_index + 1
+ addi_a2,a2,1
+ la_a0 &arg_count
+ st_a2,a0,0
+
+:pa_finish
+ # call_end_pos = tok + 24
+ addi_t0,t0,24
+ la_a0 &call_end_pos
+ st_t0,a0,0
+ ret
+
+:pa_maybe_comma
+ # only split at depth == 1
+ la_a0 &pa_depth
+ ld_t1,a0,0
+ li_a3 %1 %0
+ la_br &pa_default_advance
+ bne_t1,a3
+
+ # depth == 1 split: append (arg_start, tok) at arg_index
+ la_a0 &pa_arg_index
+ ld_a2,a0,0
+ li_a3 M1PP_MAX_PARAMS
+ la_br &pa_comma_ok
+ blt_a2,a3
+ la_br &err_bad_macro_header
+ b
+:pa_comma_ok
+ la_a0 &pa_arg_start
+ ld_a1,a0,0
+ la_a3 &arg_starts
+ shli_t1,a2,3
+ add_a3,a3,t1
+ st_a1,a3,0
+ la_a3 &arg_ends
+ add_a3,a3,t1
+ st_t0,a3,0
+ # arg_index++
+ addi_a2,a2,1
+ la_a0 &pa_arg_index
+ st_a2,a0,0
+ # arg_start = tok + 24
+ addi_t0,t0,24
+ la_a0 &pa_arg_start
+ st_t0,a0,0
+ la_a0 &pa_pos
+ st_t0,a0,0
+ la_br &pa_loop
+ b
+
+:pa_default_advance
+ # comma at depth != 1: just advance
+ addi_t0,t0,24
+ la_a0 &pa_pos
+ st_t0,a0,0
+ la_br &pa_loop
b
## ============================================================================
@@ -1265,30 +1429,201 @@ DEFINE EXPR_INVALID 1100000000000000
## (text+1, len-1) equals macros[i].name for some i. First match wins.
## Reads: macros, macros_end. Oracle: m1pp.c:find_macro.
:find_macro
- la_br &err_not_implemented
+ # if (tok.kind != TOK_WORD) return 0
+ ld_a1,a0,0
+ li_a2 TOK_WORD
+ la_br &find_macro_zero
+ bne_a1,a2
+
+ # if (tok.text.len < 2) return 0
+ ld_a2,a0,16
+ li_a3 %2 %0
+ la_br &find_macro_zero
+ blt_a2,a3
+
+ # if (tok.text[0] != '%') return 0
+ ld_a1,a0,8
+ lb_a3,a1,0
+ li_t0 %37 %0
+ la_br &find_macro_zero
+ bne_a3,t0
+
+ # name_ptr = tok.text + 1; name_len = tok.text.len - 1
+ addi_a1,a1,1
+ addi_a2,a2,-1
+
+ # m = ¯os[0]; m_end = macros_end
+ la_a3 ¯os
+ la_t0 ¯os_end
+ ld_t0,t0,0
+
+:find_macro_loop
+ # if (m == macros_end) return 0
+ la_br &find_macro_zero
+ beq_a3,t0
+
+ # if (m->name.len != name_len) advance
+ ld_t1,a3,8
+ la_br &find_macro_next
+ bne_t1,a2
+
+ # byte-compare m->name.ptr vs name_ptr for name_len bytes
+ ld_t1,a3,0
+ li_t2 %0 %0
+:find_macro_cmp
+ la_br &find_macro_match
+ beq_t2,a2
+ add_a0,t1,t2
+ lb_a0,a0,0
+ add_t0,a1,t2
+ lb_t0,t0,0
+ la_br &find_macro_next
+ bne_a0,t0
+ addi_t2,t2,1
+ la_br &find_macro_cmp
b
+:find_macro_next
+ # m += M1PP_MACRO_RECORD_SIZE
+ li_t1 M1PP_MACRO_RECORD_SIZE
+ add_a3,a3,t1
+ # reload macros_end (clobbered by the comparisons)
+ la_t0 ¯os_end
+ ld_t0,t0,0
+ la_br &find_macro_loop
+ b
+
+:find_macro_match
+ mov_a0,a3
+ ret
+
+:find_macro_zero
+ li_a0 %0 %0
+ ret
+
## find_param(a0=macro_ptr, a1=tok) -> a0 = (index+1) or 0. Leaf.
## Linear search over macro->params[0..param_count). Non-WORD tok -> 0, so
## callers can test the return against zero without pre-filtering.
## Oracle: m1pp.c:find_param.
:find_param
- la_br &err_not_implemented
+ # if (tok.kind != TOK_WORD) return 0
+ ld_a2,a1,0
+ li_a3 TOK_WORD
+ la_br &find_param_zero
+ bne_a2,a3
+
+ # param_count = macro->param_count
+ ld_a2,a0,16
+ la_br &find_param_zero
+ beqz_a2
+
+ # Spill bases into BSS so the cmp loop has free temp regs.
+ # fp_macro = macro_ptr
+ # fp_tok = tok ptr
+ # fp_pcount = param_count
+ # fp_idx = current param index
+ la_a3 &fp_macro
+ st_a0,a3,0
+ la_a3 &fp_tok
+ st_a1,a3,0
+ la_a3 &fp_pcount
+ st_a2,a3,0
+ li_a3 %0 %0
+ la_a0 &fp_idx
+ st_a3,a0,0
+
+:find_param_outer
+ # idx, pcount
+ la_a0 &fp_idx
+ ld_t0,a0,0
+ la_a0 &fp_pcount
+ ld_a1,a0,0
+ la_br &find_param_zero
+ beq_t0,a1
+
+ # param_ptr = fp_macro + 24 + idx * 16
+ la_a0 &fp_macro
+ ld_a2,a0,0
+ addi_a2,a2,24
+ shli_a3,t0,4
+ add_a2,a2,a3
+
+ # tok ptr
+ la_a0 &fp_tok
+ ld_a3,a0,0
+
+ # Compare lengths.
+ ld_t1,a2,8
+ ld_t2,a3,16
+ la_br &find_param_next
+ bne_t1,t2
+
+ # Lengths match. Byte-compare param.ptr vs tok.text.ptr for t1 bytes.
+ # After this point we either return or restart the outer loop, so
+ # all caller-saved regs are free.
+ ld_a0,a2,0
+ ld_a1,a3,8
+ li_t0 %0 %0
+:find_param_cmp
+ la_br &find_param_match
+ beq_t0,t1
+ add_t2,a0,t0
+ lb_t2,t2,0
+ add_a2,a1,t0
+ lb_a2,a2,0
+ la_br &find_param_next
+ bne_t2,a2
+ addi_t0,t0,1
+ la_br &find_param_cmp
b
+:find_param_next
+ # idx++
+ la_a0 &fp_idx
+ ld_t0,a0,0
+ addi_t0,t0,1
+ st_t0,a0,0
+ la_br &find_param_outer
+ b
+
+:find_param_match
+ # return idx + 1
+ la_a0 &fp_idx
+ ld_a0,a0,0
+ addi_a0,a0,1
+ ret
+
+:find_param_zero
+ li_a0 %0 %0
+ ret
+
## copy_arg_tokens_to_pool(a0=arg_start, a1=arg_end) -> void (fatal if empty)
## Non-leaf (calls copy_span_to_pool). Empty arg is an error.
## Oracle: m1pp.c:copy_arg_tokens_to_pool.
:copy_arg_tokens_to_pool
- la_br &err_not_implemented
- b
+ enter_0
+ # if (arg_start == arg_end) fatal
+ la_br &err_bad_macro_header
+ beq_a0,a1
+ la_br ©_span_to_pool
+ call
+ leave
+ ret
## copy_paste_arg_to_pool(a0=arg_start, a1=arg_end) -> void (fatal unless len 1)
## Enforces the single-token-argument rule for params adjacent to ##.
## Oracle: m1pp.c:copy_paste_arg_to_pool.
:copy_paste_arg_to_pool
- la_br &err_not_implemented
- b
+ enter_0
+ # if ((arg_end - arg_start) != 24) fatal
+ sub_a2,a1,a0
+ li_a3 M1PP_TOK_SIZE
+ la_br &err_bad_macro_header
+ bne_a2,a3
+ la_br ©_span_to_pool
+ call
+ leave
+ ret
## expand_macro_tokens(a0=call_tok, a1=limit, a2=macro_ptr) -> void (fatal on bad)
## Requires call_tok+1 is TOK_LPAREN. Runs parse_args(call_tok+1, limit),
@@ -1304,17 +1639,284 @@ DEFINE EXPR_INVALID 1100000000000000
##
## Oracle: m1pp.c:expand_macro_tokens.
:expand_macro_tokens
- la_br &err_not_implemented
+ enter_0
+
+ # Snapshot inputs into BSS (find_param/copy_*/paste_pool_range clobber regs).
+ la_a3 &emt_call_tok
+ st_a0,a3,0
+ la_a3 &emt_limit
+ st_a1,a3,0
+ la_a3 &emt_macro
+ st_a2,a3,0
+
+ # lparen = call_tok + 24
+ addi_a0,a0,24
+
+ # if (lparen >= limit) fatal
+ la_br &err_bad_macro_header
+ beq_a0,a1
+ # In parse_args, "tok < limit" is the true bound; equality is also "out of range"
+ # for an LPAREN check (no token to read). C: `call_tok + 1 >= limit` -> fatal.
+
+ # if (lparen->kind != TOK_LPAREN) fatal
+ ld_a2,a0,0
+ li_a3 TOK_LPAREN
+ la_br &err_bad_macro_header
+ bne_a2,a3
+
+ # parse_args(lparen, limit)
+ # a0 already lparen; a1 already limit
+ la_br &parse_args
+ call
+
+ # Check arg_count == macro->param_count
+ la_a0 &arg_count
+ ld_t0,a0,0
+ la_a0 &emt_macro
+ ld_t1,a0,0
+ ld_t1,t1,16
+ la_br &err_bad_macro_header
+ bne_t0,t1
+
+ # Snapshot call_end_pos -> emt_after_pos now (matches C oracle: end_pos
+ # is captured before the body walk, which is technically safe in this
+ # implementation but spares us a re-read in case future helpers write
+ # call_end_pos).
+ la_a0 &call_end_pos
+ ld_t0,a0,0
+ la_a1 &emt_after_pos
+ st_t0,a1,0
+
+ # mark = pool_used; emt_mark = mark
+ la_a0 &pool_used
+ ld_t0,a0,0
+ la_a1 &emt_mark
+ st_t0,a1,0
+
+ # body_pos = macro->body_start; body_end = macro->body_end
+ la_a0 &emt_macro
+ ld_t1,a0,0
+ li_a2 M1PP_MACRO_BODY_START_OFF
+ add_a3,t1,a2
+ ld_a3,a3,0
+ la_a0 &emt_body_pos
+ st_a3,a0,0
+ la_a0 &emt_body_start
+ st_a3,a0,0
+ li_a2 M1PP_MACRO_BODY_END_OFF
+ add_a3,t1,a2
+ ld_a3,a3,0
+ la_a0 &emt_body_end
+ st_a3,a0,0
+
+:emt_loop
+ # if (body_pos == body_end) break
+ la_a0 &emt_body_pos
+ ld_t0,a0,0
+ la_a1 &emt_body_end
+ ld_t1,a1,0
+ la_br &emt_done
+ beq_t0,t1
+
+ # param_idx = find_param(macro, body_tok)
+ la_a0 &emt_macro
+ ld_a0,a0,0
+ mov_a1,t0
+ la_br &find_param
+ call
+
+ # if (param_idx == 0) fall through to copy literal body token
+ la_br &emt_copy_literal
+ beqz_a0
+
+ # param_idx != 0: substitute. emt_do_substitute_* will re-derive
+ # the arg span (calls find_param again to recover the index) after
+ # the "pasted" classification below. This saves us from spilling idx
+ # across the body_pos +/- TOK_PASTE peek.
+
+ # Reload body_pos for the pasted-classification loads.
+ la_a0 &emt_body_pos
+ ld_t0,a0,0
+
+ # Compute pasted = (body_pos > body_start AND (body_pos - 24)->kind == TOK_PASTE)
+ # OR (body_pos + 24 < body_end AND (body_pos + 24)->kind == TOK_PASTE)
+ la_a1 &emt_body_start
+ ld_t1,a1,0
+
+ # Branch to emt_check_after if body_pos == body_start
+ la_br &emt_check_after
+ beq_t0,t1
+
+ # prev_kind = (body_pos - 24)->kind
+ addi_t2,t0,-24
+ ld_a2,t2,0
+ li_a3 TOK_PASTE
+ la_br &emt_pasted
+ beq_a2,a3
+
+:emt_check_after
+ # next_pos = body_pos + 24; if (next_pos >= body_end) skip
+ addi_t2,t0,24
+ la_a1 &emt_body_end
+ ld_a3,a1,0
+ # if (next_pos == body_end) -> not pasted (need next_pos < body_end)
+ la_br &emt_not_pasted
+ beq_t2,a3
+ # next_kind = next_pos->kind
+ ld_a2,t2,0
+ li_a3 TOK_PASTE
+ la_br &emt_pasted
+ beq_a2,a3
+ la_br &emt_not_pasted
+ b
+
+:emt_pasted
+ # body_pos is a param adjacent to ##: substitute one-token arg.
+ la_br &emt_do_substitute_paste
+ b
+
+:emt_not_pasted
+ # body_pos is a param NOT adjacent to ##: substitute arg span.
+ la_br &emt_do_substitute_plain
+ b
+
+:emt_copy_literal
+ # Append *body_pos to expand_pool. Check overflow.
+ la_a0 &pool_used
+ ld_t0,a0,0
+ li_a1 M1PP_EXPAND_CAP
+ la_br &err_token_overflow
+ beq_t0,a1
+ # dst = &expand_pool + pool_used
+ la_a2 &expand_pool
+ add_a2,a2,t0
+ # src = body_pos
+ la_a0 &emt_body_pos
+ ld_a3,a0,0
+ # copy 24 bytes (3 x 8)
+ ld_a0,a3,0
+ st_a0,a2,0
+ ld_a0,a3,8
+ st_a0,a2,8
+ ld_a0,a3,16
+ st_a0,a2,16
+ # pool_used += 24
+ addi_t0,t0,24
+ la_a0 &pool_used
+ st_t0,a0,0
+ # body_pos += 24
+ addi_a3,a3,24
+ la_a0 &emt_body_pos
+ st_a3,a0,0
+ la_br &emt_loop
b
+:emt_do_substitute_paste
+ # Re-derive arg span for current body_pos.
+ la_a0 &emt_macro
+ ld_a0,a0,0
+ la_a1 &emt_body_pos
+ ld_a1,a1,0
+ la_br &find_param
+ call
+ addi_a0,a0,-1
+ shli_a0,a0,3
+ la_a1 &arg_starts
+ add_a1,a1,a0
+ ld_t0,a1,0
+ la_a1 &arg_ends
+ add_a1,a1,a0
+ ld_t1,a1,0
+ mov_a0,t0
+ mov_a1,t1
+ la_br ©_paste_arg_to_pool
+ call
+ # body_pos += 24
+ la_a0 &emt_body_pos
+ ld_t0,a0,0
+ addi_t0,t0,24
+ st_t0,a0,0
+ la_br &emt_loop
+ b
+
+:emt_do_substitute_plain
+ # Re-derive arg span for current body_pos.
+ la_a0 &emt_macro
+ ld_a0,a0,0
+ la_a1 &emt_body_pos
+ ld_a1,a1,0
+ la_br &find_param
+ call
+ addi_a0,a0,-1
+ shli_a0,a0,3
+ la_a1 &arg_starts
+ add_a1,a1,a0
+ ld_t0,a1,0
+ la_a1 &arg_ends
+ add_a1,a1,a0
+ ld_t1,a1,0
+ mov_a0,t0
+ mov_a1,t1
+ la_br ©_arg_tokens_to_pool
+ call
+ # body_pos += 24
+ la_a0 &emt_body_pos
+ ld_t0,a0,0
+ addi_t0,t0,24
+ st_t0,a0,0
+ la_br &emt_loop
+ b
+
+:emt_done
+ # paste_pool_range(mark). emt_after_pos and emt_mark are already published.
+ la_a0 &emt_mark
+ ld_a0,a0,0
+ la_br &paste_pool_range
+ call
+
+ leave
+ ret
+
## expand_call(a0=stream_ptr, a1=macro_ptr) -> void (fatal on bad call)
## Calls expand_macro_tokens for the call at stream->pos, sets
## stream->pos = emt_after_pos, stream->line_start = 0, and
## push_pool_stream_from_mark(emt_mark) to rescan the expansion.
## Oracle: m1pp.c:expand_call.
:expand_call
- la_br &err_not_implemented
- b
+ enter_8
+
+ # spill stream_ptr to local frame slot (sp+16 is the first local; sp+0/+8
+ # hold the saved return address and saved caller sp).
+ st_a0,sp,16
+
+ # expand_macro_tokens(stream->pos, stream->end, macro)
+ # stream->pos at +16, stream->end at +8
+ ld_t0,a0,16
+ ld_t1,a0,8
+ mov_a2,a1
+ mov_a0,t0
+ mov_a1,t1
+ la_br &expand_macro_tokens
+ call
+
+ # stream->pos = emt_after_pos
+ ld_a0,sp,16
+ la_a1 &emt_after_pos
+ ld_t0,a1,0
+ st_t0,a0,16
+
+ # stream->line_start = 0
+ li_t0 %0 %0
+ st_t0,a0,24
+
+ # push_pool_stream_from_mark(emt_mark)
+ la_a0 &emt_mark
+ ld_a0,a0,0
+ la_br &push_pool_stream_from_mark
+ call
+
+ leave
+ ret
## ============================================================================
## --- Phase 6 STUBS: ## token paste compaction --------------------------------
@@ -1680,6 +2282,42 @@ ZERO8
:eval_value
ZERO8
+## Track C scalars: parse_args + expand_macro_tokens + find_param locals
+## (P1v2 has no callee-save spill on enter, and find_param's inner byte
+## compare needs every caller-saved register; parse_args + expand_macro_tokens
+## write the parse_args output globals across iterations and across calls).
+## One u64 each (ZERO8).
+:pa_pos
+ZERO8
+:pa_arg_start
+ZERO8
+:pa_depth
+ZERO8
+:pa_arg_index
+ZERO8
+:pa_limit
+ZERO8
+:emt_call_tok
+ZERO8
+:emt_limit
+ZERO8
+:emt_macro
+ZERO8
+:emt_body_pos
+ZERO8
+:emt_body_end
+ZERO8
+:emt_body_start
+ZERO8
+:fp_macro
+ZERO8
+:fp_tok
+ZERO8
+:fp_pcount
+ZERO8
+:fp_idx
+ZERO8
+
## arg_starts[16] / arg_ends[16]: 16 × 8 = 128 bytes each, i.e. 4 ZERO32.
## Written by parse_args; read by expand_macro_tokens and expand_builtin_call.
:arg_starts