fibers: rename ABI variant .pure -> .naked
"pure" universally means side-effect-free (GCC __attribute__((pure)), FP purity, D's pure) — the opposite of a register-clobbering context switch. The concept is "naked": no compiler-generated prologue/epilogue, body is raw asm that emits its own ret. That is the established term everywhere (LLVM's naked function attribute — which we literally emit — plus Zig callconv(.naked), Rust #[naked], GCC/Clang __attribute__ ((naked))). Rename the keyword + everything keyed off it so concept, surface, field, and the emitted LLVM attribute all agree. - ast.zig: ABI enum variant pure -> naked (+ doc). - parser: accept abi(.naked); error text updated. - IR Function.is_pure -> is_naked; type_resolver/decl/generic/pack/ emit_llvm references updated; diagnostics say abi(.naked). - examples 1800-1803 renamed *-pure-* -> *-naked-* (source + expected/ snapshots; .ir/.exit/.stdout/.stderr are byte-identical — the emitted IR is unchanged, only the keyword spelling differs). - docs (PLAN-FIBERS, CHECKPOINT-FIBERS, PLAN-POST-METATYPE, the design roadmap, the compiler-API checkpoint/design) updated; the naming rationale now records why .naked over .pure. No semantic change — pure cosmetics. Suite green (725/0).
This commit is contained in:
@@ -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 <lib>` 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user