diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index dd60bfa0..5b260930 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -351,7 +351,7 @@ linkage keyword are two orthogonal annotations. - `abi(.x)` — ABI / calling-convention annotation in the slot **before** `extern`/`export`. **Unified replacement for `callconv(...)`, which is removed.** `ABI = { default, c, zig, pure }`: `.c` (C ABI), `.zig` (Zig-layout weld → the - `compiler` library), `.pure` (naked asm), `.default` (unannotated). Can appear + `compiler` library), `.naked` (naked asm), `.default` (unannotated). Can appear standalone (no extern) on any fn / fn-type / lambda. - `extern ` — linkage keyword + binding source (named library). @@ -363,12 +363,12 @@ What landed: - **Lexer/token** (`src/token.zig`, `src/lexer.zig`): `kw_callconv` → `kw_abi`, keyword string `"callconv"` → `"abi"`. - **Parser** (`src/parser.zig`): `parseOptionalCallConv` → `parseOptionalAbi` - (parses `abi(.c|.zig|.pure)`); wired in the fn-decl postfix slot (before + (parses `abi(.c|.zig|.naked)`); wired in the fn-decl postfix slot (before `extern`/`export`), the function-type-expr slot, and the lambda slot; `isFunctionDef`/`hasFnBodyAfterArrow` recognise `kw_abi`. - **AST→IR map** (`src/ir/type_resolver.zig`, `src/ir/lower/decl.zig`, `sema.zig`, `closure.zig`): the AST `.abi == .c` reads kept their C-ABI meaning; the - function-type resolver maps `.zig`/`.pure` → IR `.default` (no fn-pointer-type + function-type resolver maps `.zig`/`.naked` → IR `.default` (no fn-pointer-type CC for those decl-level ABIs; neither occurs in a function-TYPE position yet). - **CC-mismatch diagnostic** (`src/ir/lower/expr.zig`, `src/sema.zig`): the user-facing text `callconv(.c)` → `abi(.c)`. @@ -379,7 +379,7 @@ What landed: - **Tests**: parser unit tests in `src/parser.test.zig` — `abi(.zig) extern ` on a fn decl (asserts `abi == .zig`, `extern_export == .extern_`, `extern_lib == "compiler"`); bare `extern` leaves `abi == .default`; standalone `abi(.c)` / - `abi(.pure)`. lexer/sema tests updated. + `abi(.naked)`. lexer/sema tests updated. `zig build` + `zig build test` green (450/450 unit + 685 corpus). @@ -1656,7 +1656,7 @@ when reached (sentinels or accessor fns; see the design doc Risks). after `struct`. Parser unit tests (welded `Field` + plain struct), break-verified. Build + suite green. Parse-only sub-step (fns + structs) of Phase 1.1 complete. - **Phase 1.1 first sub-step + `callconv`→`abi` unification.** Parsed `abi(.zig) - extern ` on fn decls; unified `callconv` into `abi(.c|.zig|.pure)` (removed + extern ` on fn decls; unified `callconv` into `abi(.c|.zig|.naked)` (removed the `callconv` keyword), migrated 52 sx files + compiler diagnostics + docs + snapshots. Build + suite green. The original design's `extern(.zig)` single qualifier was split into `abi(.zig)` (ABI/layout, before extern) + `extern diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index fc2de3fd..ce1d129a 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -4,57 +4,57 @@ 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 -**B1.0b (`abi(.pure)` real emission) — DONE. B1.0 complete.** Replaced the emit bail with +**B1.0b (`abi(.naked)` real emission) — DONE. B1.0 complete.** Replaced the emit bail with real LLVM `naked` emission: -- `emit_llvm` declaration pass: for `func.is_pure`, add the LLVM `naked` + `noinline` + +- `emit_llvm` declaration pass: for `func.is_naked`, add the LLVM `naked` + `noinline` + `nounwind` attributes and **skip** the `frame-pointer=all` attribute (incompatible with a - frameless function). Pass 2 now emits the `.pure` body normally — `naked` makes the + frameless function). Pass 2 now emits the `.naked` body normally — `naked` makes the backend emit it verbatim (the inline asm + its own `ret`) with no prologue/epilogue. - IR shape (verified): `; Function Attrs: naked noinline nounwind` / `define internal i64 @answer() #0 { entry: call void asm sideeffect "…ret…", ""() unreachable }` / `attributes #0 = { naked noinline nounwind }`. The caller invokes it as an ordinary - `() -> i64` call (`.pure` is `call_conv == .default`). -- `examples/1800-concurrency-pure-asm.sx` — now GREEN, aarch64-pinned (`.build {"target": + `() -> i64` call (`.naked` is `call_conv == .default`). +- `examples/1800-concurrency-naked-asm.sx` — now GREEN, aarch64-pinned (`.build {"target": "macos"}`): runs end-to-end → **exit 42** on this host, ir-only on a mismatch; `.ir` snapshot captured. -- `examples/1801-concurrency-pure-generic.sx` (renamed from `-bail`) — the generic `.pure` +- `examples/1801-concurrency-naked-generic.sx` (renamed from `-bail`) — the generic `.naked` now emits a correct naked `answer__i64` (exit 42), proving generic.zig produces a naked body, not a framed one. aarch64-pinned. -- `examples/1802-concurrency-pure-asm-x86.sx` — x86_64 cross sibling (`.build {"target": +- `examples/1802-concurrency-naked-asm-x86.sx` — x86_64 cross sibling (`.build {"target": "x86_64-linux"}`, ir-only here): `.ir` locks `naked` + `movl $42, %eax` / `ret`. -- Unit test `emit: abi(.pure) function gets the naked attribute (no frame-pointer)` in +- Unit test `emit: abi(.naked) function gets the naked attribute (no frame-pointer)` in `emit_llvm.test.zig` (asserts `naked` present, `frame-pointer` absent). -- **B1.0c (review-hardening):** a param-bearing `.pure` fn emitted invalid LLVM (loud +- **B1.0c (review-hardening):** a param-bearing `.naked` fn emitted invalid LLVM (loud verifier error "cannot use argument of naked function") because the param-alloca loop wasn't gated. Fixed forward (this *enables* the B1.3 context-switch use case rather than - rejecting it): gated the param-alloca loop on `fd.abi != .pure` in decl.zig (both paths) + + rejecting it): gated the param-alloca loop on `fd.abi != .naked` in decl.zig (both paths) + generic.zig; a naked fn's args stay in registers (read by asm), declared-but-unused in - LLVM. Locked by `examples/1803-concurrency-pure-asm-param.sx` (`add(a,b)` → x0+x1 → 42). + LLVM. Locked by `examples/1803-concurrency-naked-asm-param.sx` (`add(a,b)` → x0+x1 → 42). - `zig build && zig build test` green: **725 ran, 0 failed** + unit tests. ### Earlier — B1.0a (lock + review hardening) -Plumbed `Function.is_pure` (set from `fd.abi == .pure` at both decl sites + generic.zig + -pack.zig); `funcWantsImplicitCtx` skips `.pure` (no synthetic ctx, like `.c`); all -body-lowering paths bypass `lowerValueBody` for `.pure` (asm body + `unreachable` cap — no sx +Plumbed `Function.is_naked` (set from `fd.abi == .naked` at both decl sites + generic.zig + +pack.zig); `funcWantsImplicitCtx` skips `.naked` (no synthetic ctx, like `.c`); all +body-lowering paths bypass `lowerValueBody` for `.naked` (asm body + `unreachable` cap — no sx return); `emit_llvm` Pass 2 bailed loudly (since flipped to real emission). Adversarial -review caught the generic/pack `is_pure` gap (a generic `.pure` silently shipped a framed -body); closed + locked. The review's `.pure`-lambda CRITICAL was a false positive +review caught the generic/pack `is_naked` gap (a generic `.naked` silently shipped a framed +body); closed + locked. The review's `.naked`-lambda CRITICAL was a false positive (unparseable — `isLambda` breaks on the `abi` keyword). ## Current state -Stream A (atomics) is feature-complete (✅). Stream B1: **B1.0 complete** — `abi(.pure)` +Stream A (atomics) is feature-complete (✅). Stream B1: **B1.0 complete** — `abi(.naked)` emits a real LLVM `naked` function end-to-end (decl, generic, pack paths), the substrate for the fiber context-switch. 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**. 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. -- **`.pure` with PARAMS works** (B1.0c, the B1.3 substrate): the param-alloca loop is gated - on `fd.abi != .pure` in decl.zig (both paths) + generic.zig — a naked fn's args stay in +- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the `.naked` body reuses it. +- **`.naked` with PARAMS works** (B1.0c, the B1.3 substrate): the param-alloca loop is gated + on `fd.abi != .naked` in decl.zig (both paths) + generic.zig — a naked fn's args stay in ABI registers (read by the asm body), declared-but-unused in LLVM (verifier-legal). - Example `1803-concurrency-pure-asm-param.sx` (`add(a,b)` reads x0/x1). **Unsupported (loud, - not silent):** a `.pure` *variadic-pack* fn (pack.zig's param loop is intertwined with + Example `1803-concurrency-naked-asm-param.sx` (`add(a,b)` reads x0/x1). **Unsupported (loud, + not silent):** a `.naked` *variadic-pack* fn (pack.zig's param loop is intertwined with comptime-param/`#insert` handling, and a naked fn can't read a runtime-sized pack from registers anyway) → loud LLVM-verifier error for that nonsensical construct. Acceptable boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker. @@ -80,20 +80,21 @@ a checkpoint note on the convention. Only if the probe surfaces a real gap (a pa ## Decisions (Stream B1 specifics; surface locked in design §4 / §4.6) - **The async runtime is sx LIBRARY code.** The compiler provides only: the general - primitives (inline asm ✅, `abi(.pure)` naked [B1.0], atomics ✅) + fiber-safe codegen + primitives (inline asm ✅, `abi(.naked)` naked [B1.0], atomics ✅) + fiber-safe codegen (`context` already fiber-local — B1.1). Schedulers, fibers, channels, futures, `Io` vtables, `mmap` stacks are all sx. -- **`abi(.pure)` is the real spelling of the design's `callconv(.naked)`** — postfix slot, - `name :: (sig) -> Ret abi(.pure) { asm { … }; }`. B1.0 = carry it into IR + emit LLVM +- **`abi(.naked)` is the real spelling of the design's `callconv(.naked)`** — postfix slot, + `name :: (sig) -> Ret abi(.naked) { asm { … }; }`. B1.0 = carry it into IR + emit LLVM `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). `.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 +- **`.naked` ≠ `.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`. +- **Naming:** sx-facing name is **`naked`** (keyword `abi(.naked)`, field `is_naked`, the + diagnostic), matching LLVM's `naked` attribute and the industry term (Zig/Rust/GCC/Clang). + The ABI variant was renamed `.pure → .naked` (user direction): "pure" universally means + *side-effect-free*, the opposite of a register-clobbering context switch. +- **B1.0 snapshot scope:** a `.naked` 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 @@ -111,34 +112,34 @@ a checkpoint note on the convention. Only if the probe surfaces a real gap (a pa ## Log - **carve** — wrote PLAN-FIBERS.md + CHECKPOINT-FIBERS.md. Grounded the B1 compiler floor: - `ABI.pure` inert (type_resolver.zig:237), IR `Function` has no naked flag (inst.zig:605), + `ABI.naked` inert (type_resolver.zig:237), IR `Function` has no naked flag (inst.zig:605), attribute API pattern (emit_llvm.zig:1339 nounwind), `.c` ctx-skip precedent (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 + (1645/1651). Corrected the design's `callconv(.naked)` → real `abi(.naked)` spelling and 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). +- **B1.0a** — plumbed `Function.is_naked` (set from `fd.abi == .naked` at both decl sites); + `funcWantsImplicitCtx` skips `.naked` (no implicit ctx, like `.c`); both body-lowering + paths bypass `lowerValueBody` for `.naked` (asm body + `unreachable` cap — no sx return); + `emit_llvm` Pass 2 bails loudly on `func.is_naked`. `examples/1800-concurrency-naked-asm.sx` + locked to the bail (exit 1 + diagnostic). Suite green (722/0). (ABI variant later renamed + `.pure → .naked` — see the Naming decision above — so all `is_*`/`abi(.*)`/example names + here read `naked`.) - **B1.0a review-hardening** — adversarial review found generic/pack Function-creation paths - left `is_pure` false (silent framed body for a generic `.pure` instance — returned 42 but - corrupted the stack). Fixed generic.zig + pack.zig (set `is_pure` + asm-only `unreachable` - cap); locked by `examples/1801-concurrency-pure-generic-bail.sx`. The review's `.pure`- + left `is_naked` false (silent framed body for a generic `.naked` instance — returned 42 but + corrupted the stack). Fixed generic.zig + pack.zig (set `is_naked` + asm-only `unreachable` + cap); locked by `examples/1801-concurrency-naked-generic-bail.sx`. The review's `.naked`- lambda CRITICAL was a false positive (unparseable — `isLambda` breaks on `abi`). Suite green (723/0). - **B1.0b** — real `naked` emission: emit_llvm declaration pass adds LLVM `naked`/`noinline`/ - `nounwind` + skips `frame-pointer` for `func.is_pure`; Pass 2 emits the body verbatim (no + `nounwind` + skips `frame-pointer` for `func.is_naked`; Pass 2 emits the body verbatim (no prologue). `1800` green aarch64-pinned (exit 42 + `.ir`); renamed `1801` → `-generic` - (generic `.pure` emits a naked body, exit 42); added x86_64 sibling `1802` (ir-only, `.ir` + (generic `.naked` emits a naked body, exit 42); added x86_64 sibling `1802` (ir-only, `.ir` locks `naked` + `movl $42, %eax`). Unit test asserts `naked` present + `frame-pointer` absent. Suite green (724/0). -- **B1.0c** — review-hardening: param-bearing `.pure` emitted invalid LLVM (loud verifier - error). Gated the param-alloca loop on `fd.abi != .pure` (decl.zig both paths + generic.zig) +- **B1.0c** — review-hardening: param-bearing `.naked` emitted invalid LLVM (loud verifier + error). Gated the param-alloca loop on `fd.abi != .naked` (decl.zig both paths + generic.zig) — naked args stay in registers, read by the asm body (the B1.3 context-switch shape). - Locked by `examples/1803-concurrency-pure-asm-param.sx`. Pack `.pure` left unsupported + Locked by `examples/1803-concurrency-naked-asm-param.sx`. Pack `.naked` left unsupported (loud, nonsensical). **B1.0 complete.** Suite green (725/0). **Next: B1.1 (per-fiber context, probe-first).** diff --git a/current/PLAN-FIBERS.md b/current/PLAN-FIBERS.md index fca4a46a..42279457 100644 --- a/current/PLAN-FIBERS.md +++ b/current/PLAN-FIBERS.md @@ -1,6 +1,6 @@ # PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler) -> **STATUS: 🚧 in progress.** B1.0 (`abi(.pure)` codegen) ✅ complete — emits a real LLVM +> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)` codegen) ✅ complete — emits a real LLVM > `naked` function end-to-end (decl / generic / pack paths; examples 1800/1801/1802 + unit > test). Next step = **B1.1** (per-fiber `context` root — probe-first, likely library-only). @@ -12,7 +12,7 @@ separate carve ([PLAN-CHANNELS.md], when reached) and depends on this + atomics **Goal:** the colorblind, stackful, **pure-sx** async runtime — fibers behind an `Io` interface, an M:1 scheduler, blocking + deterministic-sim + event-loop `Io` impls. The -**compiler floor is small and net-new**: make `abi(.pure)` actually emit an LLVM `naked` +**compiler floor is small and net-new**: make `abi(.naked)` actually emit an LLVM `naked` function (B1.0), and confirm/close the per-fiber `context` root (B1.1). **Everything else — the context-switch asm, fiber bootstrap, `mmap` stacks, the scheduler, futures, the `Io` vtables — is ordinary sx library code** (design §4, §4.4). The irreducible FFI @@ -30,25 +30,27 @@ authorization. ## Design (grounded against the tree) -### B1.0 — `abi(.pure)` codegen (the one genuinely net-new compiler piece in B1) +### B1.0 — `abi(.naked)` codegen (the one genuinely net-new compiler piece in B1) -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. +The design doc spells this `callconv(.naked)`; the **real sx surface is `abi(.naked)`** — +written in the postfix slot, `name :: (sig) -> Ret abi(.naked) { 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. +The sx-facing name is **`naked`** throughout (keyword, field `is_naked`, diagnostics) — +matching LLVM's `naked` attribute (the lowering mechanism) and the industry term +(Zig/Rust/GCC/Clang). The ABI variant was renamed `.pure → .naked`: "pure" universally +means *side-effect-free*, the opposite of a register-clobbering context switch. **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 +- The `ABI` enum **already carries `.naked`** — `ABI = enum { default, c, compiler, naked }` + ([ast.zig:142](../src/ast.zig#L142)), documented "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 LLVM `naked` - attribute**. So the net-new work is exactly: **carry `abi == .pure` into the IR +- `.naked` is **inert today**: [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) + maps `.compiler, .naked → .default` CC, and `emit_llvm` emits **no LLVM `naked` + attribute**. So the net-new work is exactly: **carry `abi == .naked` 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 pure flag** — add one (`is_pure: bool`). + (default/c) + `is_compiler_domain`, but **no naked flag** — add one (`is_naked: 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)` @@ -56,24 +58,24 @@ function attribute is only the *lowering mechanism* (B1.0b), not what we call th 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 `.pure` fn gets no synthetic `__sx_ctx`, no stack frame, + `.naked` 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). The - implicit-return machinery (`lowerValueBody`) must also be bypassed: a `.pure` body has no + implicit-return machinery (`lowerValueBody`) must also be bypassed: a `.naked` 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 `.pure` body + `LLVMGetInlineAsm` at [ops.zig:915](../src/backend/llvm/ops.zig#L915). The `.naked` body is a single asm block reusing this path. -**`.pure` ≠ `.c` (design §4.6 context-switch note):** a `.c` epilogue restores SP from the +**`.naked` ≠ `.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. `.pure` = no prologue/epilogue/frame — the asm emits its -own `ret`. This is *why* the switch must be `.pure`, not `.c`. +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`. -**Snapshot story (per the atomics precedent):** a `.pure` fn's *body is raw per-arch asm* +**Snapshot story (per the atomics precedent):** a `.naked` 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 @@ -133,18 +135,18 @@ 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 (`.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_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 +B1.0 (`.naked`) forces these plumbing sites: +- [ast.zig:142](../src/ast.zig#L142) — `ABI.naked` (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 == .naked`; gate the + implicit-ctx off for `.naked` in `funcWantsImplicitCtx` (mirror the `.c` skip at + decl.zig:515) and bypass `lowerValueBody` for `.naked` bodies (lower statements + cap with + `unreachable`, in both body-lowering paths) — a `.naked` 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). +- [type_resolver.zig:237](../src/ir/type_resolver.zig#L237) — leave CC `.default` (a `.naked` + fn-pointer type has no CC of its own; nakedness 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 + `func.is_naked` (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. @@ -152,25 +154,25 @@ B1.0 (`.pure`) forces these plumbing sites: ## Phases (xfail→green steps) -### B1.0 — `abi(.pure)` codegen — ✅ COMPLETE -- **B1.0a (lock) — ✅ DONE.** Carried `abi == .pure` into IR `Function.is_pure`; threaded - through `decl.zig` (`funcWantsImplicitCtx` skips `.pure` like `.c`; all body-lowering paths - bypass `lowerValueBody` for `.pure`, lowering the asm body + capping with `unreachable`) + - generic.zig + pack.zig; `emit_llvm` Pass 2 bailed loudly on `func.is_pure`. Locked by - `examples/1800-concurrency-pure-asm.sx` + the generic regression (review-found gap). +### B1.0 — `abi(.naked)` codegen — ✅ COMPLETE +- **B1.0a (lock) — ✅ DONE.** Carried `abi == .naked` into IR `Function.is_naked`; threaded + through `decl.zig` (`funcWantsImplicitCtx` skips `.naked` like `.c`; all body-lowering paths + bypass `lowerValueBody` for `.naked`, lowering the asm body + capping with `unreachable`) + + generic.zig + pack.zig; `emit_llvm` Pass 2 bailed loudly on `func.is_naked`. Locked by + `examples/1800-concurrency-naked-asm.sx` + the generic regression (review-found gap). - **B1.0b (green) — ✅ DONE.** `emit_llvm` declaration pass adds LLVM `naked` + `noinline` + - `nounwind` for `func.is_pure` and skips `frame-pointer=all` (incompatible with a frameless + `nounwind` for `func.is_naked` and skips `frame-pointer=all` (incompatible with a frameless function); Pass 2 emits the body normally (`naked` ⇒ verbatim asm + own `ret`, no - prologue). `1800` pinned aarch64 → exit 42 + `.ir`; `1801-concurrency-pure-generic.sx` + prologue). `1800` pinned aarch64 → exit 42 + `.ir`; `1801-concurrency-naked-generic.sx` (renamed from `-bail`) proves the generic path emits a naked body (exit 42); - `1802-concurrency-pure-asm-x86.sx` x86_64 cross sibling (ir-only here, `.ir` locks `naked` - + `movl $42, %eax`). Unit test `emit: abi(.pure) function gets the naked attribute` asserts + `1802-concurrency-naked-asm-x86.sx` x86_64 cross sibling (ir-only here, `.ir` locks `naked` + + `movl $42, %eax`). Unit test `emit: abi(.naked) function gets the naked attribute` asserts `naked` present + `frame-pointer` absent. Suite green (724/0). -- **B1.0c (review-hardening) — ✅ DONE.** A param-bearing `.pure` fn emitted invalid LLVM - (loud verifier error). Gated the param-alloca loop on `fd.abi != .pure` (decl.zig both +- **B1.0c (review-hardening) — ✅ DONE.** A param-bearing `.naked` fn emitted invalid LLVM + (loud verifier error). Gated the param-alloca loop on `fd.abi != .naked` (decl.zig both paths + generic.zig) so a naked fn's args stay in registers (read by the asm body) — this - *enables* B1.3's `swap_context(from, to)`. Locked by `1803-concurrency-pure-asm-param.sx`. - Pack `.pure` (variadic + naked, nonsensical) left unsupported → loud verifier error. + *enables* B1.3's `swap_context(from, to)`. Locked by `1803-concurrency-naked-asm-param.sx`. + Pack `.naked` (variadic + naked, nonsensical) left unsupported → loud verifier error. ### 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,7 +209,7 @@ asserting program-emitted ordering contracts. --- ## Gates -- **B1.0:** unit `emit_llvm.test.zig` (the `naked` attr present on a `.pure` fn); two +- **B1.0:** unit `emit_llvm.test.zig` (the `naked` attr present on a `.naked` fn); two arch-gated examples (aarch64 + x86_64) run end-to-end on a matching host, ir-only on a mismatch (assert `naked` + asm in `.ir`). **OUT of corpus scope, stated loudly:** the *correctness* of any hand-written register save/restore — that's the B1.3 stress harness. @@ -220,24 +222,24 @@ asserting program-emitted ordering contracts. - **B1.5:** `18xx` ordering-contract snapshots under the deterministic `Io`. ## Kickoff prompt (B1.0b — paste into a fresh session) -> Implement Stream B1 step **B1.0b** (`abi(.pure)` real emission) per +> Implement Stream B1 step **B1.0b** (`abi(.naked)` 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` +> already landed: `Function.is_naked` plumbed, `decl.zig` skips ctx + bypasses implicit-return +> for `.naked`, `emit_llvm` Pass 2 bails loudly, `examples/1800-concurrency-naked-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 +> `func.is_naked` 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 +> emit_llvm.zig:1339) and emit the `.naked` 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 +> `examples/1800-concurrency-naked-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 +> `.ir` + regen (`-Dname=examples/1800-concurrency-naked-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/1802-concurrency-pure-asm-x86.sx` (x86_64 body, `.build +> text). (3) Add `examples/1802-concurrency-naked-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 +> present on an `abi(.naked)` 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/current/PLAN-POST-METATYPE.md b/current/PLAN-POST-METATYPE.md index 3579eef6..3aea1307 100644 --- a/current/PLAN-POST-METATYPE.md +++ b/current/PLAN-POST-METATYPE.md @@ -61,14 +61,14 @@ The colorblind, stackful, pure-sx async runtime (design §4). Compiler floor is the runtime is sx lib. Likely carved as two PLANs: ### B1 — Fibers + Io + M:1 (the runtime; `PLAN-FIBERS.md`) · 🚧 **CARVED** (not started; first step B1.0a) -- B1.0 **`abi(.naked)` — make the EXISTING `.pure` ABI actually naked.** The enum - already carries `.pure` (ast.zig:142, documented "pure/naked, no prologue/epilogue"), - but it is an **inert label today**: `type_resolver.zig:237` maps `.pure → .default` +- B1.0 **`abi(.naked)` — make the EXISTING `.naked` ABI actually naked.** The enum + already carries `.naked` (ast.zig:142, documented "naked, no prologue/epilogue"), + but it is an **inert label today**: `type_resolver.zig:237` maps `.naked → .default` CC and there is **zero naked-attribute emission in emit_llvm**. So B1.0 is NOT "extend the enum" (done) — it is "emit the LLVM `naked` attr + skip prologue/epilogue - lowering for `.pure`," genuinely net-new. (Roadmap §7-step-4's "extend + lowering for `.naked`," genuinely net-new. (Roadmap §7-step-4's "extend `CallConv {default, c}`" is stale — CallConv was renamed ABI and already gained - `compiler`/`pure` in the compiler-API stream.) Gates the context-switch. + `compiler`/`naked` in the compiler-API stream.) Gates the context-switch. - B1.1 **Per-fiber `context` root + `push Context`-stack storage.** Grounding correction: `context` is **already an implicit `*Context` parameter** (comptime_vm.zig:392, lower.zig:257 "Implicit Context parameter machinery"), **not raw TLS** — so it already @@ -192,7 +192,7 @@ module hazard; S2 TLS + C-constructor JIT test per host OS (the exact prior-spik metatype stream), deterministic-`Io` oracle calibration, `context`-fiber-local/errno (C — gated by the named stress harness), S2 (E), C1 args-buffer layout (D). - **The compiler floor stays small, but deep — net-new pieces, grounded:** atomics - (100% net-new, no scaffolding), making `abi(.pure)` actually naked (the enum variant + (100% net-new, no scaffolding), making `abi(.naked)` actually naked (the enum variant exists but is inert today), per-fiber `context` root + push-stack storage (`context` is already an implicit param, NOT TLS — so this is smaller/different than "repointable codegen" implied), `declare`/`define`/`type_info` (metatype stream — **done**), the diff --git a/design/comptime-compiler-api.md b/design/comptime-compiler-api.md index 7daabb36..417c8474 100644 --- a/design/comptime-compiler-api.md +++ b/design/comptime-compiler-api.md @@ -71,7 +71,7 @@ internal surface (Zig types + functions). Two defining properties: > **before** `extern`/`export`. It is the unified replacement for the old > `callconv(...)` (which is removed): `ABI = { default, c, zig, pure }` — > `.c` (C ABI / cdecl), `.zig` (Zig-layout weld → the `compiler` library), -> `.pure` (naked asm). `.default` = unannotated (ordinary sx convention). +> `.naked` (naked asm). `.default` = unannotated (ordinary sx convention). > - `extern ` — the linkage keyword + binding source (the named library). `abi(...)` sits where `callconv(...)` went (after the return type for fns); the diff --git a/design/execution-evolution-roadmap.md b/design/execution-evolution-roadmap.md index eaabb9ca..bef61938 100644 --- a/design/execution-evolution-roadmap.md +++ b/design/execution-evolution-roadmap.md @@ -396,10 +396,10 @@ grounding) are explicit steps, not buried. capability:** name minted results by the instantiation's mangled name + input validation. 4. **`abi(.naked)`** — *correction:* `CallConv` was renamed `ABI` and **already carries - `.pure`** (ast.zig:142, "pure/naked, no prologue/epilogue") during the compiler-API - stream — so this is NOT "extend the enum." `.pure` is an **inert label today**: + `.naked`** (ast.zig:142, "naked, no prologue/epilogue") during the compiler-API + stream — so this is NOT "extend the enum." `.naked` is an **inert label today**: `type_resolver.zig:237` maps it to `.default` CC and emit_llvm emits **no** naked - attribute. The net-new work is making `.pure` actually emit LLVM `naked` + skip + attribute. The net-new work is making `.naked` actually emit LLVM `naked` + skip prologue/epilogue lowering. Gates A2. 5. **Per-fiber `context` root + push-stack storage** — *correction:* `context` is **already an implicit `*Context` parameter** (comptime_vm.zig:392, lower.zig:257 diff --git a/examples/1800-concurrency-pure-asm.sx b/examples/1800-concurrency-naked-asm.sx similarity index 100% rename from examples/1800-concurrency-pure-asm.sx rename to examples/1800-concurrency-naked-asm.sx diff --git a/examples/1801-concurrency-pure-generic.sx b/examples/1801-concurrency-naked-generic.sx similarity index 100% rename from examples/1801-concurrency-pure-generic.sx rename to examples/1801-concurrency-naked-generic.sx diff --git a/examples/1802-concurrency-pure-asm-x86.sx b/examples/1802-concurrency-naked-asm-x86.sx similarity index 100% rename from examples/1802-concurrency-pure-asm-x86.sx rename to examples/1802-concurrency-naked-asm-x86.sx diff --git a/examples/1803-concurrency-pure-asm-param.sx b/examples/1803-concurrency-naked-asm-param.sx similarity index 100% rename from examples/1803-concurrency-pure-asm-param.sx rename to examples/1803-concurrency-naked-asm-param.sx diff --git a/examples/expected/1800-concurrency-pure-asm.build b/examples/expected/1800-concurrency-naked-asm.build similarity index 100% rename from examples/expected/1800-concurrency-pure-asm.build rename to examples/expected/1800-concurrency-naked-asm.build diff --git a/examples/expected/1800-concurrency-pure-asm.exit b/examples/expected/1800-concurrency-naked-asm.exit similarity index 100% rename from examples/expected/1800-concurrency-pure-asm.exit rename to examples/expected/1800-concurrency-naked-asm.exit diff --git a/examples/expected/1800-concurrency-pure-asm.ir b/examples/expected/1800-concurrency-naked-asm.ir similarity index 100% rename from examples/expected/1800-concurrency-pure-asm.ir rename to examples/expected/1800-concurrency-naked-asm.ir diff --git a/examples/expected/1800-concurrency-pure-asm.stderr b/examples/expected/1800-concurrency-naked-asm.stderr similarity index 100% rename from examples/expected/1800-concurrency-pure-asm.stderr rename to examples/expected/1800-concurrency-naked-asm.stderr diff --git a/examples/expected/1800-concurrency-pure-asm.stdout b/examples/expected/1800-concurrency-naked-asm.stdout similarity index 100% rename from examples/expected/1800-concurrency-pure-asm.stdout rename to examples/expected/1800-concurrency-naked-asm.stdout diff --git a/examples/expected/1801-concurrency-pure-generic.build b/examples/expected/1801-concurrency-naked-generic.build similarity index 100% rename from examples/expected/1801-concurrency-pure-generic.build rename to examples/expected/1801-concurrency-naked-generic.build diff --git a/examples/expected/1801-concurrency-pure-generic.exit b/examples/expected/1801-concurrency-naked-generic.exit similarity index 100% rename from examples/expected/1801-concurrency-pure-generic.exit rename to examples/expected/1801-concurrency-naked-generic.exit diff --git a/examples/expected/1801-concurrency-pure-generic.ir b/examples/expected/1801-concurrency-naked-generic.ir similarity index 100% rename from examples/expected/1801-concurrency-pure-generic.ir rename to examples/expected/1801-concurrency-naked-generic.ir diff --git a/examples/expected/1801-concurrency-pure-generic.stderr b/examples/expected/1801-concurrency-naked-generic.stderr similarity index 100% rename from examples/expected/1801-concurrency-pure-generic.stderr rename to examples/expected/1801-concurrency-naked-generic.stderr diff --git a/examples/expected/1801-concurrency-pure-generic.stdout b/examples/expected/1801-concurrency-naked-generic.stdout similarity index 100% rename from examples/expected/1801-concurrency-pure-generic.stdout rename to examples/expected/1801-concurrency-naked-generic.stdout diff --git a/examples/expected/1802-concurrency-pure-asm-x86.build b/examples/expected/1802-concurrency-naked-asm-x86.build similarity index 100% rename from examples/expected/1802-concurrency-pure-asm-x86.build rename to examples/expected/1802-concurrency-naked-asm-x86.build diff --git a/examples/expected/1802-concurrency-pure-asm-x86.exit b/examples/expected/1802-concurrency-naked-asm-x86.exit similarity index 100% rename from examples/expected/1802-concurrency-pure-asm-x86.exit rename to examples/expected/1802-concurrency-naked-asm-x86.exit diff --git a/examples/expected/1802-concurrency-pure-asm-x86.ir b/examples/expected/1802-concurrency-naked-asm-x86.ir similarity index 100% rename from examples/expected/1802-concurrency-pure-asm-x86.ir rename to examples/expected/1802-concurrency-naked-asm-x86.ir diff --git a/examples/expected/1802-concurrency-pure-asm-x86.stderr b/examples/expected/1802-concurrency-naked-asm-x86.stderr similarity index 100% rename from examples/expected/1802-concurrency-pure-asm-x86.stderr rename to examples/expected/1802-concurrency-naked-asm-x86.stderr diff --git a/examples/expected/1803-concurrency-pure-asm-param.build b/examples/expected/1803-concurrency-naked-asm-param.build similarity index 100% rename from examples/expected/1803-concurrency-pure-asm-param.build rename to examples/expected/1803-concurrency-naked-asm-param.build diff --git a/examples/expected/1803-concurrency-pure-asm-param.exit b/examples/expected/1803-concurrency-naked-asm-param.exit similarity index 100% rename from examples/expected/1803-concurrency-pure-asm-param.exit rename to examples/expected/1803-concurrency-naked-asm-param.exit diff --git a/examples/expected/1803-concurrency-pure-asm-param.ir b/examples/expected/1803-concurrency-naked-asm-param.ir similarity index 100% rename from examples/expected/1803-concurrency-pure-asm-param.ir rename to examples/expected/1803-concurrency-naked-asm-param.ir diff --git a/examples/expected/1803-concurrency-pure-asm-param.stderr b/examples/expected/1803-concurrency-naked-asm-param.stderr similarity index 100% rename from examples/expected/1803-concurrency-pure-asm-param.stderr rename to examples/expected/1803-concurrency-naked-asm-param.stderr diff --git a/examples/expected/1803-concurrency-pure-asm-param.stdout b/examples/expected/1803-concurrency-naked-asm-param.stdout similarity index 100% rename from examples/expected/1803-concurrency-pure-asm-param.stdout rename to examples/expected/1803-concurrency-naked-asm-param.stdout diff --git a/src/ast.zig b/src/ast.zig index 7dc1bdd3..8ba5c108 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -137,9 +137,10 @@ pub const Root = struct { /// bodiless decls whose Zig/VM handler is the impl) AND user compiler-domain /// functions like post-link callbacks (bodied, but emit-skipped). The ABI alone /// marks it — there is no `extern ` and no fake `#library "compiler"`. -/// - `.pure` — a pure / naked function (inline asm body), no calling-convention -/// prologue/epilogue. -pub const ABI = enum { default, c, compiler, pure }; +/// - `.naked` — a naked function (inline asm body), no calling-convention +/// prologue/epilogue. The body is responsible for its own `ret`; args arrive +/// in ABI registers (no frame, no implicit `__sx_ctx`). +pub const ABI = enum { default, c, compiler, naked }; /// Linkage modifier written in the postfix slot before `abi(...)`: /// `name :: (sig) -> Ret [extern | export] [abi(.x)] [lib] [;|{…}];` @@ -156,7 +157,7 @@ pub const FnDecl = struct { body: *Node, type_params: []const StructTypeParam = &.{}, is_arrow: bool = false, - /// ABI / calling-convention annotation (`abi(.c)` / `abi(.zig)` / `abi(.pure)`) + /// ABI / calling-convention annotation (`abi(.c)` / `abi(.zig)` / `abi(.naked)`) /// in the postfix slot after `extern`/`export`. `.default` = unannotated. /// `.zig` marks a function bound to the comptime `compiler` library — its /// signature is welded to the real internal Zig fn and it dispatches over the diff --git a/src/ir/emit_llvm.test.zig b/src/ir/emit_llvm.test.zig index 636b83bb..0a9650d8 100644 --- a/src/ir/emit_llvm.test.zig +++ b/src/ir/emit_llvm.test.zig @@ -1325,18 +1325,18 @@ test "emit: reflectArgRepr surfaces .unresolved for an unresolvable reflection a try std.testing.expect(emitter.reflectArgRepr(bogus) != .bare); } -test "emit: abi(.pure) function gets the naked attribute (no frame-pointer)" { +test "emit: abi(.naked) function gets the naked attribute (no frame-pointer)" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); - // func answer() -> i64 abi(.pure) { asm volatile { "ret" }; unreachable } - // The naked attribute is keyed off Function.is_pure in the declaration pass, + // func answer() -> i64 abi(.naked) { asm volatile { "ret" }; unreachable } + // The naked attribute is keyed off Function.is_naked in the declaration pass, // independent of the body — a minimal asm + unreachable body suffices. _ = b.beginFunction(str(&module, "answer"), &.{}, .i64); - b.currentFunc().is_pure = true; + b.currentFunc().is_naked = true; const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index e5dc876a..7ed3259a 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -408,9 +408,9 @@ pub const LLVMEmitter = struct { // its only references are in comptime code, so DCE drops the leftover // declaration. See current/PLAN-COMPILER-VM.md (S3). if (func.is_compiler_domain) continue; - // `abi(.pure)` functions emit normally — the `naked` attribute (set + // `abi(.naked)` functions emit normally — the `naked` attribute (set // in the declaration pass) makes the backend emit the body (inline - // asm + its own `ret`) with no prologue/epilogue. See Function.is_pure. + // asm + its own `ret`) with no prologue/epilogue. See Function.is_naked. self.emitFunction(&func, @intCast(i)); } @@ -1327,13 +1327,13 @@ pub const LLVMEmitter = struct { // Add frame-pointer and nounwind attributes for correct ARM64 codegen { const func_idx_attr: c.LLVMAttributeIndex = @bitCast(@as(i32, -1)); - if (func.is_pure) { - // `abi(.pure)`: emit via LLVM's `naked` attribute — the backend + if (func.is_naked) { + // `abi(.naked)`: emit via LLVM's `naked` attribute — the backend // emits the body verbatim (our inline asm + its own `ret`) with // NO prologue/epilogue/frame. Do NOT request `frame-pointer` // (incompatible with a frameless function). `noinline` keeps the // asm body out of a framed caller; `nounwind` — naked asm never - // unwinds. See Function.is_pure / current/PLAN-FIBERS.md. + // unwinds. See Function.is_naked / current/PLAN-FIBERS.md. const naked_id = c.LLVMGetEnumAttributeKindForName("naked", 5); c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, c.LLVMCreateEnumAttribute(self.context, naked_id, 0)); const noinline_id = c.LLVMGetEnumAttributeKindForName("noinline", 8); diff --git a/src/ir/inst.zig b/src/ir/inst.zig index cb782a87..12480a5d 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -640,15 +640,15 @@ 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 + /// True for an `abi(.naked)` 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 + /// switch (SP-in ≠ SP-out by design), which is why `.naked` is distinct /// from `.c`. - is_pure: bool = false, + is_naked: bool = false, pub const Param = struct { name: StringId, diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index ec200ac9..dc4943dd 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -513,11 +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 + // An `abi(.naked)` 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; + // See Function.is_naked. + if (fd.abi == .naked) 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 @@ -2315,7 +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); + func.is_naked = (fd.abi == .naked); self.extern_name_map.put(name, c_name) catch {}; self.fn_decl_fids.put(fd, fid) catch {}; return; @@ -2329,7 +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); + func.is_naked = (fd.abi == .naked); 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 @@ -2660,13 +2660,13 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId const user_param_base: u32 = if (wants_ctx) 1 else 0; if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - // An `abi(.pure)` (naked) function has no frame: its params arrive in ABI + // An `abi(.naked)` (naked) function has no frame: its params arrive in ABI // registers and are read directly by the asm body (e.g. `swap_context`'s // `from`/`to`). Spilling them to allocas would (a) need a frame and (b) emit // `store i64 %0, …` — "cannot use argument of naked function" (LLVM verifier). // Leave the LLVM args declared-but-unused (the verifier allows that); the asm // references the registers. - if (fd.abi != .pure) for (fd.params, 0..) |p, i| { + if (fd.abi != .naked) for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); const slot = self.builder.alloca(pty); const param_ref = Ref.fromIndex(@intCast(i + user_param_base)); @@ -2685,8 +2685,8 @@ 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 (self.builder.currentFunc().is_pure) { - // `abi(.pure)`: the body is a single asm block that emits its own `ret`. + if (self.builder.currentFunc().is_naked) { + // `abi(.naked)`: 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 @@ -2818,10 +2818,10 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i const user_param_base_lf: u32 = if (wants_ctx_lf) 1 else 0; if (wants_ctx_lf) self.current_ctx_ref = Ref.fromIndex(0); - // `abi(.pure)` (naked): params arrive in registers, read directly by the asm + // `abi(.naked)` (naked): params arrive in registers, read directly by the asm // body — no frame, no alloca/store (which the LLVM verifier rejects on a // naked function). See the sibling guard in the other body-lowering path. - if (fd.abi != .pure) for (fd.params, 0..) |p, i| { + if (fd.abi != .naked) for (fd.params, 0..) |p, i| { const pty = self.resolveParamType(&p); // Allocate stack slot for param, store initial value. // Refs 0..N-1 are reserved for function parameters by beginFunction. @@ -2843,8 +2843,8 @@ 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 (self.builder.currentFunc().is_pure) { - // `abi(.pure)`: asm-only body that rets itself — see the sibling path + if (self.builder.currentFunc().is_naked) { + // `abi(.naked)`: 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(); diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 5169a0bd..c18d471c 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -115,7 +115,7 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); _ = func_id; self.builder.currentFunc().has_implicit_ctx = wants_ctx; - self.builder.currentFunc().is_pure = (fd.abi == .pure); + self.builder.currentFunc().is_naked = (fd.abi == .naked); // Create entry block const entry_name = self.module.types.internString("entry"); @@ -128,10 +128,10 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name defer scope.deinit(); self.scope = &scope; - // `abi(.pure)` (naked): no frame — params arrive in registers, read by the + // `abi(.naked)` (naked): no frame — params arrive in registers, read by the // asm body, never spilled to allocas (the LLVM verifier rejects a naked // function that uses its arguments). Mirrors the decl-path guard. - if (fd.abi != .pure) { + if (fd.abi != .naked) { var param_idx: u32 = if (wants_ctx) 1 else 0; for (fd.params) |p| { if (isTypeParamDecl(&p, fd.type_params)) continue; @@ -155,10 +155,10 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name self.ensureTerminator(ret_ty); } self.builder.finalize(); - } else if (self.builder.currentFunc().is_pure) { - // `abi(.pure)`: asm-only body that rets itself — no sx value return. + } else if (self.builder.currentFunc().is_naked) { + // `abi(.naked)`: asm-only body that rets itself — no sx value return. // Lower the statements + cap with `unreachable` (mirrors the decl path). - // emit_llvm bails on `is_pure` until B1.0b implements `naked` emission. + // emit_llvm bails on `is_naked` until B1.0b implements `naked` emission. self.lowerBlock(fd.body); if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable(); self.builder.finalize(); diff --git a/src/ir/lower/pack.zig b/src/ir/lower/pack.zig index 30ef498c..9c573a4f 100644 --- a/src/ir/lower/pack.zig +++ b/src/ir/lower/pack.zig @@ -949,7 +949,7 @@ pub fn monomorphizePackFn( const name_id = self.module.types.internString(owned_name); _ = self.builder.beginFunction(name_id, params.items, ret_ty); self.builder.currentFunc().has_implicit_ctx = wants_ctx; - self.builder.currentFunc().is_pure = (fd.abi == .pure); + self.builder.currentFunc().is_naked = (fd.abi == .naked); const entry_name = self.module.types.internString("entry"); const entry = self.builder.appendBlock(entry_name, &.{}); @@ -1039,10 +1039,10 @@ pub fn monomorphizePackFn( defer self.setCurrentSourceFile(saved_source); if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - if (self.builder.currentFunc().is_pure) { - // `abi(.pure)`: asm-only body that rets itself — no sx value return. + if (self.builder.currentFunc().is_naked) { + // `abi(.naked)`: asm-only body that rets itself — no sx value return. // Lower statements + cap with `unreachable` (mirrors the decl path). - // emit_llvm bails on `is_pure` until B1.0b implements `naked` emission. + // emit_llvm bails on `is_naked` until B1.0b implements `naked` emission. self.lowerBlock(fd.body); if (!self.currentBlockHasTerminator()) self.builder.emitUnreachable(); } else if (ret_ty != .void) { diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 2dcdf103..12afd839 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -228,13 +228,13 @@ pub const TypeResolver = struct { const cc: types.TypeInfo.CallConv = switch (ft.abi) { .default => .default, .c => .c, - // `.compiler` (compiler-domain fn) and `.pure` (naked asm) are + // `.compiler` (compiler-domain fn) and `.naked` (naked asm) are // decl-level ABIs with no function-pointer-type calling // convention of their own; the IR function-type CC models only // sx-default vs C. An `abi(.compiler)` function-TYPE param marks // the bound function compiler-domain (handled at the call/bind // site, not here) — its CC is still sx-default. - .compiler, .pure => .default, + .compiler, .naked => .default, }; break :blk table.functionTypeCC(param_ids.items, ret_ty, cc); }, diff --git a/src/parser.test.zig b/src/parser.test.zig index e2a299d1..230c07bf 100644 --- a/src/parser.test.zig +++ b/src/parser.test.zig @@ -144,15 +144,15 @@ test "parser: bare extern leaves abi == .default" { // Lock: `abi(.c)` parses standalone (no extern/export) in the postfix slot — the // migrated spelling of the old `callconv(.c)` on an ordinary function pointer / -// fn decl. And `abi(.pure)` parses (naked-asm ABI). -test "parser: abi(.c) and abi(.pure) parse standalone" { +// fn decl. And `abi(.naked)` parses (naked-asm ABI). +test "parser: abi(.c) and abi(.naked) parse standalone" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const src = \\cb :: () -> i64 abi(.c) { 0; } - \\nk :: () -> i64 abi(.pure) { 0; } + \\nk :: () -> i64 abi(.naked) { 0; } \\ ; var parser = Parser.init(alloc, src); @@ -163,7 +163,7 @@ test "parser: abi(.c) and abi(.pure) parse standalone" { try std.testing.expectEqual(ast.ABI.c, decls[0].data.fn_decl.abi); try std.testing.expectEqual(ast.ExternExportModifier.none, decls[0].data.fn_decl.extern_export); try std.testing.expect(decls[1].data == .fn_decl); - try std.testing.expectEqual(ast.ABI.pure, decls[1].data.fn_decl.abi); + try std.testing.expectEqual(ast.ABI.naked, decls[1].data.fn_decl.abi); } // Lock: the postfix `abi(...)` slot PARSES on a STRUCT decl — `Name :: struct diff --git a/src/parser.zig b/src/parser.zig index 0b791b6d..dff7dca2 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1931,7 +1931,7 @@ pub const Parser = struct { } // Optional ABI / calling-convention annotation: `abi(.c)` / `abi(.zig)` / - // `abi(.pure)`. Sits in the postfix slot BEFORE the `extern`/`export` + // `abi(.naked)`. Sits in the postfix slot BEFORE the `extern`/`export` // linkage keyword (it is part of the function declaration). `abi(.zig)` // marks a binding to the comptime `compiler` library. const abi = try self.parseOptionalAbi(); @@ -3684,7 +3684,7 @@ pub const Parser = struct { return_type = try self.parseTypeExpr(); } - // Optional ABI annotation: abi(.c) / abi(.zig) / abi(.pure) + // Optional ABI annotation: abi(.c) / abi(.zig) / abi(.naked) const abi = try self.parseOptionalAbi(); // A closure is its own function boundary: clear the cleanup-body flags @@ -3793,7 +3793,7 @@ pub const Parser = struct { } /// Optional ABI / calling-convention annotation `abi(.c)` / `abi(.zig)` / - /// `abi(.pure)` in the postfix slot before `extern`/`export`. `.default` when + /// `abi(.naked)` in the postfix slot before `extern`/`export`. `.default` when /// absent. Subsumes the old `callconv(...)` spelling. fn parseOptionalAbi(self: *Parser) anyerror!ast.ABI { if (self.current.tag != .kw_abi) return .default; @@ -3801,16 +3801,16 @@ pub const Parser = struct { try self.expect(.l_paren); try self.expect(.dot); if (self.current.tag != .identifier) - return self.fail("expected ABI name ('.c', '.compiler', or '.pure') after '.'"); + return self.fail("expected ABI name ('.c', '.compiler', or '.naked') after '.'"); const abi_name = self.tokenSlice(self.current); const abi: ast.ABI = if (std.mem.eql(u8, abi_name, "c")) .c else if (std.mem.eql(u8, abi_name, "compiler")) .compiler - else if (std.mem.eql(u8, abi_name, "pure")) - .pure + else if (std.mem.eql(u8, abi_name, "naked")) + .naked else - return self.fail("unknown ABI (expected '.c', '.compiler', or '.pure')"); + return self.fail("unknown ABI (expected '.c', '.compiler', or '.naked')"); self.advance(); try self.expect(.r_paren); return abi; diff --git a/src/token.zig b/src/token.zig index 3896817e..83777ffb 100644 --- a/src/token.zig +++ b/src/token.zig @@ -42,7 +42,7 @@ pub const Tag = enum { kw_impl, // impl kw_Self, // Self (in protocol declarations) kw_inline, // inline (compile-time if/for/while) - kw_abi, // abi (ABI / calling-convention annotation: abi(.c)/abi(.zig)/abi(.pure)) + kw_abi, // abi (ABI / calling-convention annotation: abi(.c)/abi(.zig)/abi(.naked)) kw_extern, // extern (import: external linkage, C ABI, no body) kw_export, // export (define + expose: external linkage, C ABI) kw_asm, // asm (inline assembly expression / global asm decl)