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

@@ -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