diff --git a/examples/1800-concurrency-pure-asm.sx b/examples/1800-concurrency-pure-asm.sx index baefdf91..629e1be4 100644 --- a/examples/1800-concurrency-pure-asm.sx +++ b/examples/1800-concurrency-pure-asm.sx @@ -1,4 +1,4 @@ -// Stream B1 (fibers) step B1.0a — LOCK commit for `abi(.pure)`. +// Stream B1 (fibers) — `abi(.pure)` emits a naked function end-to-end. // // An `abi(.pure)` function has no calling-convention prologue/epilogue/frame // and no implicit `__sx_ctx`: its body is a single asm block that sets the @@ -6,12 +6,13 @@ // fiber context-switch is built on (design §4.6) — a `.c` epilogue would // restore SP from the wrong stack across a switch (SP-in ≠ SP-out by design). // -// This commit only plumbs the `is_pure` flag through lowering; LLVM emission -// (the `naked` attribute + asm-only body) is NOT implemented yet, so emit bails -// LOUDLY (build-gating, nonzero exit) rather than emit a framed body. The bail -// is at the function level, before any asm/instruction selection, so it is -// host-independent (no `.build` target pin needed until B1.0b adds emission). -// B1.0b flips this to a green, aarch64-pinned end-to-end run. +// Lowered via LLVM's `naked` function attribute: the body is emitted verbatim +// (the inline asm + its own `ret`) with NO frame setup; the IR shows +// `attributes #N = { naked noinline nounwind }` and the bare asm. aarch64-pinned +// (the asm body is per-arch); runs end-to-end here (exit 42), ir-only on a +// mismatch. See the x86_64 sibling 1802. NOTE: the `.ir` proves the `naked` +// keyword + asm emitted, NOT register-save correctness (that's the B1.3 +// switch-stress harness's job). answer :: () -> i64 abi(.pure) { asm volatile { #string ASM diff --git a/examples/1801-concurrency-pure-generic-bail.sx b/examples/1801-concurrency-pure-generic-bail.sx deleted file mode 100644 index ec6634af..00000000 --- a/examples/1801-concurrency-pure-generic-bail.sx +++ /dev/null @@ -1,23 +0,0 @@ -// 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/1801-concurrency-pure-generic.sx b/examples/1801-concurrency-pure-generic.sx new file mode 100644 index 00000000..491946a1 --- /dev/null +++ b/examples/1801-concurrency-pure-generic.sx @@ -0,0 +1,22 @@ +// Stream B1 (fibers) — `abi(.pure)` on a GENERIC function emits a correct naked +// body (regression for an adversarial-review finding). +// +// A generic function is monomorphized through a different Function-creation path +// (lower/generic.zig) than a plain decl. That path originally left `is_pure` +// unset, so a generic `abi(.pure)` instance silently shipped a FRAMED body — it +// returned 42 but leaked the prologue's stack adjustment (the exact SP-in ≠ +// SP-out corruption the `.pure` ABI exists to avoid). generic.zig (and the +// sibling pack-expansion path in pack.zig) now set `is_pure` and emit the +// asm-only naked body. This pins that: the monomorphized `answer__i64` is a +// proper naked function (no frame), returning 42. aarch64-pinned (the asm body +// is per-arch); runs end-to-end on a matching host, ir-only elsewhere. +answer :: ($T: Type) -> i64 abi(.pure) { + asm volatile { + #string A + mov x0, #42 + ret +A + }; +} + +main :: () -> i64 { return answer(i64); } diff --git a/examples/1802-concurrency-pure-asm-x86.sx b/examples/1802-concurrency-pure-asm-x86.sx new file mode 100644 index 00000000..2f562545 --- /dev/null +++ b/examples/1802-concurrency-pure-asm-x86.sx @@ -0,0 +1,19 @@ +// Stream B1 (fibers) — x86_64 sibling of 1800: `abi(.pure)` emits a naked +// function whose body is raw x86_64 asm (returns 42 in eax, then its own `ret`). +// +// Lowered via LLVM's `naked` attribute (no prologue/epilogue/frame). x86_64- +// pinned via `.build`: ir-only on a non-x86 host — the `.ir` snapshot locks the +// `naked` attribute + the bare asm body (`movl $42, %eax` / `ret`) and the +// frame-pointer-free attribute set — and runs end-to-end (exit 42) on +// x86_64-linux. The IR text (the `naked` attribute, the `call void asm`) is +// target-independent; only the asm string differs from the aarch64 1800. +answer :: () -> i64 abi(.pure) { + asm volatile { + #string A + movl $42, %eax + ret +A + }; +} + +main :: () -> i64 { return answer(); } diff --git a/examples/expected/1800-concurrency-pure-asm.build b/examples/expected/1800-concurrency-pure-asm.build new file mode 100644 index 00000000..42e24dd2 --- /dev/null +++ b/examples/expected/1800-concurrency-pure-asm.build @@ -0,0 +1 @@ +{ "target": "macos" } diff --git a/examples/expected/1800-concurrency-pure-asm.exit b/examples/expected/1800-concurrency-pure-asm.exit index d00491fd..d81cc071 100644 --- a/examples/expected/1800-concurrency-pure-asm.exit +++ b/examples/expected/1800-concurrency-pure-asm.exit @@ -1 +1 @@ -1 +42 diff --git a/examples/expected/1800-concurrency-pure-asm.ir b/examples/expected/1800-concurrency-pure-asm.ir new file mode 100644 index 00000000..ca682f50 --- /dev/null +++ b/examples/expected/1800-concurrency-pure-asm.ir @@ -0,0 +1,15 @@ + +; Function Attrs: naked noinline nounwind +define internal i64 @answer() #0 { +entry: + call void asm sideeffect " mov x0, #42\0A ret\0A", ""() + unreachable +} + +; Function Attrs: nounwind +define i32 @main() #1 { +entry: + %call = call i64 @answer() + %ca.tr = trunc i64 %call to i32 + ret i32 %ca.tr +} diff --git a/examples/expected/1800-concurrency-pure-asm.stderr b/examples/expected/1800-concurrency-pure-asm.stderr index 4c509e59..8b137891 100644 --- a/examples/expected/1800-concurrency-pure-asm.stderr +++ b/examples/expected/1800-concurrency-pure-asm.stderr @@ -1 +1 @@ -error: `abi(.pure)` function 'answer' LLVM emission not yet implemented + diff --git a/examples/expected/1801-concurrency-pure-generic-bail.exit b/examples/expected/1801-concurrency-pure-generic-bail.exit deleted file mode 100644 index d00491fd..00000000 --- a/examples/expected/1801-concurrency-pure-generic-bail.exit +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/expected/1801-concurrency-pure-generic-bail.stderr b/examples/expected/1801-concurrency-pure-generic-bail.stderr deleted file mode 100644 index fd60d092..00000000 --- a/examples/expected/1801-concurrency-pure-generic-bail.stderr +++ /dev/null @@ -1 +0,0 @@ -error: `abi(.pure)` function 'answer__i64' LLVM emission not yet implemented diff --git a/examples/expected/1801-concurrency-pure-generic.build b/examples/expected/1801-concurrency-pure-generic.build new file mode 100644 index 00000000..42e24dd2 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic.build @@ -0,0 +1 @@ +{ "target": "macos" } diff --git a/examples/expected/1801-concurrency-pure-generic.exit b/examples/expected/1801-concurrency-pure-generic.exit new file mode 100644 index 00000000..d81cc071 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic.exit @@ -0,0 +1 @@ +42 diff --git a/examples/expected/1801-concurrency-pure-generic.ir b/examples/expected/1801-concurrency-pure-generic.ir new file mode 100644 index 00000000..4b55b284 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic.ir @@ -0,0 +1,15 @@ + +; Function Attrs: nounwind +define i32 @main() #0 { +entry: + %call = call i64 @answer__i64() + %ca.tr = trunc i64 %call to i32 + ret i32 %ca.tr +} + +; Function Attrs: naked noinline nounwind +define internal i64 @answer__i64() #1 { +entry: + call void asm sideeffect " mov x0, #42\0A ret\0A", ""() + unreachable +} diff --git a/examples/expected/1801-concurrency-pure-generic-bail.stdout b/examples/expected/1801-concurrency-pure-generic.stderr similarity index 100% rename from examples/expected/1801-concurrency-pure-generic-bail.stdout rename to examples/expected/1801-concurrency-pure-generic.stderr diff --git a/examples/expected/1801-concurrency-pure-generic.stdout b/examples/expected/1801-concurrency-pure-generic.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1801-concurrency-pure-generic.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/1802-concurrency-pure-asm-x86.build b/examples/expected/1802-concurrency-pure-asm-x86.build new file mode 100644 index 00000000..7fbbed1a --- /dev/null +++ b/examples/expected/1802-concurrency-pure-asm-x86.build @@ -0,0 +1 @@ +{ "target": "x86_64-linux" } diff --git a/examples/expected/1802-concurrency-pure-asm-x86.exit b/examples/expected/1802-concurrency-pure-asm-x86.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1802-concurrency-pure-asm-x86.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1802-concurrency-pure-asm-x86.ir b/examples/expected/1802-concurrency-pure-asm-x86.ir new file mode 100644 index 00000000..442bee96 --- /dev/null +++ b/examples/expected/1802-concurrency-pure-asm-x86.ir @@ -0,0 +1,15 @@ + +; Function Attrs: naked noinline nounwind +define internal i64 @answer() #0 { +entry: + call void asm sideeffect " movl $$42, %eax\0A ret\0A", ""() + unreachable +} + +; Function Attrs: nounwind +define i32 @main() #1 { +entry: + %call = call i64 @answer() + %ca.tr = trunc i64 %call to i32 + ret i32 %ca.tr +} diff --git a/examples/expected/1802-concurrency-pure-asm-x86.stderr b/examples/expected/1802-concurrency-pure-asm-x86.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1802-concurrency-pure-asm-x86.stderr @@ -0,0 +1 @@ + diff --git a/src/ir/emit_llvm.test.zig b/src/ir/emit_llvm.test.zig index f5b3a404..636b83bb 100644 --- a/src/ir/emit_llvm.test.zig +++ b/src/ir/emit_llvm.test.zig @@ -1324,3 +1324,40 @@ test "emit: reflectArgRepr surfaces .unresolved for an unresolvable reflection a try std.testing.expectEqual(LLVMEmitter.ReflectArgRepr.unresolved, emitter.reflectArgRepr(bogus)); try std.testing.expect(emitter.reflectArgRepr(bogus) != .bare); } + +test "emit: abi(.pure) function gets the naked attribute (no frame-pointer)" { + const alloc = std.testing.allocator; + var module = Module.init(alloc); + defer module.deinit(); + + var b = Builder.init(&module); + + // func answer() -> i64 abi(.pure) { asm volatile { "ret" }; unreachable } + // The naked attribute is keyed off Function.is_pure in the declaration pass, + // independent of the body — a minimal asm + unreachable body suffices. + _ = b.beginFunction(str(&module, "answer"), &.{}, .i64); + b.currentFunc().is_pure = true; + const entry = b.appendBlock(str(&module, "entry"), &.{}); + b.switchToBlock(entry); + + b.emitVoid(.{ .inline_asm = .{ + .template = str(&module, "ret"), + .operands = &.{}, + .clobbers = &.{}, + .has_side_effects = true, + } }, .void); + b.emitUnreachable(); + b.finalize(); + + var emitter = LLVMEmitter.init(alloc, &module, "test_pure", .{}); + defer emitter.deinit(); + emitter.emit(); + + try std.testing.expect(emitter.verify()); + + const ir_str = emitter.dumpToString(); + // The naked attribute is present; a naked function carries no frame-pointer + // attribute (incompatible with a frameless function). + try std.testing.expect(std.mem.indexOf(u8, ir_str, "naked") != null); + try std.testing.expect(std.mem.indexOf(u8, ir_str, "frame-pointer") == null); +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 71dfb0f6..e5dc876a 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -408,17 +408,9 @@ pub const LLVMEmitter = struct { // its only references are in comptime code, so DCE drops the leftover // declaration. See current/PLAN-COMPILER-VM.md (S3). if (func.is_compiler_domain) continue; - // B1.0a (lock): `abi(.pure)` emission is not implemented yet — the - // LLVM `naked` attribute + asm-only body land in B1.0b. Bail LOUDLY - // (build-gating, like a comptime failure) rather than emit a framed - // body, whose prologue/epilogue would corrupt the deliberate - // SP-in ≠ SP-out of a context switch. See current/PLAN-FIBERS.md. - if (func.is_pure) { - const fname = self.ir_mod.types.getString(func.name); - std.debug.print("error: `abi(.pure)` function '{s}' LLVM emission not yet implemented\n", .{fname}); - self.comptime_failed = true; - continue; - } + // `abi(.pure)` functions emit normally — the `naked` attribute (set + // in the declaration pass) makes the backend emit the body (inline + // asm + its own `ret`) with no prologue/epilogue. See Function.is_pure. self.emitFunction(&func, @intCast(i)); } @@ -1334,22 +1326,37 @@ pub const LLVMEmitter = struct { // Add frame-pointer and nounwind attributes for correct ARM64 codegen { - const fp_kind = "frame-pointer"; - const fp_val = "all"; - const fp_attr = c.LLVMCreateStringAttribute( - self.context, - fp_kind.ptr, - @intCast(fp_kind.len), - fp_val.ptr, - @intCast(fp_val.len), - ); const func_idx_attr: c.LLVMAttributeIndex = @bitCast(@as(i32, -1)); - c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, fp_attr); + if (func.is_pure) { + // `abi(.pure)`: emit via LLVM's `naked` attribute — the backend + // emits the body verbatim (our inline asm + its own `ret`) with + // NO prologue/epilogue/frame. Do NOT request `frame-pointer` + // (incompatible with a frameless function). `noinline` keeps the + // asm body out of a framed caller; `nounwind` — naked asm never + // unwinds. See Function.is_pure / current/PLAN-FIBERS.md. + const naked_id = c.LLVMGetEnumAttributeKindForName("naked", 5); + c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, c.LLVMCreateEnumAttribute(self.context, naked_id, 0)); + const noinline_id = c.LLVMGetEnumAttributeKindForName("noinline", 8); + c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, c.LLVMCreateEnumAttribute(self.context, noinline_id, 0)); + const nounwind_id = c.LLVMGetEnumAttributeKindForName("nounwind", 8); + c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, c.LLVMCreateEnumAttribute(self.context, nounwind_id, 0)); + } else { + const fp_kind = "frame-pointer"; + const fp_val = "all"; + const fp_attr = c.LLVMCreateStringAttribute( + self.context, + fp_kind.ptr, + @intCast(fp_kind.len), + fp_val.ptr, + @intCast(fp_val.len), + ); + c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, fp_attr); - // Add nounwind - const nounwind_id = c.LLVMGetEnumAttributeKindForName("nounwind", 8); - const nounwind_attr = c.LLVMCreateEnumAttribute(self.context, nounwind_id, 0); - c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, nounwind_attr); + // Add nounwind + const nounwind_id = c.LLVMGetEnumAttributeKindForName("nounwind", 8); + const nounwind_attr = c.LLVMCreateEnumAttribute(self.context, nounwind_id, 0); + c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, nounwind_attr); + } } // Apple ARM64 ABI for >16B non-HFA composites: pass by reference