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:
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