From 40424df1b83e2ef533ae0871a69bd2ff5f9cfa5a Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 14:45:29 +0300 Subject: [PATCH] fibers B1.0a: close generic/pack is_pure gap (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review of dd363ca found is_pure was set only at the two declareFunction decl sites. Generic monomorphization (generic.zig) and pack expansion (pack.zig) create the IR Function via a different path and left is_pure false, so a generic abi(.pure) instance bypassed the emit bail and silently shipped a framed body — it returned 42 but leaked the prologue's stack adjustment (the exact SP-in != SP-out corruption the lock exists to prevent). Both paths now set is_pure and route .pure bodies through the asm-only + unreachable cap, mirroring the decl path. Locked by examples/1801-concurrency-pure-generic-bail.sx (generic .pure reaches the loud bail). The review's other CRITICAL (a .pure lambda) is a false positive: isLambda's return-type scan (parser.zig:3652) breaks on the abi keyword, so a .pure lambda is unparseable and parseLambda's abi handling is never reached. Latent isLambda/parseLambda inconsistency, not a B1 concern. Suite green (723/0). --- current/CHECKPOINT-FIBERS.md | 24 ++++++++++++++++--- current/PLAN-FIBERS.md | 4 ++-- .../1801-concurrency-pure-generic-bail.sx | 23 ++++++++++++++++++ .../1801-concurrency-pure-generic-bail.exit | 1 + .../1801-concurrency-pure-generic-bail.stderr | 1 + .../1801-concurrency-pure-generic-bail.stdout | 1 + src/ir/lower/generic.zig | 8 +++++++ src/ir/lower/pack.zig | 9 ++++++- 8 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 examples/1801-concurrency-pure-generic-bail.sx create mode 100644 examples/expected/1801-concurrency-pure-generic-bail.exit create mode 100644 examples/expected/1801-concurrency-pure-generic-bail.stderr create mode 100644 examples/expected/1801-concurrency-pure-generic-bail.stdout diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index 9db7ef09..b94dd656 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -20,10 +20,22 @@ emit bail loudly: - `examples/1800-concurrency-pure-asm.sx` — one host example (no `.build` pin; the bail is host-independent, fires before any asm/instruction selection), locked to the bail snapshot (exit 1, empty stdout, the loud diagnostic on stderr). +- **Adversarial review (closed in-step):** the review caught that `is_pure` was set ONLY at + the two `declareFunction` decl sites — generic monomorphization + ([generic.zig](../src/ir/lower/generic.zig)) and pack expansion + ([pack.zig](../src/ir/lower/pack.zig)) create the `Function` via a different path and left + `is_pure` false, so a generic `.pure` instance silently shipped a framed body (returned 42 + but leaked the prologue's stack adjustment — the exact corruption the lock prevents). Both + paths now set `is_pure` + route `.pure` bodies through the asm-only + `unreachable` cap. + Locked by `examples/1801-concurrency-pure-generic-bail.sx`. (The review's other CRITICAL — + a `.pure` *lambda* — is a **false positive**: `isLambda`'s return-type scan + (parser.zig:3652) breaks on the `abi` keyword, so a `.pure` lambda is unparseable and + `parseLambda`'s abi-handling is never reached. Latent `isLambda`/`parseLambda` + inconsistency, not a B1 concern.) - **Naming:** the sx-facing name is **`pure`** throughout (field, diagnostic); LLVM's `naked` attribute is only the B1.0b lowering mechanism (per user direction — don't call the function "naked"). -- `zig build && zig build test` green: **722 ran, 0 failed**. +- `zig build && zig build test` green: **723 ran, 0 failed**. ## Current state Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1: **B1.0a @@ -39,7 +51,7 @@ real LLVM `naked` emission). No fibers/Io/scheduler code yet. Grounded floor fac **B1.0b (`abi(.pure)` real emission)** — per PLAN-FIBERS.md "Phases → B1.0 → B1.0b" and the kickoff prompt at the bottom of that file. Replace the emit bail with LLVM's `naked` attribute + asm-only body; pin `1800` aarch64 (run end-to-end → exit 42, capture `.ir`); add -x86_64 cross sibling `1801` (ir-only); add an `emit_llvm.test.zig` unit test asserting the +x86_64 cross sibling `1802` (ir-only); add an `emit_llvm.test.zig` unit test asserting the `naked` attr. Separate commit (cadence rule — B1.0a locked, B1.0b greens). ## Known issues / capability gaps @@ -99,4 +111,10 @@ x86_64 cross sibling `1801` (ir-only); add an `emit_llvm.test.zig` unit test ass `emit_llvm` Pass 2 bails loudly on `func.is_pure`. `examples/1800-concurrency-pure-asm.sx` locked to the bail (exit 1 + diagnostic). Renamed `is_naked`→`is_pure` per user direction (sx says `pure`, not "naked"; LLVM `naked` attr is only the B1.0b mechanism). Suite green - (722/0). **Next: B1.0b (real `naked` emission).** + (722/0). +- **B1.0a review-hardening** — adversarial review found generic/pack Function-creation paths + left `is_pure` false (silent framed body for a generic `.pure` instance — returned 42 but + corrupted the stack). Fixed generic.zig + pack.zig (set `is_pure` + asm-only `unreachable` + cap); locked by `examples/1801-concurrency-pure-generic-bail.sx`. The review's `.pure`- + lambda CRITICAL was a false positive (unparseable — `isLambda` breaks on `abi`). Suite + green (723/0). **Next: B1.0b (real `naked` emission).** diff --git a/current/PLAN-FIBERS.md b/current/PLAN-FIBERS.md index 33d7b55a..1172f23c 100644 --- a/current/PLAN-FIBERS.md +++ b/current/PLAN-FIBERS.md @@ -166,7 +166,7 @@ B1.0 (`.pure`) forces these plumbing sites: emit the `.pure` body as the asm block only (no prologue/epilogue/ctx). Pin `1800` aarch64 (`.build {"target":"aarch64-macos"}`) → runs end-to-end (exit 42) on this host, ir-only on a mismatch; capture its `.ir` (asserts `naked` + the asm). Add an x86_64 cross - sibling `examples/1801-concurrency-pure-asm-x86.sx` (`.build {"target":"x86_64-linux"}`, + sibling `examples/1802-concurrency-pure-asm-x86.sx` (`.build {"target":"x86_64-linux"}`, ir-only here). Add a unit test in `emit_llvm.test.zig` asserting the `naked` attribute is present on a `.pure` function. Review the diff (no stray error text). Commit. @@ -232,7 +232,7 @@ asserting program-emitted ordering contracts. > `{"target":"aarch64-macos"}`; on this aarch64 host it runs end-to-end (exit 42), capture > `.ir` + regen (`-Dname=examples/1800-concurrency-pure-asm.sx -Dupdate-goldens`), review the > diff (assert the `.ir` shows the `naked` attr + `mov x0, #42` / `ret`, NO stray error -> text). (3) Add `examples/1801-concurrency-pure-asm-x86.sx` (x86_64 body, `.build +> text). (3) Add `examples/1802-concurrency-pure-asm-x86.sx` (x86_64 body, `.build > {"target":"x86_64-linux"}`, ir-only on this host — requires its `.ir`, now producible). > (4) Add a unit test in `src/ir/emit_llvm.test.zig` asserting the `naked` attribute is > present on an `abi(.pure)` function. Confirm `zig build test` green, commit. NOTE: the diff --git a/examples/1801-concurrency-pure-generic-bail.sx b/examples/1801-concurrency-pure-generic-bail.sx new file mode 100644 index 00000000..ec6634af --- /dev/null +++ b/examples/1801-concurrency-pure-generic-bail.sx @@ -0,0 +1,23 @@ +// Stream B1 (fibers) step B1.0a — regression for an adversarial-review finding. +// +// `abi(.pure)` on a GENERIC function is monomorphized through a different +// Function-creation path (lower/generic.zig) than a plain decl, and originally +// that path left `is_pure` unset — so the emit bail never fired and a framed +// body shipped (it "returned 42" but leaked the prologue's stack adjustment: +// the exact silent corruption the lock exists to prevent). This example pins +// the now-correct behavior: a `.pure` generic instance reaches the loud emit +// bail (build-gating, nonzero exit) just like a plain `.pure` decl. The sibling +// pack-expansion path (lower/pack.zig) was hardened the same way. Host- +// independent (the bail fires before instruction selection), so no `.build` +// pin. B1.0b will turn the plain-decl form (1800) green; this generic case +// stays a bail-lock (a naked generic is exotic and out of B1's scope). +answer :: ($T: Type) -> i64 abi(.pure) { + asm volatile { + #string A + mov x0, #42 + ret +A + }; +} + +main :: () -> i64 { return answer(i64); } diff --git a/examples/expected/1801-concurrency-pure-generic-bail.exit b/examples/expected/1801-concurrency-pure-generic-bail.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic-bail.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1801-concurrency-pure-generic-bail.stderr b/examples/expected/1801-concurrency-pure-generic-bail.stderr new file mode 100644 index 00000000..fd60d092 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic-bail.stderr @@ -0,0 +1 @@ +error: `abi(.pure)` function 'answer__i64' LLVM emission not yet implemented diff --git a/examples/expected/1801-concurrency-pure-generic-bail.stdout b/examples/expected/1801-concurrency-pure-generic-bail.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic-bail.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 4aa4ab55..fce78e26 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -115,6 +115,7 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); _ = func_id; self.builder.currentFunc().has_implicit_ctx = wants_ctx; + self.builder.currentFunc().is_pure = (fd.abi == .pure); // Create entry block const entry_name = self.module.types.internString("entry"); @@ -151,6 +152,13 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name self.ensureTerminator(ret_ty); } self.builder.finalize(); + } else if (self.builder.currentFunc().is_pure) { + // `abi(.pure)`: asm-only body that rets itself — no sx value return. + // Lower the statements + cap with `unreachable` (mirrors the decl path). + // emit_llvm bails on `is_pure` until B1.0b implements `naked` emission. + self.lowerBlock(fd.body); + if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable(); + self.builder.finalize(); } else { // Lower the function body if (ret_ty != .void) { diff --git a/src/ir/lower/pack.zig b/src/ir/lower/pack.zig index 2ea8732c..30ef498c 100644 --- a/src/ir/lower/pack.zig +++ b/src/ir/lower/pack.zig @@ -949,6 +949,7 @@ pub fn monomorphizePackFn( const name_id = self.module.types.internString(owned_name); _ = self.builder.beginFunction(name_id, params.items, ret_ty); self.builder.currentFunc().has_implicit_ctx = wants_ctx; + self.builder.currentFunc().is_pure = (fd.abi == .pure); const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); @@ -1038,7 +1039,13 @@ pub fn monomorphizePackFn( defer self.setCurrentSourceFile(saved_source); if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - if (ret_ty != .void) { + if (self.builder.currentFunc().is_pure) { + // `abi(.pure)`: asm-only body that rets itself — no sx value return. + // Lower statements + cap with `unreachable` (mirrors the decl path). + // emit_llvm bails on `is_pure` until B1.0b implements `naked` emission. + self.lowerBlock(fd.body); + if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable(); + } else if (ret_ty != .void) { const body_val = self.lowerBlockValue(fd.body); if (!self.currentBlockHasTerminator()) { if (body_val) |val| {