fibers B1.0b: abi(.pure) emits a real LLVM naked function (green)
Flip the B1.0a emit bail to real emission. The emit_llvm declaration pass now adds LLVM's naked + noinline + nounwind attributes for an is_pure function and skips frame-pointer=all (incompatible with a frameless function); Pass 2 emits the body normally, and the naked attribute makes the backend emit it verbatim (the inline asm + its own ret) with no prologue/epilogue. IR shape verified: ; Function Attrs: naked noinline nounwind define internal i64 @answer() #0 { entry: call void asm sideeffect "...ret...", ""() unreachable } The caller invokes it as an ordinary () -> i64 call (.pure is call_conv == .default). - examples/1800-concurrency-pure-asm.sx: now green, aarch64-pinned (.build macos) -> exit 42 + .ir snapshot. - examples/1801-concurrency-pure-generic.sx (renamed from -bail): the generic .pure now emits a correct naked answer__i64 (exit 42), proving generic.zig produces a naked body, not a framed one. - examples/1802-concurrency-pure-asm-x86.sx: x86_64 cross sibling (.build x86_64-linux, ir-only here); .ir locks naked + movl $42,%eax. - unit test in emit_llvm.test.zig asserts the naked attribute is present and frame-pointer absent on an abi(.pure) function. Suite green (724/0).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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); }
|
||||
22
examples/1801-concurrency-pure-generic.sx
Normal file
22
examples/1801-concurrency-pure-generic.sx
Normal file
@@ -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); }
|
||||
19
examples/1802-concurrency-pure-asm-x86.sx
Normal file
19
examples/1802-concurrency-pure-asm-x86.sx
Normal file
@@ -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(); }
|
||||
1
examples/expected/1800-concurrency-pure-asm.build
Normal file
1
examples/expected/1800-concurrency-pure-asm.build
Normal file
@@ -0,0 +1 @@
|
||||
{ "target": "macos" }
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
42
|
||||
|
||||
15
examples/expected/1800-concurrency-pure-asm.ir
Normal file
15
examples/expected/1800-concurrency-pure-asm.ir
Normal file
@@ -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
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
error: `abi(.pure)` function 'answer' LLVM emission not yet implemented
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -1 +0,0 @@
|
||||
error: `abi(.pure)` function 'answer__i64' LLVM emission not yet implemented
|
||||
1
examples/expected/1801-concurrency-pure-generic.build
Normal file
1
examples/expected/1801-concurrency-pure-generic.build
Normal file
@@ -0,0 +1 @@
|
||||
{ "target": "macos" }
|
||||
1
examples/expected/1801-concurrency-pure-generic.exit
Normal file
1
examples/expected/1801-concurrency-pure-generic.exit
Normal file
@@ -0,0 +1 @@
|
||||
42
|
||||
15
examples/expected/1801-concurrency-pure-generic.ir
Normal file
15
examples/expected/1801-concurrency-pure-generic.ir
Normal file
@@ -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
|
||||
}
|
||||
1
examples/expected/1801-concurrency-pure-generic.stdout
Normal file
1
examples/expected/1801-concurrency-pure-generic.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/1802-concurrency-pure-asm-x86.build
Normal file
1
examples/expected/1802-concurrency-pure-asm-x86.build
Normal file
@@ -0,0 +1 @@
|
||||
{ "target": "x86_64-linux" }
|
||||
1
examples/expected/1802-concurrency-pure-asm-x86.exit
Normal file
1
examples/expected/1802-concurrency-pure-asm-x86.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
15
examples/expected/1802-concurrency-pure-asm-x86.ir
Normal file
15
examples/expected/1802-concurrency-pure-asm-x86.ir
Normal file
@@ -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
|
||||
}
|
||||
1
examples/expected/1802-concurrency-pure-asm-x86.stderr
Normal file
1
examples/expected/1802-concurrency-pure-asm-x86.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user