boot2

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

commit 4d156315d79d8c10153cdde943b0edb4bd13c603
parent e0a71b2419b2d73e5e8c8c5b453e290c69df7d54
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 23 Apr 2026 15:36:03 -0700

Merge branch 'worktree-agent-a63654c7cd13786ed' into integrate-m1pp

# Conflicts:
#	m1pp/m1pp.M1

Diffstat:
Mm1pp/m1pp.M1 | 658+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 648 insertions(+), 10 deletions(-)

diff --git a/m1pp/m1pp.M1 b/m1pp/m1pp.M1 @@ -1480,7 +1480,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 ## ============================================================================ @@ -1492,30 +1656,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 = &macros[0]; m_end = macros_end + la_a3 &macros + la_t0 &macros_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 &macros_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 &copy_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 &copy_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), @@ -1531,17 +1866,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 &copy_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 &copy_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 -------------------------------- @@ -2177,6 +2779,42 @@ ZERO8 :paste_scratch ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 +## 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