boot2

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

commit 518c5c343c649db0492d762e55d24c36b6df8a32
parent 27daf68ca620a6ad909d6abe17c49cf8e2468e0a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 29 Apr 2026 16:39:00 -0700

P1pp: fix loop scoping tags, were global, now are local

Diffstat:
MP1/P1pp.P1pp | 72++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcc/cc.scm | 15++++++++-------
Mscheme1/scheme1.P1pp | 2+-
Atests/P1/loop-tag-scoping.P1pp | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/P1/loop-tag-scoping.expected | 2++
5 files changed, 135 insertions(+), 40 deletions(-)

diff --git a/P1/P1pp.P1pp b/P1/P1pp.P1pp @@ -357,98 +357,106 @@ # ---- Tagged loops ------------------------------------------------------- # -# Each tagged form emits two global labels `tag_top` and `tag_end`, built -# by `##` paste so references cross every macro boundary cleanly. +# Each tagged form emits two scope-local labels `tag_top` and `tag_end`, +# built by `##` paste so references cross every macro boundary cleanly. # `%break(tag)` jumps to `tag_end`; `%continue(tag)` jumps to `tag_top`. +# +# The labels use `::` so M1pp mangles them with the enclosing %scope +# (which `%fn` opens automatically). Without that, two TUs catm'd into +# one binary — e.g. libc.P1pp + tcc.flat.P1pp — would both define +# `:L0_top` ... `:LN_top` from cc.scm's per-TU label counter, and the +# late definitions would silently steal earlier branches. Outside any +# %scope (hand-written libp1pp callers, libp1pp's own internals at file +# scope) `::tag_top` degrades to `:tag_top` — backwards compatible. %macro loop_tag(tag, body) - : ## tag ## _top + :: ## tag ## _top body - %b(& ## tag ## _top) - : ## tag ## _end + %b(&:: ## tag ## _top) + :: ## tag ## _end %endm %macro while_tag_eq(tag, ra, rb, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %beq(ra, rb, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_ne(tag, ra, rb, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %bne(ra, rb, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_lt(tag, ra, rb, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %blt(ra, rb, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_ltu(tag, ra, rb, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %bltu(ra, rb, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_eqz(tag, ra, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %beqz(ra, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_nez(tag, ra, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %bnez(ra, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro while_tag_ltz(tag, ra, body) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) :@body body - : ## tag ## _top + :: ## tag ## _top %bltz(ra, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro for_lt_tag(tag, i_reg, n_reg, body) %li(i_reg, 0) - %b(& ## tag ## _test) + %b(&:: ## tag ## _test) :@body body - : ## tag ## _top + :: ## tag ## _top %addi(i_reg, i_reg, 1) - : ## tag ## _test + :: ## tag ## _test %blt(i_reg, n_reg, &@body) - : ## tag ## _end + :: ## tag ## _end %endm %macro break(tag) - %b(& ## tag ## _end) + %b(&:: ## tag ## _end) %endm %macro continue(tag) - %b(& ## tag ## _top) + %b(&:: ## tag ## _top) %endm # ========================================================================= diff --git a/cc/cc.scm b/cc/cc.scm @@ -2894,13 +2894,14 @@ (cg-in-fn?-set! cg #t) (cg-vstack-set! cg '()) (cg-frame-hi-set! cg 0) - ;; cg-label-ctr is NOT reset per-fn. Loop tags emit single-colon - ;; (global) labels via libp1pp's %loop_tag macro (`:L0_top`, - ;; `:L0_end`) — see P1/P1pp.P1pp:loop_tag — so two functions both - ;; using L0 would produce duplicate global labels and break linking. - ;; Switch dispatch labels (`sw_disp_L<N>`) inherit the same tag and - ;; are also single-colon. Keeping the counter monotonic across - ;; functions guarantees uniqueness without needing to mangle. + ;; cg-label-ctr is NOT reset per-fn. Loop tags reach libp1pp's + ;; %loop_tag / %while_tag_* / %break / %continue macros, which emit + ;; `::tag_top` / `::tag_end` — scope-local to the enclosing %fn — + ;; so within-TU collisions are already prevented by M1pp scoping. + ;; Switch dispatch labels (`sw_disp_L<N>`) likewise emit through + ;; `::` and inherit %fn's scope. Keeping the counter monotonic + ;; across functions is no longer required for correctness, just + ;; for stable, readable label names in expanded.M1 traces. (cg-max-outgoing-set! cg 0) (cg-fn-meta-set! cg '()) (%cg-fn-set! cg '%fn-name name) diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp @@ -240,7 +240,7 @@ # eof = skip_ws() %call(&skip_ws) # if eof break - %bnez(a0, &eval_end) + %bnez(a0, &::eval_end) # expr = parse_one() %call(&parse_one) # eval(expr, env=nil) diff --git a/tests/P1/loop-tag-scoping.P1pp b/tests/P1/loop-tag-scoping.P1pp @@ -0,0 +1,84 @@ +# tests/p1/loop-tag-scoping.P1pp — regression test for libp1pp's +# tag-loop scoping (commit that switched %loop_tag and friends from +# `: ## tag ## _top` to `:: ## tag ## _top`). +# +# The bug: %loop_tag, %while_tag_*, %for_lt_tag, %break, %continue +# all paste-built `:tag_top` / `:tag_end` as single-colon globals. +# When two functions in the same TU both used `%loop_tag(L0, ...)`, +# they emitted duplicate `:L0_top` / `:L0_end` labels — M0 keeps the +# last definition, so all `&L0_top` references resolved to the +# winner regardless of which function's loop they were emitted from. +# +# In real life this hit when libc.P1pp + tcc.flat.P1pp were catm'd +# (see TCC-TODO §loop-tag fix). The shape reproduces in a single TU +# by having two `%fn` blocks both use tag `L0`, and arranging for +# the FIRST function's `%continue` / `%break` to be the ones whose +# correctness matters. +# +# Reproducer setup: +# - Global byte slot starting at '0'. +# - `bumper` is the FIRST function: it loops 3 times, incrementing +# the slot (`'0'` → `'3'`) on each iteration. +# - `p1_main` is the SECOND function. It calls bumper once, then +# enters its own `%loop_tag(L0, { %break(L0) })` (no-op). +# After the loop it writes the slot to stdout and exits 0. +# +# Without the scope fix: +# - bumper's `:L0_top` / `:L0_end` are emitted but lose to +# p1_main's later definitions in M0's symbol table. +# - bumper's `%continue(L0)` and `%break(L0)` resolve to +# p1_main's L0_top/L0_end, which are at p1_main's text address. +# - On bumper's first iteration: increment runs once, then +# `%continue(L0)` jumps into p1_main's body (in bumper's frame). +# p1_main's loop body just `%break(L0)` -> p1_main's L0_end. +# End of p1_main body -> %eret pops bumper's saved frame -> +# ret to "after %call(&bumper)" in p1_main with sp restored. +# - bumper effectively ran one iteration; slot = '0'+1 = '1'. +# - p1_main writes '1' to stdout and exits. +# +# With the fix: +# - %loop_tag emits `::tag_top` / `::tag_end` which M1pp scope- +# mangles against the enclosing %fn's %scope. bumper's labels +# become `:bumper__L0_top` / `_end`, p1_main's become +# `:p1_main__L0_top` / `_end`. No collision; each function's +# control flow is local. +# - bumper iterates the full 3 times; slot = '0'+3 = '3'. +# - p1_main writes '3' to stdout and exits. +# +# Expected stdout: "3" (single byte). + +:counter_buf $(0) + +%fn(bumper, 8, { + # Local 0 (sp+16 with the p1_mem +16 compensation): int iter + %li(a0, 0) + %st(a0, sp, 0) + %loop_tag(L0, { + %la(t0, &counter_buf) + %lb(t1, t0, 0) + %addi(t1, t1, 1) + %sb(t1, t0, 0) # counter_buf[0]++ + %ld(t0, sp, 0) + %addi(t0, t0, 1) + %st(t0, sp, 0) # iter++ + %ld(a0, sp, 0) + %li(a1, 3) + %if_lt(a0, a1, { %continue(L0) }) + %break(L0) + }) +}) + +%fn(p1_main, 0, { + %li(a0, 48) # '0' + %la(t0, &counter_buf) + %sb(a0, t0, 0) # counter_buf[0] = '0' + %call(&bumper) + %loop_tag(L0, { %break(L0) }) # second loop_tag with same tag — collision target + %li(a0, 1) # fd stdout + %la(a1, &counter_buf) + %li(a2, 1) + %call(&sys_write) + %li(a0, 0) # exit 0 +}) + +:ELF_end diff --git a/tests/P1/loop-tag-scoping.expected b/tests/P1/loop-tag-scoping.expected @@ -0,0 +1 @@ +3 +\ No newline at end of file