fibers B1.0a: plumb abi(.pure), emit bails (lock)

First implementation step of Stream B1 (fibers). Make the inert abi(.pure)
ABI carry an is_pure flag through lowering, with LLVM emission deliberately
bailing loudly until B1.0b — the lock half of the lock->green cadence.

- IR Function.is_pure, set from fd.abi == .pure at both declareFunction
  decl sites.
- funcWantsImplicitCtx skips .pure (no synthetic __sx_ctx, mirroring the
  .c skip): a pure fn reads args from ABI registers, an implicit ctx would
  occupy a register slot the asm doesn't expect.
- both body-lowering paths bypass lowerValueBody for .pure: lower the asm
  body as statements + cap with unreachable. A pure body has no sx return
  (the asm rets itself), so the implicit-return diagnostic must not fire.
- emit_llvm Pass 2 bails loudly when func.is_pure (build-gating nonzero
  exit) rather than emit a framed body, whose epilogue would corrupt a
  context switch's deliberate SP-in != SP-out.

examples/1800-concurrency-pure-asm.sx: one host example (no .build pin --
the bail fires before instruction selection, so it is host-independent),
locked to the bail snapshot. B1.0b flips emit to LLVM's naked attribute +
asm-only body and pins the example per-arch.

The sx-facing name is "pure" throughout (field, diagnostic); LLVM's naked
attribute is only the B1.0b lowering mechanism. Suite green (722/0).
This commit is contained in:
agra
2026-06-20 14:34:53 +03:00
parent 7044b8133b
commit dd363ca877
9 changed files with 207 additions and 100 deletions

View File

@@ -4,29 +4,43 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step
per the cadence rule). New corpus category: `18xx` concurrency. per the cadence rule). New corpus category: `18xx` concurrency.
## Last completed step ## Last completed step
**Carve** — wrote PLAN-FIBERS.md + this checkpoint. Grounded the B1 compiler floor against **B1.0a (`abi(.pure)` lock commit) — DONE.** Plumbed the `is_pure` flag end-to-end and made
the tree (see Decisions). Baseline verified green: `zig build && zig build test` → **721 emit bail loudly:
ran, 0 failed** (one Android-SDK-gated example skipped; the trailing "failed command:" line - IR `Function.is_pure: bool` ([inst.zig](../src/ir/inst.zig)) — set from `fd.abi == .pure`
is the zig listen-protocol echo, not a failure). HEAD `3fad2d5`, tree clean. at both `declareFunction` decl sites ([decl.zig](../src/ir/lower/decl.zig)).
- `funcWantsImplicitCtx` returns false for `.pure` (mirrors the `.c` skip, decl.zig:515) —
a `.pure` fn gets no synthetic `__sx_ctx`.
- Both body-lowering paths bypass `lowerValueBody` for `.pure`: lower the asm body as
statements + cap with `unreachable` (a `.pure` body has no sx return — the asm rets
itself; this avoids the implicit-return diagnostic).
- `emit_llvm` Pass 2 (~line 402) **bails loudly** when `func.is_pure`
("`abi(.pure)` function '…' LLVM emission not yet implemented") via `comptime_failed`
(driver aborts nonzero) — NOT a framed body (whose epilogue would corrupt a context
switch's SP-in ≠ SP-out).
- `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).
- **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**.
## Current state ## Current state
Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1 is **carved, Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1: **B1.0a
not started**. No fibers/Io/scheduler code exists yet. The compiler floor for B1 is grounded: landed**; the `abi(.pure)` ABI is plumbed but emit deliberately bails (B1.0b flips it to
- `abi(.pure)` exists in the `ABI` enum but is **inert** — maps to `.default` CC, emits no real LLVM `naked` emission). No fibers/Io/scheduler code yet. Grounded floor facts:
naked attribute. B1.0 makes it actually emit LLVM `naked`.
- `context` is already an implicit `*Context` param (slot 0) + `push Context` is a stack - `context` is already an implicit `*Context` param (slot 0) + `push Context` is a stack
`alloca`**fiber-local for free**. The only shared root is the `__sx_default_context` `alloca`**fiber-local for free**. Only shared root = `__sx_default_context` global
global (entry-point bind). B1.1 is therefore expected to be a **library convention** (spawn (entry-point bind). B1.1 expected to be a **library convention** (spawn trampoline
trampoline snapshots the spawner's ctx into slot 0), **likely zero compiler change** snapshots the spawner's ctx into slot 0), **likely zero compiler change** probe first.
confirm by probe first. - Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the `.pure` body reuses it.
- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the naked body reuses it.
## Next step ## Next step
**B1.0a (naked-ABI lock commit)** — per PLAN-FIBERS.md "Phases → B1.0 → B1.0a" 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. Add `Function.is_naked`, thread `abi == .pure` kickoff prompt at the bottom of that file. Replace the emit bail with LLVM's `naked`
through `decl.zig` (skip implicit-ctx like `.c`), make `emit_llvm` **BAIL loudly** on a naked attribute + asm-only body; pin `1800` aarch64 (run end-to-end → exit 42, capture `.ir`); add
fn, add the two arch-gated examples (`1800` aarch64 / `1801` x86_64), lock to the bail x86_64 cross sibling `1801` (ir-only); add an `emit_llvm.test.zig` unit test asserting the
diagnostic. STOP before B1.0b (real emission) — separate commit (cadence rule). `naked` attr. Separate commit (cadence rule — B1.0a locked, B1.0b greens).
## Known issues / capability gaps ## Known issues / capability gaps
- **Orthogonal (not a B1 blocker):** default VALUES for comptime params don't bind on - **Orthogonal (not a B1 blocker):** default VALUES for comptime params don't bind on
@@ -50,12 +64,17 @@ diagnostic. STOP before B1.0b (real emission) — separate commit (cadence rule)
`naked` + skip prologue/ctx (mirror the existing `.c` skip), NOT extend the enum (it's `naked` + skip prologue/ctx (mirror the existing `.c` skip), NOT extend the enum (it's
already there, just inert). already there, just inert).
- **`.pure``.c`:** a `.c` epilogue would restore SP from the wrong stack across a context - **`.pure``.c`:** a `.c` epilogue would restore SP from the wrong stack across a context
switch (SP-in ≠ SP-out by design). naked = no prologue/epilogue/frame; the asm emits its switch (SP-in ≠ SP-out by design). `.pure` = no prologue/epilogue/frame; the asm emits its
own `ret`. This is why the switch must be naked. own `ret`. This is why the switch must be `.pure`.
- **B1.0 snapshot scope:** the `naked` attr text is arch-invariant, but a naked body is raw - **Naming:** sx-facing name is **`pure`** (field `is_pure`, the diagnostic). LLVM's `naked`
per-arch asm — so B1.0 needs **two arch-gated examples** (aarch64 + x86_64, `.build` function attribute is only the lowering mechanism (B1.0b) — do not call the function
target-gated, ir-only on mismatch), unlike atomics' single host `.ir`. The `.ir` proves "naked" (user direction).
`naked` + asm emitted, NOT register-save correctness (that's B1.3's stress harness). - **B1.0 snapshot scope:** a `.pure` body is raw per-arch asm; LLVM's `naked` attr text is
arch-invariant. **B1.0a** = one host example locked to the emit bail (host-independent —
fires before instruction selection; no `.build` pin). **B1.0b** = pin aarch64 + add an
x86_64 cross sibling (`.build` target-gated, ir-only on mismatch), like the asm corpus
split. The `.ir` proves the `naked` attr + asm emitted, NOT register-save correctness
(that's B1.3's stress harness).
- **B1.1 grounded as library-only (pending probe):** push frames are stack-`alloca`'d and - **B1.1 grounded as library-only (pending probe):** push frames are stack-`alloca`'d and
the implicit ctx rides slot 0, so a spawn trampoline can pass a snapshotted ctx with no the implicit ctx rides slot 0, so a spawn trampoline can pass a snapshotted ctx with no
compiler change. The design doc's "never raw TLS" guards a non-problem (context is not compiler change. The design doc's "never raw TLS" guards a non-problem (context is not
@@ -73,6 +92,11 @@ diagnostic. STOP before B1.0b (real emission) — separate commit (cadence rule)
(decl.zig:515), `push Context` stack-alloca + slot-0 implicit ctx (stmt.zig:1263, (decl.zig:515), `push Context` stack-alloca + slot-0 implicit ctx (stmt.zig:1263,
lower.zig:259), `__sx_default_context` root (decl.zig:2667/2815), inline-asm corpus lower.zig:259), `__sx_default_context` root (decl.zig:2667/2815), inline-asm corpus
(1645/1651). Corrected the design's `callconv(.naked)` → real `abi(.pure)` spelling and (1645/1651). Corrected the design's `callconv(.naked)` → real `abi(.pure)` spelling and
the B1.0 snapshot story (two arch-gated examples, not one host `.ir`). B1.1 grounded as the B1.0 snapshot story. B1.1 grounded as likely library-only. Baseline green (721/0).
likely library-only. Baseline green (721/0). Stream ready; **B1.0a is the first - **B1.0a** — plumbed `Function.is_pure` (set from `fd.abi == .pure` at both decl sites);
implementation step.** `funcWantsImplicitCtx` skips `.pure` (no implicit ctx, like `.c`); both body-lowering
paths bypass `lowerValueBody` for `.pure` (asm body + `unreachable` cap — no sx return);
`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).**

View File

@@ -1,7 +1,8 @@
# PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler) # PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
> **STATUS: 🚧 carved, not started.** First implementation step = **B1.0a** (naked-ABI > **STATUS: 🚧 in progress.** B1.0a (`abi(.pure)` lock commit) ✅ landed. Next step =
> lock commit). See the kickoff prompt at the bottom. > **B1.0b** (`abi(.pure)` real emission — LLVM `naked` attr). See the kickoff prompt at the
> bottom.
Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the
design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
@@ -29,54 +30,60 @@ authorization.
## Design (grounded against the tree) ## Design (grounded against the tree)
### B1.0 — `abi(.pure)` ⇒ LLVM naked (the one genuinely net-new compiler piece in B1) ### B1.0 — `abi(.pure)` codegen (the one genuinely net-new compiler piece in B1)
The design doc spells `callconv(.naked)`; the **real sx surface is `abi(.pure)`** written The design doc spells this `callconv(.naked)`; the **real sx surface is `abi(.pure)`**
in the postfix slot, `name :: (sig) -> Ret abi(.pure) { asm { … }; }` (cf. written in the postfix slot, `name :: (sig) -> Ret abi(.pure) { asm { … }; }` (cf.
`build_options :: () -> BuildOptions abi(.compiler);` in [build.sx:28](../library/modules/build.sx#L28)). `build_options :: () -> BuildOptions abi(.compiler);` in [build.sx:28](../library/modules/build.sx#L28)).
The sx-facing name is **pure** throughout (field, flag, diagnostics); LLVM's `naked`
function attribute is only the *lowering mechanism* (B1.0b), not what we call the function.
**Grounding (verified — do not re-derive):** **Grounding (verified — do not re-derive):**
- The `ABI` enum **already carries `.pure`**`ABI = enum { default, c, compiler, pure }` - The `ABI` enum **already carries `.pure`**`ABI = enum { default, c, compiler, pure }`
([ast.zig:142](../src/ast.zig#L142)), documented "pure / naked function (inline asm ([ast.zig:142](../src/ast.zig#L142)), documented "pure / naked function (inline asm
body), no calling-convention prologue/epilogue." So B1.0 is **NOT** "extend the enum." body), no calling-convention prologue/epilogue." So B1.0 is **NOT** "extend the enum."
- `.pure` is **inert today**: [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) - `.pure` is **inert today**: [type_resolver.zig:237](../src/ir/type_resolver.zig#L237)
maps `.compiler, .pure → .default` CC, and `emit_llvm` emits **no naked attribute**. So maps `.compiler, .pure → .default` CC, and `emit_llvm` emits **no LLVM `naked`
the net-new work is exactly: **carry `abi == .pure` into the IR `Function`, emit the LLVM attribute**. So the net-new work is exactly: **carry `abi == .pure` into the IR
`naked` attr, and skip the implicit-`Context` / prologue lowering** so the body is just `Function`, emit LLVM's `naked` attr, and skip the implicit-`Context` / prologue
the asm block + its own `ret`. lowering** so the body is just the asm block + its own `ret`.
- The IR `Function` struct ([inst.zig:605](../src/ir/inst.zig#L605)) carries `call_conv` - The IR `Function` struct ([inst.zig:605](../src/ir/inst.zig#L605)) carries `call_conv`
(default/c) + `is_compiler_domain`, but **no naked flag** — add one (`is_naked: bool`). (default/c) + `is_compiler_domain`, but **no pure flag** — add one (`is_pure: bool`).
- Attribute API is in-tree: `nounwind` is set at - Attribute API is in-tree: `nounwind` is set at
[emit_llvm.zig:1339](../src/ir/emit_llvm.zig#L1339) via [emit_llvm.zig:1339](../src/ir/emit_llvm.zig#L1339) via
`LLVMGetEnumAttributeKindForName("nounwind", 8)``LLVMCreateEnumAttribute(ctx, id, 0)` `LLVMGetEnumAttributeKindForName("nounwind", 8)``LLVMCreateEnumAttribute(ctx, id, 0)`
`LLVMAddAttributeAtIndex(func, func_idx_attr /* -1 */, attr)`. The `naked` attr is the `LLVMAddAttributeAtIndex(func, func_idx_attr /* -1 */, attr)`. The LLVM `naked` attr
same shape: `LLVMGetEnumAttributeKindForName("naked", 5)`. is the same shape: `LLVMGetEnumAttributeKindForName("naked", 5)`.
- The `.c` ABI **already skips the implicit ctx** at lowering — `lam.abi == .c` / - The `.c` ABI **already skips the implicit ctx** at lowering — `lam.abi == .c` /
`fd.abi == .c` gates (closure.zig:171, [decl.zig:515](../src/ir/lower/decl.zig#L515)). `fd.abi == .c` gates (closure.zig:171, [decl.zig:515](../src/ir/lower/decl.zig#L515)).
`.pure` must skip it **too** (a naked fn gets no synthetic `__sx_ctx`, no stack frame, `.pure` must skip it **too** (a `.pure` fn gets no synthetic `__sx_ctx`, no stack frame,
no prologue — args arrive in ABI registers and are read directly from asm). no prologue — args arrive in ABI registers and are read directly from asm). The
implicit-return machinery (`lowerValueBody`) must also be bypassed: a `.pure` body has no
sx return (the asm rets itself), so lower its statements and cap the block with
`unreachable`.
- **Inline asm already works end-to-end** (lower→emit→JIT): aarch64 - **Inline asm already works end-to-end** (lower→emit→JIT): aarch64
([examples/1645](../examples/1645-platform-asm-aarch64-add.sx)), x86_64 ([examples/1645](../examples/1645-platform-asm-aarch64-add.sx)), x86_64
([examples/1651](../examples/1651-platform-asm-x86-syscall-write.sx)), global asm, JIT ([examples/1651](../examples/1651-platform-asm-x86-syscall-write.sx)), global asm, JIT
([1653](../examples/1653-platform-asm-global-jit.sx)). `emitInlineAsm` / ([1653](../examples/1653-platform-asm-global-jit.sx)). `emitInlineAsm` /
`LLVMGetInlineAsm` at [ops.zig:915](../src/backend/llvm/ops.zig#L915). The naked body is `LLVMGetInlineAsm` at [ops.zig:915](../src/backend/llvm/ops.zig#L915). The `.pure` body
a single asm block reusing this path. is a single asm block reusing this path.
**`.pure``.c` (design §4.6 context-switch note):** a `.c` epilogue restores SP from the **`.pure``.c` (design §4.6 context-switch note):** a `.c` epilogue restores SP from the
frame; a context switch deliberately makes SP-in ≠ SP-out, so the `.c` epilogue would frame; a context switch deliberately makes SP-in ≠ SP-out, so the `.c` epilogue would
restore from the *wrong* stack. `naked` = no prologue/epilogue/frame — the asm emits its restore from the *wrong* stack. `.pure` = no prologue/epilogue/frame — the asm emits its
own `ret`. This is *why* the switch must be naked, not `.c`. own `ret`. This is *why* the switch must be `.pure`, not `.c`.
**Snapshot story (per the atomics precedent, corrected):** the LLVM `naked` attribute text **Snapshot story (per the atomics precedent):** a `.pure` fn's *body is raw per-arch asm*
is **arch-invariant**, but a naked fn's *body is raw per-arch asm* (it can't be portable — (it can't be portable — that's the point), while LLVM's `naked` attribute text is
that's the point). So unlike atomics (where one host `.ir` sufficed), B1.0 needs **two arch-invariant. **B1.0a** (lock) needs only **one host example** locked to the emit bail —
arch-gated examples** — an aarch64 one and an x86_64 one — exactly like the existing asm the bail fires at the function level *before* any asm/instruction selection, so it is
corpus split (1645 aarch64 / 1651 x86). Each carries a `.build {"target": "<triple>"}` host-independent (no `.build` target pin). **B1.0b** (green) adds emission, pins that
sidecar: it runs end-to-end on a matching host and falls to **ir-only** on a mismatch example aarch64 (`.build {"target": "aarch64-macos"}`, end-to-end on a matching host,
(asserting the `.ir` shows `define … #N` with the `naked` attribute + the asm body). State ir-only on a mismatch), and adds an x86_64 cross sibling — mirroring the existing asm
loudly: **the `.ir` proves the `naked` keyword + asm emitted, NOT that the hand-written corpus split (1645 aarch64 / 1651 x86). The ir-only `.ir` (only producible once emission
register save/restore is correct** — that is the B1.3 switch-stress harness's job, never lands in B1.0b) asserts the `naked` attribute + the asm body. State loudly: **the `.ir`
the corpus's. proves the `naked` keyword + asm emitted, NOT that any hand-written register save/restore
is correct** — that is the B1.3 switch-stress harness's job, never the corpus's.
### B1.1 — per-fiber `context` root (grounding says this is SMALL, likely library-only) ### B1.1 — per-fiber `context` root (grounding says this is SMALL, likely library-only)
@@ -126,39 +133,42 @@ compiler work.** Prerequisite of B1.3 (a fiber needs a valid root before it swit
don't churn every snapshot. don't churn every snapshot.
### Files the compiler floor touches (B1.0 only; B1.1B1.5 are library + tests) ### Files the compiler floor touches (B1.0 only; B1.1B1.5 are library + tests)
B1.0 (naked) forces the exhaustive-switch / plumbing sites: B1.0 (`.pure`) forces these plumbing sites:
- [ast.zig:142](../src/ast.zig#L142) — `ABI.pure` (exists; reference only). - [ast.zig:142](../src/ast.zig#L142) — `ABI.pure` (exists; reference only).
- [inst.zig:605](../src/ir/inst.zig#L605) — add `is_naked: bool = false` to `Function`. - [inst.zig:605](../src/ir/inst.zig#L605) — add `is_pure: bool = false` to `Function`.
- [decl.zig](../src/ir/lower/decl.zig) — set `is_naked` from `fd.abi == .pure`; gate the - [decl.zig](../src/ir/lower/decl.zig) — set `is_pure` from `fd.abi == .pure`; gate the
implicit-ctx / param-stack / prologue lowering off for `.pure` (mirror the `.c` skips at implicit-ctx off for `.pure` in `funcWantsImplicitCtx` (mirror the `.c` skip at
decl.zig:515 + the entry-ctx bind at :2667/:2815 — a naked fn binds no ctx). decl.zig:515) and bypass `lowerValueBody` for `.pure` bodies (lower statements + cap with
- [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) — leave CC `.default` (a naked `unreachable`, in both body-lowering paths) — a `.pure` fn binds no ctx and has no sx
fn-pointer type has no CC of its own; the nakedness is a decl-level emit attribute). return.
- [emit_llvm.zig:1339](../src/ir/emit_llvm.zig#L1339)-adjacent — emit the `naked` enum attr - [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) — leave CC `.default` (a `.pure`
on the LLVM function when `is_naked`; ensure no body-prologue is generated (naked body = fn-pointer type has no CC of its own; pureness is a decl-level emit attribute).
the asm block only). - [emit_llvm.zig:402](../src/ir/emit_llvm.zig#L402) Pass 2 — **B1.0a:** bail loudly when
`func.is_pure` (build-gating). **B1.0b:** instead emit LLVM's `naked` attr (shape per
`nounwind` at emit_llvm.zig:1339) + the asm-only body (no prologue).
- Any `.op`/`Function`-field switch the Zig build flags — let the build tell you. - Any `.op`/`Function`-field switch the Zig build flags — let the build tell you.
--- ---
## Phases (xfail→green steps) ## Phases (xfail→green steps)
### B1.0 — `abi(.pure)` ⇒ LLVM naked ← START HERE ### B1.0 — `abi(.pure)` codegen
- **B1.0a (lock)**carry `abi == .pure` into IR `Function.is_naked`; thread through - **B1.0a (lock) — ✅ DONE** (commit pending). Carried `abi == .pure` into IR
`decl.zig` (skip implicit-ctx for `.pure`, like `.c`); in `emit_llvm` **BAIL loudly** when `Function.is_pure`; threaded through `decl.zig` (`funcWantsImplicitCtx` skips `.pure` like
a naked fn is emitted ("naked (`abi(.pure)`) emission not yet implemented"). Add `.c`; both body-lowering paths bypass `lowerValueBody` for `.pure`, lowering the asm body +
`examples/1800-concurrency-naked-aarch64.sx` (a tiny naked fn with an aarch64 asm body that capping with `unreachable`); `emit_llvm` Pass 2 **bails loudly** on `func.is_pure`
reads its arg from `x0`/returns via `ret`, `.build {"target":"aarch64-macos"}`) **and** ("`abi(.pure)` function '…' LLVM emission not yet implemented", build-gating nonzero exit).
`examples/1801-concurrency-naked-x86_64.sx` (x86_64 sibling, `.build `examples/1800-concurrency-pure-asm.sx` (one host example, no `.build` pin — the bail is
{"target":"x86_64-linux"}`). Seed markers; capture the **bail diagnostic** as the locked host-independent) locked to the bail snapshot. Suite green (722/0).
snapshot (these are ir-only on the non-matching host, so the bail must surface at emit/IR - **B1.0b (green) ← NEXT** — emit LLVM's `naked` attr
time, not run). `zig build && zig build test` green against the bail. Commit. (`LLVMGetEnumAttributeKindForName("naked", 5)` + `LLVMCreateEnumAttribute` +
- **B1.0b (green)** — emit the LLVM `naked` attr (`LLVMGetEnumAttributeKindForName("naked", `LLVMAddAttributeAtIndex` at func index 1; shape per `nounwind` at emit_llvm.zig:1339);
5)` + add at func index 1); ensure the naked body lowers to *only* the asm block (no emit the `.pure` body as the asm block only (no prologue/epilogue/ctx). Pin `1800`
prologue/epilogue, no ctx). On a matching host the example runs (asserts the computed aarch64 (`.build {"target":"aarch64-macos"}`) → runs end-to-end (exit 42) on this host,
value); on a mismatch it's ir-only (assert `.ir` shows `naked` + the asm). Capture both ir-only on a mismatch; capture its `.ir` (asserts `naked` + the asm). Add an x86_64 cross
arch `.ir` snapshots; add a unit test in `emit_llvm.test.zig` asserting the `naked` sibling `examples/1801-concurrency-pure-asm-x86.sx` (`.build {"target":"x86_64-linux"}`,
attribute is present on a `.pure` function. Review the diff (no stray error text). Commit. 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.
### B1.1 — per-fiber `context` root (probe-first; likely zero compiler change) ### B1.1 — per-fiber `context` root (probe-first; likely zero compiler change)
- **B1.1a (probe + lock)** — write a probe (`.sx-tmp/`) + an `18xx` example that snapshots a - **B1.1a (probe + lock)** — write a probe (`.sx-tmp/`) + an `18xx` example that snapshots a
@@ -207,20 +217,25 @@ asserting program-emitted ordering contracts.
it; `18xx` under the deterministic `Io`. it; `18xx` under the deterministic `Io`.
- **B1.5:** `18xx` ordering-contract snapshots under the deterministic `Io`. - **B1.5:** `18xx` ordering-contract snapshots under the deterministic `Io`.
## Kickoff prompt (B1.0a — paste into a fresh session) ## Kickoff prompt (B1.0b — paste into a fresh session)
> Implement Stream B1 step **B1.0a** (naked-ABI lock commit) per > Implement Stream B1 step **B1.0b** (`abi(.pure)` real emission) per
> `current/PLAN-FIBERS.md`. Verify `zig build && zig build test` is green first. Then: (1) > `current/PLAN-FIBERS.md`. Verify `zig build && zig build test` is green first (B1.0a is
> add `is_naked: bool = false` to the IR `Function` struct (`src/ir/inst.zig:605`); (2) in > already landed: `Function.is_pure` plumbed, `decl.zig` skips ctx + bypasses implicit-return
> `src/ir/lower/decl.zig`, set `is_naked` from `fd.abi == .pure` and gate the implicit-`Context` > for `.pure`, `emit_llvm` Pass 2 bails loudly, `examples/1800-concurrency-pure-asm.sx`
> / param-stack / entry-ctx lowering OFF for `.pure` (mirror the existing `fd.abi == .c` > locked to the bail). Then: (1) in `src/ir/emit_llvm.zig` Pass 2 (~line 402), REPLACE the
> skips at decl.zig:515 + the `__sx_default_context` binds at :2667/:2815 — a naked fn binds > `func.is_pure` bail with real emission — set LLVM's `naked` attribute on the function
> no ctx); (3) in `src/ir/emit_llvm.zig`, **BAIL loudly** when emitting a naked function > (`LLVMGetEnumAttributeKindForName("naked", 5)` → `LLVMCreateEnumAttribute(ctx, id, 0)` →
> ("naked (`abi(.pure)`) emission not yet implemented") — do NOT emit the attr yet; (4) add > `LLVMAddAttributeAtIndex(llvm_func, -1, attr)`; shape per the `nounwind` set at
> `examples/1800-concurrency-naked-aarch64.sx` (tiny naked fn, aarch64 asm body, `.build > emit_llvm.zig:1339) and emit the `.pure` body as its asm block only, no prologue/epilogue
> {"target":"aarch64-macos"}`) and `examples/1801-concurrency-naked-x86_64.sx` (x86_64 > (the body already lowers to the inline-asm op + an `unreachable` terminator). (2) Pin
> sibling, `.build {"target":"x86_64-linux"}`); seed the `.exit` markers, capture the > `examples/1800-concurrency-pure-asm.sx` aarch64 with a `.build` sidecar
> emit/IR-time bail diagnostic as the locked snapshot, confirm `zig build test` green, review > `{"target":"aarch64-macos"}`; on this aarch64 host it runs end-to-end (exit 42), capture
> the diff, commit. STOP — B1.0b (real `naked` emission) is the next step; do NOT implement > `.ir` + regen (`-Dname=examples/1800-concurrency-pure-asm.sx -Dupdate-goldens`), review the
> emission in the same commit that adds the examples. Handle any exhaustive-switch site the > diff (assert the `.ir` shows the `naked` attr + `mov x0, #42` / `ret`, NO stray error
> Zig build flags from the new `Function` field. If you hit an UNRELATED compiler bug, file > text). (3) Add `examples/1801-concurrency-pure-asm-x86.sx` (x86_64 body, `.build
> `issues/NNNN`, mark `CHECKPOINT-FIBERS.md` BLOCKED, and STOP. > {"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
> `.ir` proves the keyword + asm emitted, NOT register-save correctness (that's the B1.3
> switch-stress harness). If you hit an UNRELATED compiler bug, file `issues/NNNN`, mark
> `CHECKPOINT-FIBERS.md` BLOCKED, and STOP.

View File

@@ -0,0 +1,24 @@
// Stream B1 (fibers) step B1.0a — LOCK commit for `abi(.pure)`.
//
// 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
// return register and emits its own `ret`. This is the substrate the per-arch
// 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.
answer :: () -> i64 abi(.pure) {
asm volatile {
#string ASM
mov x0, #42
ret
ASM
};
}
main :: () -> i64 { return answer(); }

View File

@@ -0,0 +1 @@
1

View File

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

View File

@@ -0,0 +1 @@

View File

@@ -408,6 +408,17 @@ pub const LLVMEmitter = struct {
// its only references are in comptime code, so DCE drops the leftover // its only references are in comptime code, so DCE drops the leftover
// declaration. See current/PLAN-COMPILER-VM.md (S3). // declaration. See current/PLAN-COMPILER-VM.md (S3).
if (func.is_compiler_domain) continue; 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;
}
self.emitFunction(&func, @intCast(i)); self.emitFunction(&func, @intCast(i));
} }

View File

@@ -640,6 +640,16 @@ pub const Function = struct {
/// drops the leftover declaration. See current/PLAN-COMPILER-VM.md (S3). /// drops the leftover declaration. See current/PLAN-COMPILER-VM.md (S3).
is_compiler_domain: bool = false, is_compiler_domain: bool = false,
/// True for an `abi(.pure)` function — no calling-convention
/// prologue/epilogue/frame, no implicit `__sx_ctx`. Its body is a single
/// inline-asm block that reads args from ABI registers and emits its own
/// `ret` (the context-switch primitive; design §4.6). emit_llvm lowers this
/// via LLVM's `naked` function attribute and generates no frame setup. A
/// `.c` epilogue would restore SP from the wrong stack across a context
/// switch (SP-in ≠ SP-out by design), which is why `.pure` is distinct
/// from `.c`.
is_pure: bool = false,
pub const Param = struct { pub const Param = struct {
name: StringId, name: StringId,
ty: TypeId, ty: TypeId,

View File

@@ -513,6 +513,11 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool { pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
if (!self.implicit_ctx_enabled) return false; if (!self.implicit_ctx_enabled) return false;
if (fd.abi == .c) return false; if (fd.abi == .c) return false;
// An `abi(.pure)` function has no frame and no synthetic params — its body
// is a single asm block reading args from ABI registers. No implicit
// `__sx_ctx` (it would occupy a register slot the asm doesn't expect).
// See Function.is_pure.
if (fd.abi == .pure) return false;
// A BODILESS `abi(.compiler)` decl (compiler-API surface) is dispatched by name // A BODILESS `abi(.compiler)` decl (compiler-API surface) is dispatched by name
// to a Zig/VM handler with exactly the declared args; an implicit `__sx_ctx` // to a Zig/VM handler with exactly the declared args; an implicit `__sx_ctx`
// prepend would shift every arg (breaking the handler's arity check). No sx // prepend would shift every arg (breaking the handler's arity check). No sx
@@ -2310,6 +2315,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
func.source_file = self.current_source_file; func.source_file = self.current_source_file;
func.is_variadic = is_variadic; func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx; func.has_implicit_ctx = wants_ctx;
func.is_pure = (fd.abi == .pure);
self.extern_name_map.put(name, c_name) catch {}; self.extern_name_map.put(name, c_name) catch {};
self.fn_decl_fids.put(fd, fid) catch {}; self.fn_decl_fids.put(fd, fid) catch {};
return; return;
@@ -2323,6 +2329,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
func.source_file = self.current_source_file; func.source_file = self.current_source_file;
func.is_variadic = is_variadic; func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx; func.has_implicit_ctx = wants_ctx;
func.is_pure = (fd.abi == .pure);
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true; if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
// A BODIED `abi(.compiler)` function is a user compiler-domain function (e.g. a // A BODIED `abi(.compiler)` function is a user compiler-domain function (e.g. a
// post-link callback): the VM runs its sx body, but it NEVER runs in the binary // post-link callback): the VM runs its sx body, but it NEVER runs in the binary
@@ -2672,7 +2679,15 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
// Lower the function body (set target_type to return type for implicit returns) // Lower the function body (set target_type to return type for implicit returns)
const saved_target = self.target_type; const saved_target = self.target_type;
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null; self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
if (ret_ty != .void and ret_ty != .noreturn) { if (self.builder.currentFunc().is_pure) {
// `abi(.pure)`: the body is a single asm block that emits its own `ret`.
// There is no sx-level value return — lower the statements and cap the
// block with `unreachable` (control never falls back into sx). This
// bypasses the implicit-return machinery, which would otherwise reject
// the missing return. LLVM emission lands in B1.0b.
self.lowerBlock(fd.body);
if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable();
} else if (ret_ty != .void and ret_ty != .noreturn) {
self.lowerValueBody(fd.body, ret_ty); self.lowerValueBody(fd.body, ret_ty);
} else { } else {
// void / noreturn: no value to return — lower as statements and let // void / noreturn: no value to return — lower as statements and let
@@ -2819,7 +2834,12 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
// Lower the function body, capturing the last expression's value for implicit return // Lower the function body, capturing the last expression's value for implicit return
const saved_target = self.target_type; const saved_target = self.target_type;
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null; self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
if (ret_ty != .void and ret_ty != .noreturn) { if (self.builder.currentFunc().is_pure) {
// `abi(.pure)`: asm-only body that rets itself — see the sibling path
// above. Lower statements, cap with `unreachable`; emission is B1.0b.
self.lowerBlock(fd.body);
if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable();
} else if (ret_ty != .void and ret_ty != .noreturn) {
self.lowerValueBody(fd.body, ret_ty); self.lowerValueBody(fd.body, ret_ty);
} else { } else {
// void / noreturn: no value to return — lower as statements and // void / noreturn: no value to return — lower as statements and