boot2

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

commit 67b8fdb86aedb707c973b5f8849f2bf2304a1e0a
parent 600fbdc515633243f8e9ef9b04f9c84ff07af210
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun,  3 May 2026 18:24:24 -0700

cc: update to m1pp+hex2pp

Diffstat:
MMakefile | 31++++++++++++++++++++-----------
Mcc/cc.scm | 181++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mdocs/LIBC.md | 14+++++++-------
Mtests/cc-cg/65-goto.scm | 5++---
4 files changed, 120 insertions(+), 111 deletions(-)

diff --git a/Makefile b/Makefile @@ -142,11 +142,17 @@ $(IMAGE_STAMPS): build/%/.image: scripts/Containerfile.busybox # # Stage 1: vendored hex0-seed -> M0/hex2-0/catm in the container. -TOOLS_M0 := $(foreach a,$(ALL_ARCHES),build/$(a)/tools/M0) +TOOLS_M0 := $(foreach a,$(ALL_ARCHES),build/$(a)/tools/M0) +TOOLS_CATM := $(foreach a,$(ALL_ARCHES),build/$(a)/tools/catm) tools: $(TOOLS_DIR)/M0 -$(TOOLS_M0): build/%/tools/M0: scripts/boot1.sh build/%/.image \ +# boot1.sh produces M0, hex2-0, and catm in one shot. Grouped targets +# (`&:`) tell make they're all outputs of a single recipe execution, so +# downstream rules can depend on whichever tool they actually invoke +# (e.g. cc/scheme1 tests need only catm, not M0/hex2-0). +build/%/tools/M0 build/%/tools/catm build/%/tools/hex2-0 &: \ + scripts/boot1.sh build/%/.image \ vendor/seed/%/hex0-seed vendor/seed/%/hex0.hex0 \ vendor/seed/%/hex1.hex0 vendor/seed/%/hex2.hex1 \ vendor/seed/%/catm.hex2 vendor/seed/%/M0.hex2 \ @@ -236,8 +242,9 @@ $(SCHEME1_BINS): build/%/scheme1/scheme1: $(SCHEME1_SRC) $(P1PP_BUILD_DEPS) $(call PODMAN,$*) sh scripts/boot-build-p1pp.sh $@ $(SCHEME1_SRC) # cc.scm: catm prelude + cc.scm + main.scm entry into one source the -# scheme1 interpreter can run. Catm runs inside the per-arch container. -$(CC_BINS): build/%/cc/cc.scm: $(CC_SRCS) build/%/.image build/%/tools/M0 +# scheme1 interpreter can run. Catm runs inside the per-arch container; +# only catm (not M0/hex2-0) is needed. +$(CC_BINS): build/%/cc/cc.scm: $(CC_SRCS) build/%/.image build/%/tools/catm mkdir -p $(@D) $(call PODMAN,$*) build/$*/tools/catm $@ $(CC_SRCS) @@ -434,23 +441,25 @@ TEST_P1_DEPS := $(foreach a,$(TEST_ARCHES), \ build/$(a)/.image build/$(a)/tools/M0 P1/P1-$(a).M1 \ build/$(a)/M1pp/M1pp build/$(a)/hex2pp/hex2pp vendor/seed/$(a)/ELF.hex2) -# scheme1 suite per-arch deps: image, expander, hex2pp, scheme1 binary. +# scheme1 suite per-arch deps: image, expander, hex2pp, scheme1 binary, +# and catm (boot-run-scheme1.sh prepends prelude.scm with catm). # (run-tests.sh runs the pre-built binary against each .scm fixture; it -# does not rebuild the interpreter per fixture.) +# does not rebuild the interpreter per fixture.) M0/hex2-0 are not in +# the runtime test path — they're pulled in transitively only as build +# inputs to M1pp/hex2pp. TEST_SCHEME1_DEPS := $(foreach a,$(TEST_ARCHES), \ build/$(a)/.image build/$(a)/M1pp/M1pp build/$(a)/hex2pp/hex2pp \ - build/$(a)/scheme1/scheme1) + build/$(a)/scheme1/scheme1 build/$(a)/tools/catm) # cc-* suites: scheme1 + M1pp + hex2pp cover everything. cc-util / # cc-lex / cc-pp byte-diff their pure transformations; cc-cg / cc # compile the emitted P1pp through the P1pp toolchain (M1pp + hex2pp) # and run the resulting ELF. cc.scm is only needed by the cc suite # (it invokes the catm'd compiler against a .c fixture); the rest -# catm their own per-suite layer list. catm comes from build/$(a)/tools/ -# (built once during the seed bootstrap; only the cc-unit catm chain -# uses it now — the P1pp pipeline no longer touches it). +# catm their own per-suite layer list. The runtime path uses only catm +# from build/$(a)/tools/ — M0/hex2-0 are not invoked. TEST_CC_UNIT_DEPS := $(foreach a,$(TEST_ARCHES), \ - build/$(a)/.image build/$(a)/tools/M0 \ + build/$(a)/.image build/$(a)/tools/catm \ build/$(a)/M1pp/M1pp build/$(a)/hex2pp/hex2pp \ build/$(a)/scheme1/scheme1) diff --git a/cc/cc.scm b/cc/cc.scm @@ -2604,7 +2604,8 @@ ;; signed/unsigned dispatch, pointer scaling. ;; ;; Output uses libp1pp's structured macros (%fn, %ifelse_nez, -;; %loop_tag, %break, %continue) per docs/LIBP1PP.md. +;; %break, %continue) per docs/LIBP1PP.md. Function-local control-flow +;; labels are hex2++ dotted labels inside %fn's .scope. ;; ;; Frame layout: ;; [sp + 0 .. staging*8) outgoing-arg staging @@ -2931,14 +2932,12 @@ (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 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-label-ctr is NOT reset per-fn. Compiler-internal labels are + ;; emitted as dotted hex2++ locals inside %fn's .scope (and sometimes + ;; nested .scope blocks), so within-TU collisions are already prevented + ;; by local lookup. 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) @@ -3080,7 +3079,7 @@ (buf-push! tb ")\n")))) (buf-drain! tb (cg-fn-buf cg)) ;; ret block: ≤8B → a0; 9–16B → a0+a1; >16B sret → a0 = saved sret ptr. - (buf-push! tb "::ret\n") + (buf-push! tb ":.ret\n") (let ((rk (ctype-kind ret-type)) (sret? (%cg-fn-get cg '%fn-sret?))) (cond @@ -3850,7 +3849,7 @@ (sret? (%cg-fn-get cg '%fn-sret?))) (cond ((eq? rk 'void) - (%cg-emit-many cg (list "%b(&::ret)\n"))) + (%cg-emit-many cg (list "%b(&.ret)\n"))) ((or (eq? rk 'struct) (eq? rk 'union)) ;; struct-by-value: ≤16B (A1) → ret-slot; >16B (A2 sret) → *sret-slot. (let* ((p (cg-pop cg)) (sz (ctype-size ret-type))) @@ -3860,15 +3859,15 @@ (cond (sret? (%cg-emit-ld-slot cg 't2 (%cg-fn-get cg '%fn-sret-slot))) - (else + (else (%cg-emit-lea-slot cg "t2" (%cg-slot-expr cg ret-slot)))) (%cg-emit-byte-copy cg 't2 't0 't1 sz) - (%cg-emit-many cg (list "%b(&::ret)\n")))) + (%cg-emit-many cg (list "%b(&.ret)\n")))) (else (let ((p (cg-pop cg))) (%cg-load-opnd-into cg p 'a0) (%cg-emit-st-slot cg 'a0 ret-slot) - (%cg-emit-many cg (list "%b(&::ret)\n"))))))) + (%cg-emit-many cg (list "%b(&.ret)\n"))))))) ;; -------------------------------------------------------------------- ;; Structured control flow @@ -3922,23 +3921,26 @@ ;; body-thunk receives the loop tag as its argument; parser uses ;; that tag for cg-break / cg-continue inside the body. (let ((tag (%cg-fresh-loop-tag cg))) - (%cg-emit-many cg (list "%loop_tag(" tag ", {\n")) + (%cg-emit-many cg (list ".scope\n" + ":.top\n")) (head-thunk) (cond ((zero? (cg-depth cg)) 0) (else (let ((c (cg-pop cg))) (%cg-load-opnd-into cg c 't0) - (%cg-emit-many cg (list "%if_eqz(t0, { %break(" tag ") })\n"))))) + (%cg-emit-many cg (list "%if_eqz(t0, { %break })\n"))))) (body-thunk tag) - (%cg-emit-many cg (list "})\n")) + (%cg-emit-many cg (list "%b(&.top)\n" + ":.end\n" + ".endscope\n")) tag)) (define (cg-break cg tag) - (%cg-emit-many cg (list "%break(" tag ")\n"))) + (%cg-emit-many cg (list "%break\n"))) (define (cg-continue cg tag) - (%cg-emit-many cg (list "%continue(" tag ")\n"))) + (%cg-emit-many cg (list "%continue\n"))) ;; -------------------------------------------------------------------- ;; Variadic receive (§G.2). Layout: cg-fn-begin/v reserves a 16-slot @@ -4031,16 +4033,20 @@ ;; -------------------------------------------------------------------- ;; Labels and unconditional goto. -;; user_<name> namespace keeps the user's label space disjoint from -;; the compiler-internal ::ret and ::lbl_<n>. Labels resolve through -;; libp1pp's %scope mechanism, so forward references inside the same -;; %fn block work without explicit forward declaration. +;; C labels have function scope, even when the labelled statement appears +;; inside a nested block/loop. Emit them as function-qualified global +;; labels rather than dotted hex2++ locals, because dotted definitions +;; inside a nested `.scope` would be invisible to gotos outside it. ;; -------------------------------------------------------------------- +(define (%cg-user-label cg name-bv) + (let ((fn (%cg-fn-get cg '%fn-name))) + (bv-cat (list "cc__" fn "__user_" name-bv)))) + (define (cg-emit-label cg name-bv) - (%cg-emit-many cg (list "::user_" name-bv "\n"))) + (%cg-emit-many cg (list ":" (%cg-user-label cg name-bv) "\n"))) (define (cg-goto cg name-bv) - (%cg-emit-many cg (list "%b(&::user_" name-bv ")\n"))) + (%cg-emit-many cg (list "%b(&" (%cg-user-label cg name-bv) ")\n"))) ;; -------------------------------------------------------------------- ;; switch @@ -4059,8 +4065,8 @@ (disp-lbl (bytevector-append "sw_disp_" tag))) (%cg-load-opnd-into cg p 't0) (%cg-emit-st-slot cg 't0 off) - (%cg-emit-many cg (list "%loop_tag(" tag ", {\n" - "%b(&::" disp-lbl ")\n")) + (%cg-emit-many cg (list ".scope\n" + "%b(&." disp-lbl ")\n")) (%swctx off tag #f))) (define (cg-switch-case cg sw const-int) @@ -4070,12 +4076,12 @@ (cur (or (%cg-fn-get cg key) '())) (entry (cons const-int lbl))) (%cg-fn-set! cg key (cons entry cur)) - (%cg-emit-many cg (list "::" lbl "\n")))) + (%cg-emit-many cg (list ":." lbl "\n")))) (define (cg-switch-default cg sw) (let ((lbl (%cg-fresh-lbl cg))) (swctx-default-lbl-set! sw lbl) - (%cg-emit-many cg (list "::" lbl "\n")))) + (%cg-emit-many cg (list ":." lbl "\n")))) (define (cg-switch-end cg sw) (let* ((tag (swctx-end-tag sw)) @@ -4083,20 +4089,21 @@ (cases (reverse (or (%cg-fn-get cg key) '()))) (default-lbl (swctx-default-lbl sw)) (disp-lbl (bytevector-append "sw_disp_" tag))) - (%cg-emit-many cg (list "%break(" tag ")\n" - "::" disp-lbl "\n")) + (%cg-emit-many cg (list "%break\n" + ":." disp-lbl "\n")) (%cg-emit-many cg (list "%ld(t0, sp, " (%cg-slot-expr cg (swctx-ctrl-slot sw)) ")\n")) (for-each (lambda (c) (%cg-emit-many cg (list "%switch_case(t0, t1, " - (%n (car c)) ", &::" (cdr c) ")\n"))) + (%n (car c)) ", &." (cdr c) ")\n"))) cases) (cond - (default-lbl (%cg-emit-many cg (list "%b(&::" default-lbl ")\n"))) + (default-lbl (%cg-emit-many cg (list "%b(&." default-lbl ")\n"))) (else 0)) - (%cg-emit-many cg (list "%break(" tag ")\n" - "})\n")))) + (%cg-emit-many cg (list "%break\n" + ":.end\n" + ".endscope\n")))) ;; -------------------------------------------------------------------- ;; Globals and data @@ -4109,9 +4116,8 @@ ;; (piece ...) — initialized in .data; pieces concatenated. ;; ;; Each piece is either: -;; <bytevector> — raw bytes; emitted as `'XXXX...'` M0 -;; quoted-hex chunks (64 bytes / 128 hex -;; chars per line). +;; <bytevector> — raw bytes; emitted as bare hex chunks +;; (64 bytes / 128 hex chars per line). ;; (label-ref . <label-bv>) — 8-byte pointer slot containing &label; ;; emitted as `&<label> %(0)` (4B label ref + ;; 4B zero pad). @@ -4126,10 +4132,12 @@ (define (cg-emit-global cg sym init) (let* ((lbl (%cg-sym-label sym)) (sz (ctype-size (sym-type sym))) - (size (if (< sz 0) 8 sz))) + (size (if (< sz 0) 8 sz)) + (al (max 1 (ctype-align (sym-type sym))))) (cond (init - (buf-push! (cg-data cg) (bv-cat (list "\n:" lbl "\n"))) + (buf-push! (cg-data cg) (bv-cat (list "\n.align " (%n al) "\n:" + lbl "\n"))) (let walk ((ps init)) (cond ((null? ps) 0) @@ -4138,7 +4146,7 @@ (walk (cdr ps)))))) (else (buf-push! (cg-bss cg) - (bv-cat (list "\n:" lbl "\n" + (bv-cat (list "\n.align " (%n al) "\n:" lbl "\n" (let zero-loop ((rem size) (acc '())) (cond ((<= rem 0) (bv-cat (reverse acc))) @@ -4189,8 +4197,10 @@ (cg-str-pool-set! cg (alist-set bv-content lbl (cg-str-pool cg))) (buf-push! (cg-data cg) - (bv-cat (append (list "\n:" lbl "\n") - (%cg-bv->hex-lines bv-content #t)))) + (bv-cat (append (list "\n.align " (%n %CG-STR-ALIGN) + "\n:" lbl "\n") + (%cg-bv->hex-lines bv-content #t) + (list ".align " (%n %CG-STR-ALIGN) "\n")))) lbl))))) ;; Mint a fresh, never-recurring label for an unnamed file-scope @@ -4204,34 +4214,23 @@ (cg-label-ctr-set! cg (+ n 1)) lbl)) -;; Render BV's bytes as `'XXXXXX'` quoted-hex M0 literals — uniform -;; format for every byte, regardless of whether it would otherwise be -;; printable. Avoids the `"..."` lex path entirely (m1pp's quoted-text -;; lex has no escape mechanism, so embedded `"`, `\`, control chars, -;; and high-bit bytes can't ride raw between the quotes), and avoids -;; per-byte `!(N)` lines (5+× larger output). Lines are chunked to -;; ≤128 hex chars (= 64 bytes) — M0's per-line quoted-literal buffer -;; is 256 bytes on amd64 and overflows otherwise. +;; Render BV's bytes as bare hex accepted directly by hex2++. Lines are +;; chunked to ≤128 hex chars (= 64 bytes) to keep generated P1pp readable. ;; ;; If TRAILING-NUL? is #t, an extra 0x00 byte is appended to terminate -;; a C string AND total emitted bytes are rounded up to %CG-STR-ALIGN -;; (8). The padding makes the next label after a string land at an -;; 8-aligned address — aarch64 BLR / 4-byte LDR fault otherwise. -;; Padding is gated on TRAILING-NUL? because the only other caller -;; (%cg-init-piece->bv) emits arbitrary initializer bytes whose -;; length is sized exactly to the C-visible field; padding a 4-byte -;; int slot to 8 would shift every following struct field. The -;; %cg-hex-line tail already emits 0x00 for indices past LEN, so -;; padding costs only output bytes. Returns a list of bytevectors -;; ready for bv-cat. +;; a C string. Alignment is emitted explicitly by callers with .align +;; so hex2++ owns padding instead of cc.scm manufacturing zero bytes. +;; The other caller (%cg-init-piece->bv) emits arbitrary initializer +;; bytes whose length is sized exactly to the C-visible field; padding a +;; 4-byte int slot to 8 would shift every following struct field. +;; Returns a list of bytevectors ready for bv-cat. (define %CG-HEX-CHUNK-BYTES 64) (define %CG-STR-ALIGN 8) (define (%cg-bv->hex-lines bv trailing-nul?) (let* ((len (bytevector-length bv)) (logical (cond (trailing-nul? (+ len 1)) (else len))) - (total (cond (trailing-nul? (align-up logical %CG-STR-ALIGN)) - (else logical)))) + (total logical)) (cond ((= total 0) '()) (else @@ -4244,17 +4243,15 @@ (else total)))) (loop end (cons (%cg-hex-line bv i end len) acc)))))))))) -;; One `'XXXX...XX'\n` line covering BV bytes [START, END). Indices +;; One `XXXX...XX\n` line covering BV bytes [START, END). Indices ;; >= LEN render as 0x00 (used for the trailing NUL terminator). (define (%cg-hex-line bv start end len) (let* ((nbytes (- end start)) - (out (make-bytevector (+ 1 (* 2 nbytes) 1 1)))) - (bytevector-u8-set! out 0 (char->integer #\')) - (let loop ((j start) (k 1)) + (out (make-bytevector (+ (* 2 nbytes) 1)))) + (let loop ((j start) (k 0)) (cond ((= j end) - (bytevector-u8-set! out k (char->integer #\')) - (bytevector-u8-set! out (+ k 1) (char->integer #\newline)) + (bytevector-u8-set! out k (char->integer #\newline)) out) (else (let ((b (cond ((< j len) (bytevector-u8-ref bv j)) @@ -6531,35 +6528,37 @@ (define (parse-do-stmt ps) (expect-kw ps 'do) ;; `continue` in a do-while must jump to the *cond test* (C11 - ;; §6.8.6.2 ¶2), not to the top of the body. cg-continue jumps to - ;; ::tag_top, so we lay the loop out so that ::tag_top labels the - ;; cond test and the body lives between an entry-skip and a back - ;; edge. The macro %loop_tag isn't shaped right for this — emit - ;; raw P1pp here, mirroring parse-for-stmt's hand-rolled layout. + ;; §6.8.6.2 ¶2), not to the top of the body. The scoped loop labels + ;; `.top` at the condition test and `.end` after the loop, so bare + ;; %continue / %break bind through hex2++ local lookup. ;; ;; Layout: - ;; ::tag_body + ;; .scope + ;; :.body ;; <body> - ;; ::tag_top ; %continue(tag) jumps here + ;; :.top ; %continue jumps here ;; <cond> - ;; %if_eqz(c, %break(tag)) - ;; %b(&::tag_body) - ;; ::tag_end + ;; %if_eqz(c, %break) + ;; %b(&.body) + ;; :.end + ;; .endscope (let* ((cg (ps-cg ps)) (tag (%cg-fresh-loop-tag cg))) - (%cg-emit-many cg (list "::" tag "_body\n")) + (%cg-emit-many cg (list ".scope\n" + ":.body\n")) (push-loop-ctx! ps 'do tag #t) (parse-stmt ps) (pop-loop-ctx! ps) (expect-kw ps 'while) (expect-punct ps 'lparen) - (%cg-emit-many cg (list "::" tag "_top\n")) + (%cg-emit-many cg (list ":.top\n")) (parse-expr ps) (rval! ps) (expect-punct ps 'rparen) (expect-punct ps 'semi) (let ((c (cg-pop cg))) (%cg-load-opnd-into cg c 't0) - (%cg-emit-many cg (list "%if_eqz(t0, { %break(" tag ") })\n"))) - (%cg-emit-many cg (list "%b(&::" tag "_body)\n" - "::" tag "_end\n"))) + (%cg-emit-many cg (list "%if_eqz(t0, { %break })\n"))) + (%cg-emit-many cg (list "%b(&.body)\n" + ":.end\n" + ".endscope\n"))) #t) (define (parse-for-stmt ps) @@ -6581,21 +6580,23 @@ ;; A C `continue` in a for-loop must run the step expression before ;; retesting the condition. Arrange the loop as: ;; jump test; top: step; test: condition; body; jump top - (%cg-emit-many cg (list "%b(&::" tag "_test)\n" - "::" tag "_top\n")) + (%cg-emit-many cg (list ".scope\n" + "%b(&.test)\n" + ":.top\n")) (parse-saved-expr-stmt ps step-toks) - (%cg-emit-many cg (list "::" tag "_test\n")) + (%cg-emit-many cg (list ":.test\n")) (cond ((null? cond-toks) (cg-push-imm cg %t-i32 1)) (else (parse-saved-expr ps cond-toks) (rval! ps))) (let ((c (cg-pop cg))) (%cg-load-opnd-into cg c 't0) - (%cg-emit-many cg (list "%if_eqz(t0, { %break(" tag ") })\n"))) + (%cg-emit-many cg (list "%if_eqz(t0, { %break })\n"))) (push-loop-ctx! ps 'for tag #t) (parse-stmt ps) (pop-loop-ctx! ps) - (%cg-emit-many cg (list "%b(&::" tag "_top)\n" - "::" tag "_end\n"))) + (%cg-emit-many cg (list "%b(&.top)\n" + ":.end\n" + ".endscope\n"))) (scope-leave! ps) #t) (define (parse-saved-expr ps toks) diff --git a/docs/LIBC.md b/docs/LIBC.md @@ -205,10 +205,10 @@ library: it suppresses the auto-emitted entry stub (`%fn(p1_main, 16, { %call(&main) })`) and the trailing `:ELF_end`, and namespaces anonymous string labels as `PFX+"cc__str_N"` so two cc.scm outputs in the same link don't collide on `cc__str_0..N`. -String literals are 8-byte padded unconditionally (any TU, lib or -exec) so labels following a string land at an aligned address — -without it, aarch64 BLR / 4-byte LDR SIGBUS once a non-multiple-of-4 -string shows up in `.data`. +String literals emit their bytes plus a NUL terminator, then an +explicit `.align 8` (any TU, lib or exec) so labels following a string +land at an aligned address — without it, aarch64 BLR / 4-byte LDR +SIGBUS once a non-multiple-of-4 string shows up in `.data`. Wired together, the link is just `catm`: @@ -432,10 +432,10 @@ That's tracked in [TCC.md](TCC.md), not here. ### cc.scm bugs surfaced by Phase A -The four link-composition issues — string padding, per-TU label +The four link-composition issues — string alignment, per-TU label namespacing, missing library mode, missing :ELF_end suppression — -are now fixed in cc.scm itself (string padding is unconditional in -`%cg-bv->hex-lines`; the other three are gated on the `--lib=PFX` +are now fixed in cc.scm itself (string alignment is emitted with +`.align 8`; the other three are gated on the `--lib=PFX` flag, see §Linking). Remaining issues: - **Empty-arg-list redecl rejection.** mes headers' K&R-style `f();` diff --git a/tests/cc-cg/65-goto.scm b/tests/cc-cg/65-goto.scm @@ -12,9 +12,8 @@ ;; return s; /* 3 */ ;; } ;; -;; Exercises cg-emit-label (drops ::user_<name>) and cg-goto (emits -;; %b(&::user_<name>)). Forward refs work because libp1pp's %scope -;; resolves labels at emit time. +;; Exercises cg-emit-label / cg-goto. C labels are function-qualified +;; globals, so forward refs work even when loop/switch scopes are nested. (let* ((cg (cg-init)) (params (cg-fn-begin cg "main" '() %t-i32))