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:
agra
2026-06-20 16:36:12 +03:00
parent 40424df1b8
commit 4b384788e6
21 changed files with 172 additions and 59 deletions

View File

@@ -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

View File

@@ -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); }

View 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); }

View 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(); }

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -1 +1 @@
1
42

View 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
}

View File

@@ -1 +1 @@
error: `abi(.pure)` function 'answer' LLVM emission not yet implemented

View File

@@ -1 +0,0 @@
error: `abi(.pure)` function 'answer__i64' LLVM emission not yet implemented

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1 @@
42

View 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
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
{ "target": "x86_64-linux" }

View File

@@ -0,0 +1 @@
0

View 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
}

View File

@@ -0,0 +1 @@