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

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

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

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

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

View File

@@ -408,6 +408,17 @@ pub const LLVMEmitter = struct {
// its only references are in comptime code, so DCE drops the leftover
// declaration. See current/PLAN-COMPILER-VM.md (S3).
if (func.is_compiler_domain) continue;
// B1.0a (lock): `abi(.pure)` emission is not implemented yet — the
// LLVM `naked` attribute + asm-only body land in B1.0b. Bail LOUDLY
// (build-gating, like a comptime failure) rather than emit a framed
// body, whose prologue/epilogue would corrupt the deliberate
// SP-in ≠ SP-out of a context switch. See current/PLAN-FIBERS.md.
if (func.is_pure) {
const fname = self.ir_mod.types.getString(func.name);
std.debug.print("error: `abi(.pure)` function '{s}' LLVM emission not yet implemented\n", .{fname});
self.comptime_failed = true;
continue;
}
self.emitFunction(&func, @intCast(i));
}

View File

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

View File

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