From dd363ca877639cca82820ce665dd469e25c1aa03 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 14:34:53 +0300 Subject: [PATCH] fibers B1.0a: plumb abi(.pure), emit bails (lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- current/CHECKPOINT-FIBERS.md | 78 ++++++--- current/PLAN-FIBERS.md | 157 ++++++++++-------- examples/1800-concurrency-pure-asm.sx | 24 +++ .../expected/1800-concurrency-pure-asm.exit | 1 + .../expected/1800-concurrency-pure-asm.stderr | 1 + .../expected/1800-concurrency-pure-asm.stdout | 1 + src/ir/emit_llvm.zig | 11 ++ src/ir/inst.zig | 10 ++ src/ir/lower/decl.zig | 24 ++- 9 files changed, 207 insertions(+), 100 deletions(-) create mode 100644 examples/1800-concurrency-pure-asm.sx create mode 100644 examples/expected/1800-concurrency-pure-asm.exit create mode 100644 examples/expected/1800-concurrency-pure-asm.stderr create mode 100644 examples/expected/1800-concurrency-pure-asm.stdout diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index a3c2d816..9db7ef09 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -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. ## Last completed step -**Carve** — wrote PLAN-FIBERS.md + this checkpoint. Grounded the B1 compiler floor against -the tree (see Decisions). Baseline verified green: `zig build && zig build test` → **721 -ran, 0 failed** (one Android-SDK-gated example skipped; the trailing "failed command:" line -is the zig listen-protocol echo, not a failure). HEAD `3fad2d5`, tree clean. +**B1.0a (`abi(.pure)` lock commit) — DONE.** Plumbed the `is_pure` flag end-to-end and made +emit bail loudly: +- IR `Function.is_pure: bool` ([inst.zig](../src/ir/inst.zig)) — set from `fd.abi == .pure` + 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 -Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1 is **carved, -not started**. No fibers/Io/scheduler code exists yet. The compiler floor for B1 is grounded: -- `abi(.pure)` exists in the `ABI` enum but is **inert** — maps to `.default` CC, emits no - naked attribute. B1.0 makes it actually emit LLVM `naked`. +Stream A (atomics) is feature-complete (✅) and unblocks B2-channels. Stream B1: **B1.0a +landed**; the `abi(.pure)` ABI is plumbed but emit deliberately bails (B1.0b flips it to +real LLVM `naked` emission). No fibers/Io/scheduler code yet. Grounded floor facts: - `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` - global (entry-point bind). B1.1 is therefore expected to be a **library convention** (spawn - trampoline snapshots the spawner's ctx into slot 0), **likely zero compiler change** — - confirm by probe first. -- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the naked body reuses it. + `alloca` ⇒ **fiber-local for free**. Only shared root = `__sx_default_context` global + (entry-point bind). B1.1 expected to be a **library convention** (spawn trampoline + snapshots the spawner's ctx into slot 0), **likely zero compiler change** — probe first. +- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the `.pure` body reuses it. ## Next step -**B1.0a (naked-ABI lock commit)** — per PLAN-FIBERS.md "Phases → B1.0 → B1.0a" and the -kickoff prompt at the bottom of that file. Add `Function.is_naked`, thread `abi == .pure` -through `decl.zig` (skip implicit-ctx like `.c`), make `emit_llvm` **BAIL loudly** on a naked -fn, add the two arch-gated examples (`1800` aarch64 / `1801` x86_64), lock to the bail -diagnostic. STOP before B1.0b (real emission) — separate commit (cadence rule). +**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` +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 +`naked` attr. Separate commit (cadence rule — B1.0a locked, B1.0b greens). ## Known issues / capability gaps - **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 already there, just inert). - **`.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 - own `ret`. This is why the switch must be naked. -- **B1.0 snapshot scope:** the `naked` attr text is arch-invariant, but a naked body is raw - per-arch asm — so B1.0 needs **two arch-gated examples** (aarch64 + x86_64, `.build` - target-gated, ir-only on mismatch), unlike atomics' single host `.ir`. The `.ir` proves - `naked` + asm emitted, NOT register-save correctness (that's B1.3's stress harness). + 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 `.pure`. +- **Naming:** sx-facing name is **`pure`** (field `is_pure`, the diagnostic). LLVM's `naked` + function attribute is only the lowering mechanism (B1.0b) — do not call the function + "naked" (user direction). +- **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 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 @@ -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, 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 - the B1.0 snapshot story (two arch-gated examples, not one host `.ir`). B1.1 grounded as - likely library-only. Baseline green (721/0). Stream ready; **B1.0a is the first - implementation step.** + the B1.0 snapshot story. B1.1 grounded as likely library-only. Baseline green (721/0). +- **B1.0a** — plumbed `Function.is_pure` (set from `fd.abi == .pure` at both decl sites); + `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).** diff --git a/current/PLAN-FIBERS.md b/current/PLAN-FIBERS.md index 689fc159..33d7b55a 100644 --- a/current/PLAN-FIBERS.md +++ b/current/PLAN-FIBERS.md @@ -1,7 +1,8 @@ # PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler) -> **STATUS: 🚧 carved, not started.** First implementation step = **B1.0a** (naked-ABI -> lock commit). See the kickoff prompt at the bottom. +> **STATUS: 🚧 in progress.** B1.0a (`abi(.pure)` lock commit) ✅ landed. Next step = +> **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 design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) @@ -29,54 +30,60 @@ authorization. ## 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 -in the postfix slot, `name :: (sig) -> Ret abi(.pure) { asm { … }; }` (cf. +The design doc spells this `callconv(.naked)`; the **real sx surface is `abi(.pure)`** — +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)). +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):** - 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 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) - maps `.compiler, .pure → .default` CC, and `emit_llvm` emits **no naked attribute**. So - the net-new work is exactly: **carry `abi == .pure` into the IR `Function`, emit the LLVM - `naked` attr, and skip the implicit-`Context` / prologue lowering** so the body is just - the asm block + its own `ret`. + maps `.compiler, .pure → .default` CC, and `emit_llvm` emits **no LLVM `naked` + attribute**. So the net-new work is exactly: **carry `abi == .pure` into the IR + `Function`, emit LLVM's `naked` attr, and skip the implicit-`Context` / prologue + 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` - (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 [emit_llvm.zig:1339](../src/ir/emit_llvm.zig#L1339) via `LLVMGetEnumAttributeKindForName("nounwind", 8)` → `LLVMCreateEnumAttribute(ctx, id, 0)` - → `LLVMAddAttributeAtIndex(func, func_idx_attr /* -1 */, attr)`. The `naked` attr is the - same shape: `LLVMGetEnumAttributeKindForName("naked", 5)`. + → `LLVMAddAttributeAtIndex(func, func_idx_attr /* -1 */, attr)`. The LLVM `naked` attr + is the same shape: `LLVMGetEnumAttributeKindForName("naked", 5)`. - 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)). - `.pure` must skip it **too** (a naked fn gets no synthetic `__sx_ctx`, no stack frame, - no prologue — args arrive in ABI registers and are read directly from asm). + `.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). 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 ([examples/1645](../examples/1645-platform-asm-aarch64-add.sx)), x86_64 ([examples/1651](../examples/1651-platform-asm-x86-syscall-write.sx)), global asm, JIT ([1653](../examples/1653-platform-asm-global-jit.sx)). `emitInlineAsm` / - `LLVMGetInlineAsm` at [ops.zig:915](../src/backend/llvm/ops.zig#L915). The naked body is - a single asm block reusing this path. + `LLVMGetInlineAsm` at [ops.zig:915](../src/backend/llvm/ops.zig#L915). The `.pure` body + is a single asm block reusing this path. **`.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 -restore from the *wrong* stack. `naked` = no prologue/epilogue/frame — the asm emits its -own `ret`. This is *why* the switch must be naked, not `.c`. +restore from the *wrong* stack. `.pure` = no prologue/epilogue/frame — the asm emits its +own `ret`. This is *why* the switch must be `.pure`, not `.c`. -**Snapshot story (per the atomics precedent, corrected):** the LLVM `naked` attribute text -is **arch-invariant**, but a naked fn's *body is raw per-arch asm* (it can't be portable — -that's the point). So unlike atomics (where one host `.ir` sufficed), B1.0 needs **two -arch-gated examples** — an aarch64 one and an x86_64 one — exactly like the existing asm -corpus split (1645 aarch64 / 1651 x86). Each carries a `.build {"target": ""}` -sidecar: it runs end-to-end on a matching host and falls to **ir-only** on a mismatch -(asserting the `.ir` shows `define … #N` with the `naked` attribute + the asm body). State -loudly: **the `.ir` proves the `naked` keyword + asm emitted, NOT that the hand-written -register save/restore is correct** — that is the B1.3 switch-stress harness's job, never -the corpus's. +**Snapshot story (per the atomics precedent):** a `.pure` fn's *body is raw per-arch asm* +(it can't be portable — that's the point), while LLVM's `naked` attribute text is +arch-invariant. **B1.0a** (lock) needs only **one host example** locked to the emit bail — +the bail fires at the function level *before* any asm/instruction selection, so it is +host-independent (no `.build` target pin). **B1.0b** (green) adds emission, pins that +example aarch64 (`.build {"target": "aarch64-macos"}`, end-to-end on a matching host, +ir-only on a mismatch), and adds an x86_64 cross sibling — mirroring the existing asm +corpus split (1645 aarch64 / 1651 x86). The ir-only `.ir` (only producible once emission +lands in B1.0b) asserts the `naked` attribute + the asm body. State loudly: **the `.ir` +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) @@ -126,39 +133,42 @@ compiler work.** Prerequisite of B1.3 (a fiber needs a valid root before it swit don't churn every snapshot. ### Files the compiler floor touches (B1.0 only; B1.1–B1.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). -- [inst.zig:605](../src/ir/inst.zig#L605) — add `is_naked: bool = false` to `Function`. -- [decl.zig](../src/ir/lower/decl.zig) — set `is_naked` from `fd.abi == .pure`; gate the - implicit-ctx / param-stack / prologue lowering off for `.pure` (mirror the `.c` skips at - decl.zig:515 + the entry-ctx bind at :2667/:2815 — a naked fn binds no ctx). -- [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) — leave CC `.default` (a naked - fn-pointer type has no CC of its own; the nakedness is a decl-level emit attribute). -- [emit_llvm.zig:1339](../src/ir/emit_llvm.zig#L1339)-adjacent — emit the `naked` enum attr - on the LLVM function when `is_naked`; ensure no body-prologue is generated (naked body = - the asm block only). +- [inst.zig:605](../src/ir/inst.zig#L605) — add `is_pure: bool = false` to `Function`. +- [decl.zig](../src/ir/lower/decl.zig) — set `is_pure` from `fd.abi == .pure`; gate the + implicit-ctx off for `.pure` in `funcWantsImplicitCtx` (mirror the `.c` skip at + decl.zig:515) and bypass `lowerValueBody` for `.pure` bodies (lower statements + cap with + `unreachable`, in both body-lowering paths) — a `.pure` fn binds no ctx and has no sx + return. +- [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) — leave CC `.default` (a `.pure` + fn-pointer type has no CC of its own; pureness is a decl-level emit attribute). +- [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. --- ## Phases (xfail→green steps) -### B1.0 — `abi(.pure)` ⇒ LLVM naked ← START HERE -- **B1.0a (lock)** — carry `abi == .pure` into IR `Function.is_naked`; thread through - `decl.zig` (skip implicit-ctx for `.pure`, like `.c`); in `emit_llvm` **BAIL loudly** when - a naked fn is emitted ("naked (`abi(.pure)`) emission not yet implemented"). Add - `examples/1800-concurrency-naked-aarch64.sx` (a tiny naked fn with an aarch64 asm body that - reads its arg from `x0`/returns via `ret`, `.build {"target":"aarch64-macos"}`) **and** - `examples/1801-concurrency-naked-x86_64.sx` (x86_64 sibling, `.build - {"target":"x86_64-linux"}`). Seed markers; capture the **bail diagnostic** as the locked - snapshot (these are ir-only on the non-matching host, so the bail must surface at emit/IR - time, not run). `zig build && zig build test` green against the bail. Commit. -- **B1.0b (green)** — emit the LLVM `naked` attr (`LLVMGetEnumAttributeKindForName("naked", - 5)` + add at func index −1); ensure the naked body lowers to *only* the asm block (no - prologue/epilogue, no ctx). On a matching host the example runs (asserts the computed - value); on a mismatch it's ir-only (assert `.ir` shows `naked` + the asm). Capture both - arch `.ir` snapshots; 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.0 — `abi(.pure)` codegen +- **B1.0a (lock) — ✅ DONE** (commit pending). Carried `abi == .pure` into IR + `Function.is_pure`; threaded through `decl.zig` (`funcWantsImplicitCtx` skips `.pure` like + `.c`; both body-lowering paths bypass `lowerValueBody` for `.pure`, lowering the asm body + + capping with `unreachable`); `emit_llvm` Pass 2 **bails loudly** on `func.is_pure` + ("`abi(.pure)` function '…' LLVM emission not yet implemented", build-gating nonzero exit). + `examples/1800-concurrency-pure-asm.sx` (one host example, no `.build` pin — the bail is + host-independent) locked to the bail snapshot. Suite green (722/0). +- **B1.0b (green) ← NEXT** — emit LLVM's `naked` attr + (`LLVMGetEnumAttributeKindForName("naked", 5)` + `LLVMCreateEnumAttribute` + + `LLVMAddAttributeAtIndex` at func index −1; shape per `nounwind` at emit_llvm.zig:1339); + 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, + 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"}`, + 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.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`. - **B1.5:** `18xx` ordering-contract snapshots under the deterministic `Io`. -## Kickoff prompt (B1.0a — paste into a fresh session) -> Implement Stream B1 step **B1.0a** (naked-ABI lock commit) per -> `current/PLAN-FIBERS.md`. Verify `zig build && zig build test` is green first. Then: (1) -> add `is_naked: bool = false` to the IR `Function` struct (`src/ir/inst.zig:605`); (2) in -> `src/ir/lower/decl.zig`, set `is_naked` from `fd.abi == .pure` and gate the implicit-`Context` -> / param-stack / entry-ctx lowering OFF for `.pure` (mirror the existing `fd.abi == .c` -> skips at decl.zig:515 + the `__sx_default_context` binds at :2667/:2815 — a naked fn binds -> no ctx); (3) in `src/ir/emit_llvm.zig`, **BAIL loudly** when emitting a naked function -> ("naked (`abi(.pure)`) emission not yet implemented") — do NOT emit the attr yet; (4) add -> `examples/1800-concurrency-naked-aarch64.sx` (tiny naked fn, aarch64 asm body, `.build -> {"target":"aarch64-macos"}`) and `examples/1801-concurrency-naked-x86_64.sx` (x86_64 -> sibling, `.build {"target":"x86_64-linux"}`); seed the `.exit` markers, capture the -> emit/IR-time bail diagnostic as the locked snapshot, confirm `zig build test` green, review -> the diff, commit. STOP — B1.0b (real `naked` emission) is the next step; do NOT implement -> emission in the same commit that adds the examples. Handle any exhaustive-switch site the -> Zig build flags from the new `Function` field. If you hit an UNRELATED compiler bug, file -> `issues/NNNN`, mark `CHECKPOINT-FIBERS.md` BLOCKED, and STOP. +## Kickoff prompt (B1.0b — paste into a fresh session) +> Implement Stream B1 step **B1.0b** (`abi(.pure)` real emission) per +> `current/PLAN-FIBERS.md`. Verify `zig build && zig build test` is green first (B1.0a is +> already landed: `Function.is_pure` plumbed, `decl.zig` skips ctx + bypasses implicit-return +> for `.pure`, `emit_llvm` Pass 2 bails loudly, `examples/1800-concurrency-pure-asm.sx` +> locked to the bail). Then: (1) in `src/ir/emit_llvm.zig` Pass 2 (~line 402), REPLACE the +> `func.is_pure` bail with real emission — set LLVM's `naked` attribute on the function +> (`LLVMGetEnumAttributeKindForName("naked", 5)` → `LLVMCreateEnumAttribute(ctx, id, 0)` → +> `LLVMAddAttributeAtIndex(llvm_func, -1, attr)`; shape per the `nounwind` set at +> emit_llvm.zig:1339) and emit the `.pure` body as its asm block only, no prologue/epilogue +> (the body already lowers to the inline-asm op + an `unreachable` terminator). (2) Pin +> `examples/1800-concurrency-pure-asm.sx` aarch64 with a `.build` sidecar +> `{"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 +> 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 +> {"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. diff --git a/examples/1800-concurrency-pure-asm.sx b/examples/1800-concurrency-pure-asm.sx new file mode 100644 index 00000000..baefdf91 --- /dev/null +++ b/examples/1800-concurrency-pure-asm.sx @@ -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(); } diff --git a/examples/expected/1800-concurrency-pure-asm.exit b/examples/expected/1800-concurrency-pure-asm.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/expected/1800-concurrency-pure-asm.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1800-concurrency-pure-asm.stderr b/examples/expected/1800-concurrency-pure-asm.stderr new file mode 100644 index 00000000..4c509e59 --- /dev/null +++ b/examples/expected/1800-concurrency-pure-asm.stderr @@ -0,0 +1 @@ +error: `abi(.pure)` function 'answer' LLVM emission not yet implemented diff --git a/examples/expected/1800-concurrency-pure-asm.stdout b/examples/expected/1800-concurrency-pure-asm.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1800-concurrency-pure-asm.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index ff8006c0..71dfb0f6 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -408,6 +408,17 @@ 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; + } self.emitFunction(&func, @intCast(i)); } diff --git a/src/ir/inst.zig b/src/ir/inst.zig index bedd1142..cb782a87 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -640,6 +640,16 @@ pub const Function = struct { /// drops the leftover declaration. See current/PLAN-COMPILER-VM.md (S3). 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 { name: StringId, ty: TypeId, diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 92eaeea1..ca1e1e7f 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -513,6 +513,11 @@ pub fn detectContextDecl(decls: []const *const Node) bool { pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool { if (!self.implicit_ctx_enabled) 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 // 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 @@ -2310,6 +2315,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) func.source_file = self.current_source_file; func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; + func.is_pure = (fd.abi == .pure); self.extern_name_map.put(name, c_name) catch {}; self.fn_decl_fids.put(fd, fid) catch {}; 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.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; + func.is_pure = (fd.abi == .pure); if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true; // 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 @@ -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) const saved_target = self.target_type; 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); } else { // 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 const saved_target = self.target_type; 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); } else { // void / noreturn: no value to return — lower as statements and