fibers B1.0a: close generic/pack is_pure gap (review)
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).
This commit is contained in:
@@ -20,10 +20,22 @@ emit bail loudly:
|
|||||||
- `examples/1800-concurrency-pure-asm.sx` — one host example (no `.build` pin; the bail is
|
- `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
|
host-independent, fires before any asm/instruction selection), locked to the bail snapshot
|
||||||
(exit 1, empty stdout, the loud diagnostic on stderr).
|
(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
|
- **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
|
`naked` attribute is only the B1.0b lowering mechanism (per user direction — don't call
|
||||||
the function "naked").
|
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
|
## Current state
|
||||||
Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1: **B1.0a
|
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
|
**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`
|
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
|
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).
|
`naked` attr. Separate commit (cadence rule — B1.0a locked, B1.0b greens).
|
||||||
|
|
||||||
## Known issues / capability gaps
|
## 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`
|
`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
|
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
|
(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).**
|
||||||
|
|||||||
@@ -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`
|
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,
|
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
|
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
|
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.
|
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
|
> `{"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
|
> `.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
|
> 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).
|
> {"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
|
> (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
|
> present on an `abi(.pure)` function. Confirm `zig build test` green, commit. NOTE: the
|
||||||
|
|||||||
23
examples/1801-concurrency-pure-generic-bail.sx
Normal file
23
examples/1801-concurrency-pure-generic-bail.sx
Normal file
@@ -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); }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
error: `abi(.pure)` function 'answer__i64' LLVM emission not yet implemented
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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);
|
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
||||||
_ = func_id;
|
_ = func_id;
|
||||||
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
|
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
|
||||||
|
self.builder.currentFunc().is_pure = (fd.abi == .pure);
|
||||||
|
|
||||||
// Create entry block
|
// Create entry block
|
||||||
const entry_name = self.module.types.internString("entry");
|
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.ensureTerminator(ret_ty);
|
||||||
}
|
}
|
||||||
self.builder.finalize();
|
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 {
|
} else {
|
||||||
// Lower the function body
|
// Lower the function body
|
||||||
if (ret_ty != .void) {
|
if (ret_ty != .void) {
|
||||||
|
|||||||
@@ -949,6 +949,7 @@ pub fn monomorphizePackFn(
|
|||||||
const name_id = self.module.types.internString(owned_name);
|
const name_id = self.module.types.internString(owned_name);
|
||||||
_ = self.builder.beginFunction(name_id, params.items, ret_ty);
|
_ = self.builder.beginFunction(name_id, params.items, ret_ty);
|
||||||
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
|
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_name = self.module.types.internString("entry");
|
||||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||||
@@ -1038,7 +1039,13 @@ pub fn monomorphizePackFn(
|
|||||||
defer self.setCurrentSourceFile(saved_source);
|
defer self.setCurrentSourceFile(saved_source);
|
||||||
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
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);
|
const body_val = self.lowerBlockValue(fd.body);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
if (body_val) |val| {
|
if (body_val) |val| {
|
||||||
|
|||||||
Reference in New Issue
Block a user