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:
@@ -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).**
|
||||||
|
|||||||
@@ -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.1–B1.5 are library + tests)
|
### 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).
|
- [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.
|
||||||
|
|||||||
24
examples/1800-concurrency-pure-asm.sx
Normal file
24
examples/1800-concurrency-pure-asm.sx
Normal 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(); }
|
||||||
1
examples/expected/1800-concurrency-pure-asm.exit
Normal file
1
examples/expected/1800-concurrency-pure-asm.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
examples/expected/1800-concurrency-pure-asm.stderr
Normal file
1
examples/expected/1800-concurrency-pure-asm.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
error: `abi(.pure)` function 'answer' LLVM emission not yet implemented
|
||||||
1
examples/expected/1800-concurrency-pure-asm.stdout
Normal file
1
examples/expected/1800-concurrency-pure-asm.stdout
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user