commit c13c79ba665e09e3a39490cb97ba27698904d1bb parent fa542273abf56197707d1ce2db72563bdd9daa12 Author: Ryan Sepassi <rsepassi@gmail.com> Date: Mon, 27 Apr 2026 10:35:20 -0700 cc: fix three kitchen-sink-blocking bugs; split into per-feature e2e fixtures Three independent bugs in cc, all masked by 01-kitchen-sink failing end-to-end: 1. cc/parse.scm:%parse-init-struct-list — out-of-order designators on global structs ({.y=5,.x=7}) were appended sequentially with a forward-only `filled` cursor, doubling the data section. Replaced with a sort-by-offset model that emits pad pieces in any gaps and preserves label-ref pieces. 2. cc/cg.scm:cg-fn-begin/v — was resetting cg-label-ctr per function, so two functions both got loop tag L0. libp1pp's %loop_tag emits single-colon (global) labels, so duplicates collided in expanded.M1 and hex2 resolved them arbitrarily, corrupting control flow. Made the counter monotonic across the TU. 3. cc/parse.scm:%parse-init-local-struct-list — fields skipped by a designator (struct Point a = {.y=5}) were left uninitialized on the stack instead of zero-filled per C11 §6.7.9 ¶21. Switched zero-fill from positional `rest` view to a `seen` set of named fields, and fixed an associated 3-arg `+` typo (scheme1's + is binary, so the third arg was silently dropped). Split tests/cc-e2e/01-kitchen-sink.c into per-feature fixtures (02-arith.c through 32-local-struct-desig.c) so future regressions surface against an isolated reproducer instead of an integration test. docs/CC-KITCHEN-SINK-PUNCHLIST.md captures rebuild + per-fixture run instructions, the three bugs and fixes, and a stale-cc.scm pitfall that surfaced mid-investigation. docs/CC-HEAP-REWIND.md gets a brief "resolved" pointer. Verified on aarch64: cc-e2e 33/33, cc-cg 48/48, cc-parse 68/68. amd64 / riscv64 not re-run; both fixes are arch-independent. Diffstat:
66 files changed, 1103 insertions(+), 36 deletions(-)
diff --git a/cc/cg.scm b/cc/cg.scm @@ -287,7 +287,13 @@ (cg-in-fn?-set! cg #t) (cg-vstack-set! cg '()) (cg-frame-hi-set! cg 0) - (cg-label-ctr-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-max-outgoing-set! cg 0) (cg-fn-meta-set! cg '()) (%cg-fn-set! cg '%fn-name name) diff --git a/cc/parse.scm b/cc/parse.scm @@ -608,19 +608,77 @@ (cond ((at-punct? ps 'comma) (advance ps))) (lp (append (reverse piece) acc) (+ count 1)))))))) +(define (%piece-bytesize p) + ;; Output width of one piece (cf. %cg-init-piece->bv): a bv emits + ;; one byte per element; a (label-ref . _) emits an 8-byte slot. + (cond + ((bytevector? p) (bytevector-length p)) + ((and (pair? p) (eq? (car p) 'label-ref)) 8) + (else (die #f "init: unknown piece" p)))) + +(define (%pieces-bytesize ps-list) + (let loop ((xs ps-list) (n 0)) + (cond ((null? xs) n) + (else (loop (cdr xs) (+ n (%piece-bytesize (car xs)))))))) + +(define (%merge-init-entries entries total-size) + ;; entries: list of (abs-offset . piece-list), in source order. + ;; Sort stably by offset (later writes to the same offset win, per C + ;; designated-init semantics) and emit pad pieces in any gaps and at + ;; the tail. Preserves label-ref pieces — we never merge them into a + ;; flat bv. + (let* ((sorted (%init-stable-sort-by-offset entries)) + (out + (let walk ((xs sorted) (cursor 0) (acc '())) + (cond + ((null? xs) + (cond + ((< cursor total-size) + (reverse (cons (%pad-piece (- total-size cursor)) acc))) + (else (reverse acc)))) + (else + (let* ((e (car xs)) + (eoff (car e)) + (epieces (cdr e)) + (esize (%pieces-bytesize epieces)) + (acc1 (cond + ((> eoff cursor) + (cons (%pad-piece (- eoff cursor)) acc)) + (else acc))) + (acc2 (append (reverse epieces) acc1))) + (walk (cdr xs) (+ eoff esize) acc2))))))) + out)) + +(define (%init-stable-sort-by-offset entries) + ;; Insertion sort, stable by source order for ties. n is small (one + ;; entry per initialized field) so O(n^2) is fine. + (let lp ((xs entries) (acc '())) + (cond + ((null? xs) acc) + (else + (let ((e (car xs))) + (lp (cdr xs) + (let ins ((ys acc) (head '())) + (cond + ((null? ys) + (append (reverse head) (list e))) + ((<= (car e) (car (car ys))) + (append (reverse head) (cons e ys))) + (else + (ins (cdr ys) (cons (car ys) head))))))))))) + (define (%parse-init-struct-list ps ty) - ;; Struct/union initializer; assumes `{` already consumed. - ;; Supports positional and `.field = expr` forms. + ;; Struct/union initializer; assumes `{` already consumed. Supports + ;; positional and `.field = expr` forms — including out-of-order + ;; designators ({.y=5, .x=7}). Each entry records its absolute + ;; offset; %merge-init-entries sorts and pads at the closing brace. (let* ((fields (%init-struct-fields ty)) (size (ctype-size ty))) - (let lp ((acc '()) (filled 0) (rest fields)) + (let lp ((entries '()) (rest fields)) (cond ((at-punct? ps 'rbrace) (advance ps) - (cond - ((< filled size) - (reverse (cons (%pad-piece (- size filled)) acc))) - (else (reverse acc)))) + (%merge-init-entries (reverse entries) size)) (else (let* ((designated? (at-punct? ps 'dot)) (target @@ -643,9 +701,6 @@ (fname (car target)) (fty (car (cdr target))) (foff (car (cddr target))) - (fsize (ctype-size fty)) - ;; Pad from `filled` to `foff` if needed. - (pad-bytes (- foff filled)) (piece-list (cond ((at-punct? ps 'lbrace) @@ -662,13 +717,9 @@ (expect-punct ps 'rbrace) (list p))))) (else - (list (%const-init-piece ps fty))))) - (acc2 (cond ((> pad-bytes 0) - (cons (%pad-piece pad-bytes) acc)) - (else acc))) - (acc3 (append (reverse piece-list) acc2))) + (list (%const-init-piece ps fty)))))) (cond ((at-punct? ps 'comma) (advance ps))) - (lp acc3 (+ foff fsize) + (lp (cons (cons foff piece-list) entries) (cond (designated? ;; designated init: drop fields up to and including target @@ -792,29 +843,46 @@ (cond ((at-punct? ps 'comma) (advance ps))) (lp (+ i 1)))))))) +(define (%bv-in-list? bv xs) + (cond ((null? xs) #f) + ((equal? bv (car xs)) #t) + (else (%bv-in-list? bv (cdr xs))))) + +(define (%emit-zero-field ps base-off f) + ;; Note: scheme1's `+` is binary-only — `(+ a b c)` returns (+ a b) + ;; and silently drops the rest. Compute absolute byte offsets via + ;; nested binary +. + (let* ((fty (car (cdr f))) + (foff (car (cddr f))) + (fsize (ctype-size fty)) + (start-off (+ base-off foff))) + (let zb ((j 0)) + (cond + ((>= j fsize) #t) + (else + (%push-frame-elem-lval ps (+ start-off j) %t-u8) + (cg-push-imm (ps-cg ps) %t-u8 0) + (cg-assign (ps-cg ps)) + (cg-pop (ps-cg ps)) + (zb (+ j 1))))))) + (define (%parse-init-local-struct-list ps sm base-off ty) + ;; Track each initialized field by name in `seen`; at the closing brace + ;; zero every field NOT in `seen`. The previous design tracked positional + ;; "remaining fields" via `rest`, which silently dropped earlier fields + ;; when a designator jumped backwards (e.g. `{.y = 5}` left `x` + ;; uninitialized). C requires every unmentioned member of an aggregate + ;; with at least one designator/initializer to be zeroed (C11 §6.7.9 ¶21). (let ((fields (%init-struct-fields ty))) - (let lp ((rest fields)) + (let lp ((rest fields) (seen '())) (cond ((at-punct? ps 'rbrace) (advance ps) - ;; Zero any remaining fields. - (let zlp ((xs rest)) - (cond - ((null? xs) #t) - (else - (let* ((f (car xs)) (fty (car (cdr f))) - (foff (car (cddr f))) (fsize (ctype-size fty))) - (let zb ((j 0)) - (cond - ((>= j fsize) #t) - (else - (%push-frame-elem-lval ps (+ base-off foff j) %t-u8) - (cg-push-imm (ps-cg ps) %t-u8 0) - (cg-assign (ps-cg ps)) - (cg-pop (ps-cg ps)) - (zb (+ j 1))))) - (zlp (cdr xs))))))) + (for-each + (lambda (f) + (cond ((not (%bv-in-list? (car f) seen)) + (%emit-zero-field ps base-off f)))) + fields)) (else (let* ((designated? (at-punct? ps 'dot)) (target @@ -864,7 +932,8 @@ ((null? xs) '()) ((equal? (car (car xs)) fname) (cdr xs)) (else (drop (cdr xs)))))) - (else (cdr rest)))))))))) + (else (cdr rest))) + (cons fname seen)))))))) ;; A → B → C arena pattern from tests/scheme1/93-heap-mark-rewind.scm: diff --git a/docs/CC-HEAP-REWIND.md b/docs/CC-HEAP-REWIND.md @@ -0,0 +1,139 @@ +# cc heap-mark/rewind refactor + +Applies the A→B→C arena pattern from +`tests/scheme1/93-heap-mark-rewind.scm` to cut intermediate heap usage in +parse + cg. + +## Summary of changes + +### `cc/util.scm` — `buf` is now fixed-size byte storage + +- `buf` record: `(storage offset cap)`. `storage` is a single bytevector + allocated at construction; `offset` advances on each push; `cap` is the + bound. No chunks list, no per-push cons cell. +- API: + - `make-buf` / `make-buf/cap N` — allocate. + - `buf-push! b bv` — `bytevector-copy!` `bv`'s bytes into `storage[offset..]`. + - `buf-flush b` — slice `[0, offset)` as a fresh bv. + - `buf-reset! b` — set `offset` to 0 (reuse storage). + - `buf-drain! dst src` — copy `src`'s used bytes into `dst`, reset `src`. +- Caps live in one block at the top of the buf section. Each is `2^k − 1` + to defeat scheme1's `bv_capacity_for` (rounds up to the smallest power + of two **strictly greater** — asking for `2^k` consumes `2^(k+1)`). + Total fixed pre-allocation ≈ 12.27 MiB. + +### `cc/data.scm` — split cg state + +- `cg-globals` is now **user-visible globals only** (the things + `cg-emit-global` / `cg-emit-extern` add). Identity-stable except when + user code adds a global — exactly the signal `parse-fn-body`'s + rewind-safety check needs. +- `cg-fn-meta` (new) holds **transient per-fn state**: `%fn-name`, + `%fn-ret-slot`, `%fn-ret-type`, `%fn-vararg-first-slot`, + `%indirect-slots`, switch-case lists. Reset on every `cg-fn-begin/v`. +- `cg-in-fn?` (new) discriminates "currently inside a function body" + for `%cg-emit-buf`'s dispatch (fn-buf vs cg-text). + +### `cc/cg.scm` — pre-allocated bufs, no per-fn `bv-cat` + +- `cg-init` pre-allocates **all five** bufs (`text`, `data`, `bss`, + `fn-buf`, `prologue-buf`) once. +- `cg-fn-begin/v` resets fn-buf/prologue-buf via `buf-reset!` (no + allocation) and resets `cg-fn-meta` to `'()`. +- `cg-fn-end` writes header bytes directly into `cg-text` via + `buf-push!`, then drains `prologue-buf` and `fn-buf` into `cg-text` + via `buf-drain!`. **No fn-block bv-cat.** +- `%cg-fn-set!` / `%cg-fn-get` now read/write `cg-fn-meta` instead of + piggy-backing on `cg-globals`. `cg-switch-case` and `cg-switch-end` + updated to match. + +### `cc/parse.scm` — A→B→C around per-function parse + +- `parse-fn-body` is the arena boundary (B): + 1. Hoist the recursive scope-bind! out so the fn's outer-scope entry + is **pre-mark** (survives rewind). + 2. Snapshot identities of `cg-globals` / `cg-str-pool` / `ps-typedefs` + / `ps-tags`; take `(heap-mark)`. + 3. Call `%parse-fn-body-inner` (C) — does the real parse + cg work, + mutating bufs by byte writes only. + 4. If all four snapshots are still `eq?`, clear `cg-fn-meta` (drops + dangling alist before rewind) and `(heap-rewind! mark)`. + 5. Otherwise (the body added a string literal / block-static / typedef + / tag), keep allocations — pay full heap cost only for these fns. + +### Net effect + +Per-function scratch (vstack opnds, intermediate bvs, ctype scratch, +scope frames, switch-case alists, fn-meta) is reclaimed cleanly between +functions. Rewind verified firing on every fn in `00-return-argc` and +`01-kitchen-sink`. + +## kitchen-sink test failure (resolved) + +The kitchen-sink runtime segfault flagged here turned out to be two +independent bugs, neither caused by this refactor. Both are documented, +diagnosed, and fixed in `docs/CC-KITCHEN-SINK-PUNCHLIST.md`: + +1. **Out-of-order designated initializer for global structs** + (`%parse-init-struct-list` in `cc/parse.scm`) — latent + pre-existing bug; cc-cg/cc-parse coverage only hit forward-order + designators. +2. **Duplicate global loop-tag labels across functions** + (`cg-fn-begin/v` resetting `cg-label-ctr` in `cc/cg.scm`) — + `%loop_tag` emits single-colon (global) labels, so two + functions both using `L0` produced collisions that hex2 + resolved arbitrarily. Made the counter monotonic across the TU. + +Disregard the "Where to look first" investigation list below; the +specific items it suggested were not the cause. With both fixes +applied, all 32 cc-e2e fixtures pass on aarch64 (the kitchen-sink +plus 30 individual per-feature fixtures, plus the original +00-return-argc). + +### Where to look first + +1. **Compare cg output vs. the pre-refactor implementation on a small + test input that reaches all kitchen-sink features.** The most likely + regression class is "cg-fn-end now emits bytes in a different order + or skips a piece." Diff the `.P1pp` for any cc-cg fixture before/after + the refactor — if any byte differs, the bug is in `cg-fn-end`. + +2. **`cg-fn-end` byte ordering** (`cc/cg.scm:362`). The original used a + single `bv-cat` of `(so-macro "%fn(...{\n" prologue body ret-block "})\n")`. + The new version emits the same pieces via individual `buf-push!` + plus two `buf-drain!`s. Confirm the emitted byte sequence is + identical, particularly: + - Newlines / whitespace around the `__SO` macro. + - Order: prologue **before** body (it is now — line 395 vs 396). + - Ret-block placement (after body, before closing `})\n`). + +3. **`cg-in-fn?-set! cg #f` placement in `cg-fn-end`** (`cc/cg.scm:381`). + It's set to `#f` BEFORE the trailing `buf-push!`es, but those pushes + target `tb` (= `cg-text`) directly, not via `%cg-emit-buf`, so the + flag should not matter. Verify nothing inside the trailing + `(%cg-slot-expr cg ret-slot)` path takes the `%cg-emit-buf` route + (it shouldn't — `%cg-slot-expr` is pure). + +4. **Switch case lists.** `cg-switch-case` was rerouted from + `cg-globals` to `cg-fn-meta`. `cc-parse/65-switch` passes, but + kitchen-sink may exercise multi-switch patterns or nested switches. + Look at `cg-switch-end`'s reverse + emit loop and confirm the case + ordering hasn't flipped (`cc/cg.scm:1102-1107`). + +5. **Buf overflow silently truncating output.** If any cap is too small + for kitchen-sink, `buf-push!` calls `die` — a clean error, not a + segfault. So this isn't the segfault cause, but worth confirming + `cc/util.scm` didn't fall through silently. Tag `die` with a printf + to be sure. + +6. **Run the produced binary under a tracer** (`gdb`, `strace`) to + localize the fault. The segfault address will point at either: + - A bogus label resolution (cg ordering / scope-leak issue), or + - A wrong frame-size / slot-offset (the `__SO` macro expansion path). + +### Quickest narrowing experiment + +Compile a kitchen-sink-derived fixture that uses **only one** kitchen-sink +feature at a time (just statics, just switch, just sizeof, etc.) and see +which one segfaults. The kitchen-sink C source's `test_*` helpers map +1:1 to features. diff --git a/docs/CC-KITCHEN-SINK-PUNCHLIST.md b/docs/CC-KITCHEN-SINK-PUNCHLIST.md @@ -0,0 +1,268 @@ +# cc kitchen-sink punch list + +Status of `tests/cc-e2e/01-kitchen-sink.c` and the per-test fixtures +(`tests/cc-e2e/02-arith.c` through `tests/cc-e2e/31-addr-array.c`). +Companion to `docs/CC-HEAP-REWIND.md`. + +**Current status: all 32 cc-e2e fixtures pass on aarch64.** The two +bugs found during this debug session are documented below and have +both been fixed in `cc/parse.scm` and `cc/cg.scm`. + +## How to (re)build cc and run a single fixture + +```sh +# 1. Build prerequisites once (image, M0/catm, m1pp, scheme1). +make scheme1 ARCH=aarch64 +make m1pp ARCH=aarch64 + +# 2. Rebuild build/aarch64/cc/cc.scm whenever any cc/*.scm or +# scheme1/prelude.scm changes. (The Makefile target depends on +# every CC_SRC, but the catm itself runs in the container.) +make cc ARCH=aarch64 +# Or, by hand (matches what `make cc` does): +podman run --rm --pull=never --platform linux/arm64 \ + --tmpfs /tmp:size=512M -e ARCH=aarch64 \ + -v "$(pwd)":/work -w /work boot2-busybox:aarch64 \ + build/aarch64/tools/catm build/aarch64/cc/cc.scm \ + scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm \ + cc/pp.scm cc/cg.scm cc/parse.scm cc/main.scm + +# 3. Run one cc-e2e fixture by name. +sh scripts/run-tests.sh --suite=cc-e2e --arch=aarch64 09-array + +# 4. Or, do the three pipeline stages by hand: +ARCH=aarch64 +SRC=tests/cc-e2e/09-array.c +OUT=build/$ARCH/cc-e2e/09-array +mkdir -p "$OUT" + +# cc: .c -> .P1pp +podman run --rm --pull=never --platform linux/arm64 \ + --tmpfs /tmp:size=512M -e ARCH=$ARCH \ + -v "$(pwd)":/work -w /work boot2-busybox:$ARCH \ + sh -c "build/$ARCH/scheme1 build/$ARCH/cc/cc.scm $SRC $OUT/09-array.P1pp" + +# m1pp + M0 + hex2 + ELF wrap: .P1pp -> ELF +podman run --rm --pull=never --platform linux/arm64 \ + --tmpfs /tmp:size=512M -e ARCH=$ARCH \ + -v "$(pwd)":/work -w /work boot2-busybox:$ARCH \ + sh scripts/boot-build-p1pp.sh "$OUT/09-array.P1pp" "$OUT/09-array" + +# Run inside the per-arch container. +podman run --rm --pull=never --platform linux/arm64 \ + --tmpfs /tmp:size=512M -e ARCH=$ARCH \ + -v "$(pwd)":/work -w /work boot2-busybox:$ARCH \ + "./$OUT/09-array" +echo "exit=$?" +``` + +Intermediate artifacts for the m1pp/M0/hex2 chain land in +`build/$ARCH/.work/<fixture-name>/` (`combined.M1pp`, `expanded.M1`, +`prog.hex2`, `linked.hex2`). Inspect those when a runtime failure looks +like a backend codegen issue rather than a cc bug. + +## Pitfall encountered during this debug session + +`build/$ARCH/cc/cc.scm` is the catm of `scheme1/prelude.scm` plus all +`cc/*.scm`. The Make target rebuilds it when any source file changes, +but **if you bypass Make and edit `cc/*.scm` or `scheme1/prelude.scm` +by hand, you must re-run `make cc` (or the explicit catm above) before +the next test run.** Otherwise cc.scm holds the *old* prelude/util ABI +(e.g. 3-arg `sys-read`) while `build/$ARCH/scheme1` exposes the new +one (4-arg), and the compiler crashes with a SIGBUS or behaves +nondeterministically. + +The run-tests.sh runner silently swallows compile output and only +prints `cc compile failed`, so the symptom looks like an intermittent +crash even though it is a deterministic stale-input problem. If you +see the cc compiler dying with SIGSEGV/SIGBUS that wasn't dying +yesterday, rebuild cc.scm first. + +## Per-fixture status (aarch64) + +All passing as of the fixes below. Each individual fixture is a +self-contained excerpt of `01-kitchen-sink.c` covering one feature. +`main` checks the relevant `test_*` return value and exits 0 on match, +1 on mismatch. + +| # | Fixture | Coverage | +|----|------------------------|----------| +| 02 | `02-arith.c` | + - * / % << >> & \| ^ ! ~ unary- | +| 03 | `03-compound.c` | += -= *= /= %= <<= >>= &= \|= ^= | +| 04 | `04-inc-dec.c` | x++ ++x x-- --x as expressions | +| 05 | `05-logical.c` | && \|\| short-circuit | +| 06 | `06-ternary.c` | `cond ? then : else` | +| 07 | `07-compare.c` | == != < > <= >= | +| 08 | `08-pointer.c` | * & **pp | +| 09 | `09-array.c` | local 1-D array + indexed sum | +| 10 | `10-array-2d.c` | local 2-D array + nested loop | +| 11 | `11-struct.c` | local struct positional init | +| 12 | `12-struct-ptr.c` | global struct designated init `{.y=5,.x=7}` (Bug 1 regression) | +| 13 | `13-call.c` | int(int,int) call | +| 14 | `14-recursion.c` | recursive factorial | +| 15 | `15-variadic.c` | va_list + __builtin_va_{start,arg,end} | +| 16 | `16-fn-ptr.c` | global fn-ptr table + indirect call | +| 17 | `17-apply.c` | fn-ptr param + struct-ptr param via `->` | +| 18 | `18-ptr-recursion.c` | pointer-arith + recursion + global-array decay | +| 19 | `19-static.c` | block-scope `static int n = 0` | +| 20 | `20-switch.c` | switch + case fall-through + default | +| 21 | `21-goto.c` | backward `goto` to a labelled stmt | +| 22 | `22-loops.c` | do-while + while + continue | +| 23 | `23-strings.c` | string literal walked via `*s` | +| 24 | `24-globals.c` | scalar + bss + array + char[] globals | +| 25 | `25-sizeof.c` | `sizeof` on int/char/struct/array | +| 26 | `26-enum.c` | enum constants with explicit value | +| 27 | `27-void-call.c` | void fn with int* out param | +| 28 | `28-cast.c` | narrowing cast + sign-extend on re-widen | +| 29 | `29-void-ptr.c` | void* round-trip cast | +| 30 | `30-comma.c` | comma operator inside an initializer | +| 31 | `31-addr-array.c` | &arr → T(*)[N] | +| 01 | `01-kitchen-sink.c` | every fixture above in one TU | + +## Bugs found and fixed + +### Bug 1 — out-of-order designated initializer for global structs (latent, pre-existing) + +**Symptom.** A global struct initialized with designators in non-declaration +order produced an oversized data section, with each designator emitting +a full struct's worth of bytes: + +```c +struct Point { int x; int y; }; /* sizeof == 8 */ +struct Point g_pt2 = { .y = 5, .x = 7 }; /* should emit 8 bytes: 7,0,0,0, 5,0,0,0 */ +``` + +emitted 16 bytes (`0,0,0,0, 5,0,0,0, 7,0,0,0, 0,0,0,0`). Reads of +`g_pt2.x` and `g_pt2.y` then saw the wrong values. + +**Where.** `cc/parse.scm:%parse-init-struct-list`. The original +implementation tracked a `filled` cursor that only moved forward. +When `.x` arrived after `.y`, the target offset (0) was *less* than +the current `filled` (8), so the "pad if pad-bytes > 0" branch was +false and the new value got appended after the previous one rather +than overwriting at offset 0. The closing `rbrace` then padded to the +declared size, doubling the data section. + +**Fix.** Switched to a sort-by-offset model. Each entry is now +collected as `(abs-offset . piece-list)` in source order; at the +closing brace `%merge-init-entries` stable-sorts by offset and emits +pad pieces in any gaps and at the tail. Preserves `(label-ref . _)` +pieces (e.g. designators that take an `&foo` value) — they are not +merged into a flat bv. New helpers: `%piece-bytesize`, +`%pieces-bytesize`, `%merge-init-entries`, +`%init-stable-sort-by-offset`. (`cc/parse.scm` ~line 519.) + +The `cc-cg/54-init-struct-desig` and `cc-parse/54-init-struct-desig` +fixtures only exercised forward designators (`{ .b = 7 }`), which the +original logic happened to handle, so the cc-cg/cc-parse suites had +not been catching this. + +The local-struct path `%parse-init-local-struct-list` writes each +field to its absolute slot offset directly, so it handles +out-of-order designators correctly. It does have a related quirk: +it does not zero a designator-skipped field (e.g. `{.y = 5}` leaves +`x` uninitialized in the local). Worth tracking separately if +strict-C zero-init for partial designators becomes important. + +### Bug 2 — duplicate global loop-tag labels across functions (refactor regression in 53e1036 / earlier — predates the heap-rewind change) + +**Symptom.** Any C source with two or more functions each containing a +loop produced a binary that crashed at runtime, even when only one of +the functions was called. Minimal repro: + +```c +int test_array(void) { + int a[5] = { 1, 2, 3, 4, 5 }; + int sum = 0; int i; + for (i = 0; i < 5; i = i + 1) sum = sum + a[i]; + return sum; +} +int test_array_2d(void) { /* never called */ + int m[2][3] = { {1,2,3}, {4,5,6} }; + int sum = 0; int i; int j; + for (i = 0; i < 2; i = i + 1) + for (j = 0; j < 3; j = j + 1) + sum = sum + m[i][j]; + return sum; +} +int main(int argc, char **argv) { + if (test_array() != 15) return 8; + return 0; +} +``` + +`test_array`'s emitted P1pp was byte-identical between this build and +the (passing) build with `test_array_2d` removed — so the cc-emitted +text wasn't itself wrong. The bug was in label resolution downstream. + +**Where.** `cc/cg.scm:%cg-fresh-loop-tag` produces names like `L0`, +`L1`, … from `cg-label-ctr`, which `cg-fn-begin/v` was resetting to 0 +at the start of every function body. The libp1pp `%loop_tag(tag, body)` +macro (`P1/P1pp.P1pp:loop_tag`) emits **single-colon** labels: + +``` +%macro loop_tag(tag, body) + : ## tag ## _top + body + %b(& ## tag ## _top) + : ## tag ## _end +%endm +``` + +Single-colon labels are **global** in m1pp; only `::label` definitions +(and `&::label` references) get rewritten under the active scope stack. +So two functions both using `L0` produced two `:L0_top` and two +`:L0_end` definitions in `expanded.M1`. The hex2 step then resolved +`%break(L0)` / `%continue(L0)` references against whichever +definition it picked — corrupting control flow, often jumping into the +wrong function and corrupting the return-address slot. + +The `cg-switch-end`-emitted `sw_disp_<tag>` dispatch label is also +single-colon and inherits the same tag, so the switch dispatcher +suffered the same collision. + +**Why cc-cg / cc-parse fixtures didn't catch it.** Almost every cc-cg +and cc-parse fixture is a single-function program (only `main`). +`cc-cg/64-switch` and `cc-parse/65-switch` define one switch in +`main`. `cc-parse/69-multi-fn` has multiple functions but no loops, +so no loop-tag labels. None of those exercise two functions that both +emit `%loop_tag` / `%break` / `%continue`. + +**Fix.** Stop resetting `cg-label-ctr` at `cg-fn-begin/v`. The counter +is now monotonic across the whole translation unit, so every +`%cg-fresh-loop-tag` returns a name that has not been used before — +no two functions can produce the same `L<N>`, and the hex2 step has a +unique definition for every reference. Three lines of comment in +`cc/cg.scm:cg-fn-begin/v` explain the constraint so the reset doesn't +drift back. + +The other label-counter consumer in cg, `%cg-fresh-lbl` (used by +`cg-switch-case`, `cg-switch-default`, the goto/labelled-stmt path, +and various intermediate jumps), all emit `::lbl_<N>` / +`::user_<name>` (double-colon, scoped) so their per-function counters +were never colliding. The shared monotonic counter doesn't change +that — it just makes them more globally unique than they need to be, +which is fine. + +## Verification + +```sh +make cc ARCH=aarch64 +sh scripts/run-tests.sh --suite=cc-e2e --arch=aarch64 # 32/32 PASS +sh scripts/run-tests.sh --suite=cc-cg --arch=aarch64 # 48/48 PASS +sh scripts/run-tests.sh --suite=cc-parse --arch=aarch64 # 68/68 PASS +``` + +amd64 / riscv64 not re-verified after the fixes (the bugs and fixes +are arch-independent — both live in cc/parse.scm and cc/cg.scm — but +should be confirmed before relying on the kitchen-sink as a +cross-arch baseline). + +## Glossary of failure-class exit codes + +| exit | meaning | +|------|---------| +| 0 | pass (or `test_X()` returned 0; no test does so by design) | +| 1 | `main`'s `if (test_X() != EXPECTED) return 1;` fired — wrong value, not a crash | +| 135 | killed by SIGBUS — usually a misaligned access or a stale cc.scm/scheme1 ABI mismatch | +| 139 | killed by SIGSEGV — bad pointer, return-address corruption, etc. | diff --git a/tests/cc-e2e/02-arith.c b/tests/cc-e2e/02-arith.c @@ -0,0 +1,27 @@ +/* tests/cc-e2e/02-arith.c — basic arithmetic operators. + * Split from 01-kitchen-sink. */ + +int test_arith(void) { + int a = 5; + int b = 3; + int r = 0; + r = r + (a + b); /* 8 */ + r = r + (a - b); /* 2 */ + r = r + (a * b); /* 15 */ + r = r + (10 / 3); /* 3 */ + r = r + (10 % 3); /* 1 */ + r = r + (1 << 4); /* 16 */ + r = r + (64 >> 2); /* 16 */ + r = r + (12 & 10); /* 8 */ + r = r + (12 | 10); /* 14 */ + r = r + (12 ^ 10); /* 6 */ + r = r + !0; /* 1 */ + r = r + ~0; /* -1 */ + r = r + (-1); /* -1 */ + return r; /* 88 */ +} + +int main(int argc, char **argv) { + if (test_arith() != 88) return 1; + return 0; +} diff --git a/tests/cc-e2e/02-arith.expected-exit b/tests/cc-e2e/02-arith.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/03-compound.c b/tests/cc-e2e/03-compound.c @@ -0,0 +1,22 @@ +/* tests/cc-e2e/03-compound.c — compound-assignment operators. + * Split from 01-kitchen-sink. */ + +int test_compound(void) { + int x = 10; + x += 5; /* 15 */ + x -= 3; /* 12 */ + x *= 2; /* 24 */ + x /= 4; /* 6 */ + x %= 4; /* 2 */ + x <<= 3; /* 16 */ + x >>= 1; /* 8 */ + x &= 12; /* 8 */ + x |= 1; /* 9 */ + x ^= 6; /* 15 */ + return x; /* 15 */ +} + +int main(int argc, char **argv) { + if (test_compound() != 15) return 1; + return 0; +} diff --git a/tests/cc-e2e/03-compound.expected-exit b/tests/cc-e2e/03-compound.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/04-inc-dec.c b/tests/cc-e2e/04-inc-dec.c @@ -0,0 +1,16 @@ +/* tests/cc-e2e/04-inc-dec.c — pre/post increment and decrement. + * Split from 01-kitchen-sink. */ + +int test_inc_dec(void) { + int x = 5; + int a = x++; /* a=5, x=6 */ + int b = ++x; /* b=7, x=7 */ + int c = x--; /* c=7, x=6 */ + int d = --x; /* d=5, x=5 */ + return a + b + c + d + x; /* 5+7+7+5+5 = 29 */ +} + +int main(int argc, char **argv) { + if (test_inc_dec() != 29) return 1; + return 0; +} diff --git a/tests/cc-e2e/04-inc-dec.expected-exit b/tests/cc-e2e/04-inc-dec.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/05-logical.c b/tests/cc-e2e/05-logical.c @@ -0,0 +1,19 @@ +/* tests/cc-e2e/05-logical.c — &&, ||, short-circuit evaluation. + * Split from 01-kitchen-sink. */ + +int test_logical(void) { + int r = 0; + if (1 && 1) r = r + 1; + if (0 && 1) r = r + 100; + if (1 || 0) r = r + 2; + if (0 || 0) r = r + 100; + int side = 0; + if (1 || (side = 100)) r = r + 4; /* short-circuit; side stays 0 */ + if (0 && (side = 100)) r = r + 100; + return r + side; /* 1+2+4+0 = 7 */ +} + +int main(int argc, char **argv) { + if (test_logical() != 7) return 1; + return 0; +} diff --git a/tests/cc-e2e/05-logical.expected-exit b/tests/cc-e2e/05-logical.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/06-ternary.c b/tests/cc-e2e/06-ternary.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/06-ternary.c — `?:` ternary operator. + * Split from 01-kitchen-sink. */ + +int test_ternary(void) { + int x = 5; + int y = (x > 3) ? 10 : 20; /* 10 */ + int z = (x < 3) ? 100 : 200; /* 200 */ + return y + z; /* 210 */ +} + +int main(int argc, char **argv) { + if (test_ternary() != 210) return 1; + return 0; +} diff --git a/tests/cc-e2e/06-ternary.expected-exit b/tests/cc-e2e/06-ternary.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/07-compare.c b/tests/cc-e2e/07-compare.c @@ -0,0 +1,18 @@ +/* tests/cc-e2e/07-compare.c — relational operators. + * Split from 01-kitchen-sink. */ + +int test_compare(void) { + int r = 0; + if (5 == 5) r = r + 1; + if (5 != 6) r = r + 2; + if (5 < 6) r = r + 4; + if (6 > 5) r = r + 8; + if (5 <= 5) r = r + 16; + if (5 >= 5) r = r + 32; + return r; /* 63 */ +} + +int main(int argc, char **argv) { + if (test_compare() != 63) return 1; + return 0; +} diff --git a/tests/cc-e2e/07-compare.expected-exit b/tests/cc-e2e/07-compare.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/08-pointer.c b/tests/cc-e2e/08-pointer.c @@ -0,0 +1,16 @@ +/* tests/cc-e2e/08-pointer.c — pointer deref + double-deref. + * Split from 01-kitchen-sink. */ + +int test_pointer(void) { + int x = 7; + int *p = &x; + *p = *p + 3; /* x = 10 */ + int **pp = &p; + int v = **pp; /* 10 */ + return *p + v; /* 20 */ +} + +int main(int argc, char **argv) { + if (test_pointer() != 20) return 1; + return 0; +} diff --git a/tests/cc-e2e/08-pointer.expected-exit b/tests/cc-e2e/08-pointer.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/09-array.c b/tests/cc-e2e/09-array.c @@ -0,0 +1,15 @@ +/* tests/cc-e2e/09-array.c — local 1-D array initializer + indexed sum. + * Split from 01-kitchen-sink. */ + +int test_array(void) { + int a[5] = { 1, 2, 3, 4, 5 }; + int sum = 0; + int i; + for (i = 0; i < 5; i = i + 1) sum = sum + a[i]; + return sum; /* 15 */ +} + +int main(int argc, char **argv) { + if (test_array() != 15) return 1; + return 0; +} diff --git a/tests/cc-e2e/09-array.expected-exit b/tests/cc-e2e/09-array.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/10-array-2d.c b/tests/cc-e2e/10-array-2d.c @@ -0,0 +1,17 @@ +/* tests/cc-e2e/10-array-2d.c — local 2-D array initializer + nested loop sum. + * Split from 01-kitchen-sink. */ + +int test_array_2d(void) { + int m[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } }; + int sum = 0; + int i; int j; + for (i = 0; i < 2; i = i + 1) + for (j = 0; j < 3; j = j + 1) + sum = sum + m[i][j]; + return sum; /* 21 */ +} + +int main(int argc, char **argv) { + if (test_array_2d() != 21) return 1; + return 0; +} diff --git a/tests/cc-e2e/10-array-2d.expected-exit b/tests/cc-e2e/10-array-2d.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/11-struct.c b/tests/cc-e2e/11-struct.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/11-struct.c — local struct with positional initializer. + * Split from 01-kitchen-sink. */ + +struct Point { int x; int y; }; + +int test_struct(void) { + struct Point local = { 3, 4 }; + return local.x + local.y; /* 7 */ +} + +int main(int argc, char **argv) { + if (test_struct() != 7) return 1; + return 0; +} diff --git a/tests/cc-e2e/11-struct.expected-exit b/tests/cc-e2e/11-struct.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/12-struct-ptr.c b/tests/cc-e2e/12-struct-ptr.c @@ -0,0 +1,22 @@ +/* tests/cc-e2e/12-struct-ptr.c — struct passed by pointer + global struct + * with positional and designated initializers. + * + * Split from 01-kitchen-sink. Exercises the out-of-order designated init + * (.y = 5, .x = 7) which the global initializer code currently emits as + * an oversized data section instead of overwriting at absolute offsets. + */ + +struct Point { int x; int y; }; +struct Point g_pt = { 100, 200 }; /* positional */ +struct Point g_pt2 = { .y = 5, .x = 7 }; /* designated, out-of-order */ + +int f_struct_arg(struct Point *p) { return p->x + p->y; } + +int test_struct_ptr(void) { + return f_struct_arg(&g_pt) + g_pt2.x + g_pt2.y; /* 300 + 7 + 5 = 312 */ +} + +int main(int argc, char **argv) { + if (test_struct_ptr() != 312) return 1; + return 0; +} diff --git a/tests/cc-e2e/12-struct-ptr.expected-exit b/tests/cc-e2e/12-struct-ptr.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/13-call.c b/tests/cc-e2e/13-call.c @@ -0,0 +1,15 @@ +/* tests/cc-e2e/13-call.c — function call with two int args. + * Split from 01-kitchen-sink. */ + +int f_basic(int a, int b) { return a + b; } + +int test_call(void) { + int x = f_basic(3, 4); /* 7 */ + int y = f_basic(10, -2); /* 8 */ + return x + y; /* 15 */ +} + +int main(int argc, char **argv) { + if (test_call() != 15) return 1; + return 0; +} diff --git a/tests/cc-e2e/13-call.expected-exit b/tests/cc-e2e/13-call.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/14-recursion.c b/tests/cc-e2e/14-recursion.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/14-recursion.c — recursive factorial. + * Split from 01-kitchen-sink. */ + +int factorial(int n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} + +int test_recursion(void) { return factorial(5); } /* 120 */ + +int main(int argc, char **argv) { + if (test_recursion() != 120) return 1; + return 0; +} diff --git a/tests/cc-e2e/14-recursion.expected-exit b/tests/cc-e2e/14-recursion.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/15-variadic.c b/tests/cc-e2e/15-variadic.c @@ -0,0 +1,26 @@ +/* tests/cc-e2e/15-variadic.c — variadic function call + va_list iteration. + * Split from 01-kitchen-sink. */ + +typedef char *va_list; + +int f_variadic(int n, ...) { + va_list ap; + int total; + int i; + __builtin_va_start(ap, n); + total = 0; + i = 0; + while (i < n) { + total = total + __builtin_va_arg(ap, int); + i = i + 1; + } + __builtin_va_end(ap); + return total; +} + +int test_variadic(void) { return f_variadic(4, 1, 2, 3, 4); } /* 10 */ + +int main(int argc, char **argv) { + if (test_variadic() != 10) return 1; + return 0; +} diff --git a/tests/cc-e2e/15-variadic.expected-exit b/tests/cc-e2e/15-variadic.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/16-fn-ptr.c b/tests/cc-e2e/16-fn-ptr.c @@ -0,0 +1,18 @@ +/* tests/cc-e2e/16-fn-ptr.c — function-pointer global table + indirect call. + * Split from 01-kitchen-sink. */ + +typedef int (*op2_t)(int, int); +int op_add(int a, int b) { return a + b; } +int op_mul(int a, int b) { return a * b; } +op2_t g_ops[2] = { op_add, op_mul }; + +int test_fn_ptr(void) { + op2_t add = g_ops[0]; + op2_t mul = g_ops[1]; + return add(3, 4) + mul(5, 6); /* 7 + 30 = 37 */ +} + +int main(int argc, char **argv) { + if (test_fn_ptr() != 37) return 1; + return 0; +} diff --git a/tests/cc-e2e/16-fn-ptr.expected-exit b/tests/cc-e2e/16-fn-ptr.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/17-apply.c b/tests/cc-e2e/17-apply.c @@ -0,0 +1,22 @@ +/* tests/cc-e2e/17-apply.c — fn-ptr param + struct-ptr param + member access via ->. + * Depends on the same g_pt2 designated-init data the 12-struct-ptr fixture uses. + * Split from 01-kitchen-sink. */ + +typedef int (*op2_t)(int, int); +int op_add(int a, int b) { return a + b; } +int op_mul(int a, int b) { return a * b; } + +struct Point { int x; int y; }; +struct Point g_pt = { 100, 200 }; +struct Point g_pt2 = { .y = 5, .x = 7 }; + +int apply2(op2_t f, struct Point *p) { return f(p->x, p->y); } + +int test_apply(void) { + return apply2(op_add, &g_pt) + apply2(op_mul, &g_pt2); /* 300 + 35 = 335 */ +} + +int main(int argc, char **argv) { + if (test_apply() != 335) return 1; + return 0; +} diff --git a/tests/cc-e2e/17-apply.expected-exit b/tests/cc-e2e/17-apply.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/18-ptr-recursion.c b/tests/cc-e2e/18-ptr-recursion.c @@ -0,0 +1,16 @@ +/* tests/cc-e2e/18-ptr-recursion.c — pointer-arith + recursion + global-array decay. + * Split from 01-kitchen-sink. */ + +int g_arr[3] = { 10, 20, 30 }; + +int sum_arr(int *p, int n) { + if (n == 0) return 0; + return *p + sum_arr(p + 1, n - 1); +} + +int test_ptr_recursion(void) { return sum_arr(g_arr, 3); } /* 60 */ + +int main(int argc, char **argv) { + if (test_ptr_recursion() != 60) return 1; + return 0; +} diff --git a/tests/cc-e2e/18-ptr-recursion.expected-exit b/tests/cc-e2e/18-ptr-recursion.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/19-static.c b/tests/cc-e2e/19-static.c @@ -0,0 +1,20 @@ +/* tests/cc-e2e/19-static.c — block-scope `static int n` retains state across calls. + * Split from 01-kitchen-sink. */ + +int counter(void) { + static int n = 0; + n = n + 1; + return n; +} + +int test_static(void) { + int a = counter(); /* 1 */ + int b = counter(); /* 2 */ + int c = counter(); /* 3 */ + return a + b + c; /* 6 */ +} + +int main(int argc, char **argv) { + if (test_static() != 6) return 1; + return 0; +} diff --git a/tests/cc-e2e/19-static.expected-exit b/tests/cc-e2e/19-static.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/20-switch.c b/tests/cc-e2e/20-switch.c @@ -0,0 +1,22 @@ +/* tests/cc-e2e/20-switch.c — switch with case fall-through and default. + * Split from 01-kitchen-sink. */ + +int test_switch(void) { + int r = 0; + int i; + for (i = 0; i < 4; i = i + 1) { + switch (i) { + case 0: r = r + 1; break; + case 1: r = r + 2; break; + case 2: + case 3: r = r + 10; break; + default: r = r + 100; break; + } + } + return r; /* 1 + 2 + 10 + 10 = 23 */ +} + +int main(int argc, char **argv) { + if (test_switch() != 23) return 1; + return 0; +} diff --git a/tests/cc-e2e/20-switch.expected-exit b/tests/cc-e2e/20-switch.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/21-goto.c b/tests/cc-e2e/21-goto.c @@ -0,0 +1,15 @@ +/* tests/cc-e2e/21-goto.c — backward goto into a labelled stmt. + * Split from 01-kitchen-sink. */ + +int test_goto(void) { + int n = 0; +loop: + n = n + 1; + if (n < 5) goto loop; + return n; /* 5 */ +} + +int main(int argc, char **argv) { + if (test_goto() != 5) return 1; + return 0; +} diff --git a/tests/cc-e2e/21-goto.expected-exit b/tests/cc-e2e/21-goto.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/22-loops.c b/tests/cc-e2e/22-loops.c @@ -0,0 +1,19 @@ +/* tests/cc-e2e/22-loops.c — do-while + while + continue. + * Split from 01-kitchen-sink. */ + +int test_loops(void) { + int x = 0; + do { x = x + 1; } while (x < 3); /* x = 3 */ + int y = 0; + while (y < 10) { + y = y + 1; + if (y == 5) continue; + x = x + 1; + } /* x = 3 + 9 = 12 */ + return x; /* 12 */ +} + +int main(int argc, char **argv) { + if (test_loops() != 12) return 1; + return 0; +} diff --git a/tests/cc-e2e/22-loops.expected-exit b/tests/cc-e2e/22-loops.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/23-strings.c b/tests/cc-e2e/23-strings.c @@ -0,0 +1,17 @@ +/* tests/cc-e2e/23-strings.c — string literal walked via *s, s = s + 1. + * Split from 01-kitchen-sink. */ + +int test_strings(void) { + char *s = "world"; + int sum = 0; + while (*s) { + sum = sum + *s; + s = s + 1; + } + return sum; /* w+o+r+l+d = 119+111+114+108+100 = 552 */ +} + +int main(int argc, char **argv) { + if (test_strings() != 552) return 1; + return 0; +} diff --git a/tests/cc-e2e/23-strings.expected-exit b/tests/cc-e2e/23-strings.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/24-globals.c b/tests/cc-e2e/24-globals.c @@ -0,0 +1,17 @@ +/* tests/cc-e2e/24-globals.c — initialized data + bss + array element + char[]. + * Split from 01-kitchen-sink. */ + +int g_scalar = 42; +int g_zero; /* bss */ +int g_arr[3] = { 10, 20, 30 }; +char g_msg[] = "hello"; + +int test_globals(void) { + g_zero = g_zero + 1; /* now 1 */ + return g_scalar + g_zero + g_arr[1] + g_msg[0]; /* 42+1+20+'h'(104) = 167 */ +} + +int main(int argc, char **argv) { + if (test_globals() != 167) return 1; + return 0; +} diff --git a/tests/cc-e2e/24-globals.expected-exit b/tests/cc-e2e/24-globals.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/25-sizeof.c b/tests/cc-e2e/25-sizeof.c @@ -0,0 +1,15 @@ +/* tests/cc-e2e/25-sizeof.c — sizeof on int, char, struct, array. + * Split from 01-kitchen-sink. */ + +struct Point { int x; int y; }; +int g_arr[3] = { 10, 20, 30 }; + +int test_sizeof(void) { + return sizeof(int) + sizeof(char) + sizeof(struct Point) + sizeof(g_arr); + /* 4 + 1 + 8 + 12 = 25 */ +} + +int main(int argc, char **argv) { + if (test_sizeof() != 25) return 1; + return 0; +} diff --git a/tests/cc-e2e/25-sizeof.expected-exit b/tests/cc-e2e/25-sizeof.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/26-enum.c b/tests/cc-e2e/26-enum.c @@ -0,0 +1,13 @@ +/* tests/cc-e2e/26-enum.c — enum constants with explicit value. + * Split from 01-kitchen-sink. */ + +enum Color { RED, GREEN = 5, BLUE }; + +int test_enum(void) { + return RED + GREEN + BLUE; /* 0 + 5 + 6 = 11 */ +} + +int main(int argc, char **argv) { + if (test_enum() != 11) return 1; + return 0; +} diff --git a/tests/cc-e2e/26-enum.expected-exit b/tests/cc-e2e/26-enum.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/27-void-call.c b/tests/cc-e2e/27-void-call.c @@ -0,0 +1,15 @@ +/* tests/cc-e2e/27-void-call.c — void-returning function with int* out param. + * Split from 01-kitchen-sink. */ + +void f_void(int *out, int v) { *out = v; } + +int test_void_call(void) { + int x = 0; + f_void(&x, 42); + return x; /* 42 */ +} + +int main(int argc, char **argv) { + if (test_void_call() != 42) return 1; + return 0; +} diff --git a/tests/cc-e2e/27-void-call.expected-exit b/tests/cc-e2e/27-void-call.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/28-cast.c b/tests/cc-e2e/28-cast.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/28-cast.c — narrowing cast with sign-extend on re-widen. + * Split from 01-kitchen-sink. */ + +int test_cast(void) { + int a = 300; + int b = (int)(char)a; /* (char)300 = 44 (sign-extends back to int) */ + int c = (int)(short)a; /* (short)300 = 300 (fits in 16 bits) */ + return b + c; /* 344 */ +} + +int main(int argc, char **argv) { + if (test_cast() != 344) return 1; + return 0; +} diff --git a/tests/cc-e2e/28-cast.expected-exit b/tests/cc-e2e/28-cast.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/29-void-ptr.c b/tests/cc-e2e/29-void-ptr.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/29-void-ptr.c — void* round-trip cast. + * Split from 01-kitchen-sink. */ + +int test_void_ptr(void) { + int x = 99; + void *p = &x; + int *q = (int *)p; + return *q; /* 99 */ +} + +int main(int argc, char **argv) { + if (test_void_ptr() != 99) return 1; + return 0; +} diff --git a/tests/cc-e2e/29-void-ptr.expected-exit b/tests/cc-e2e/29-void-ptr.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/30-comma.c b/tests/cc-e2e/30-comma.c @@ -0,0 +1,13 @@ +/* tests/cc-e2e/30-comma.c — comma operator inside an initializer expression. + * Split from 01-kitchen-sink. */ + +int test_comma(void) { + int a = 0; int b = 0; + int x = (a = 5, b = 10, a + b); /* 15 */ + return x; +} + +int main(int argc, char **argv) { + if (test_comma() != 15) return 1; + return 0; +} diff --git a/tests/cc-e2e/30-comma.expected-exit b/tests/cc-e2e/30-comma.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/31-addr-array.c b/tests/cc-e2e/31-addr-array.c @@ -0,0 +1,14 @@ +/* tests/cc-e2e/31-addr-array.c — &arr yields T(*)[N] (pointer-to-whole-array). + * Split from 01-kitchen-sink. */ + +int g_arr[3] = { 10, 20, 30 }; + +int test_addr_array(void) { + int (*p)[3] = &g_arr; + return (*p)[0] + (*p)[1] + (*p)[2]; /* 60 */ +} + +int main(int argc, char **argv) { + if (test_addr_array() != 60) return 1; + return 0; +} diff --git a/tests/cc-e2e/31-addr-array.expected-exit b/tests/cc-e2e/31-addr-array.expected-exit @@ -0,0 +1 @@ +0 diff --git a/tests/cc-e2e/32-local-struct-desig.c b/tests/cc-e2e/32-local-struct-desig.c @@ -0,0 +1,35 @@ +/* tests/cc-e2e/32-local-struct-desig.c — local struct designated init. + * + * Verifies C11 §6.7.9 ¶21: any field of an aggregate with at least one + * initializer that is NOT explicitly initialized must be zeroed. + * + * The earlier %parse-init-local-struct-list dropped positional state + * after a backwards designator and left those fields uninitialized. + * Read both fields through a pointer (so a stale stack slot can't be + * folded away by anything trivial). */ + +struct Point { int x; int y; }; + +int sum_via_ptr(struct Point *p) { return p->x + p->y; } + +int test_local_struct_desig_partial(void) { + struct Point a = { .y = 5 }; /* x must be zeroed */ + return sum_via_ptr(&a); /* 0 + 5 = 5 */ +} + +int test_local_struct_desig_reverse(void) { + struct Point b = { .y = 5, .x = 7 }; /* both written, no zero needed */ + return sum_via_ptr(&b); /* 7 + 5 = 12 */ +} + +int test_local_struct_desig_only_first(void) { + struct Point c = { .x = 9 }; /* y must be zeroed */ + return sum_via_ptr(&c); /* 9 + 0 = 9 */ +} + +int main(int argc, char **argv) { + if (test_local_struct_desig_partial() != 5) return 1; + if (test_local_struct_desig_reverse() != 12) return 2; + if (test_local_struct_desig_only_first() != 9) return 3; + return 0; +} diff --git a/tests/cc-e2e/32-local-struct-desig.expected-exit b/tests/cc-e2e/32-local-struct-desig.expected-exit @@ -0,0 +1 @@ +0