Commit Graph

699 Commits

Author SHA1 Message Date
agra
1ffda415c2 feat(metatype): implement type_info($T) reflection (enum round-trip)
type_info reflects an enum / tagged-union INTO a TypeInfo value — the
inverse of define's decode — so define(declare(n), type_info(T)) mints
a byte-identical copy with NO literal variant list.

- inst.zig: new BuiltinId.type_info (comptime-only, like declare/define).
- lower/call.zig: replace the 'not yet implemented' bail. Resolve $T at
  lower time, reject non-enum/non-tagged-union loudly with a good span,
  emit callBuiltin(.type_info, [const_type], TypeInfo).
- interp.zig: reflectTypeInfo builds the exact nested-aggregate Value
  defineEnum decodes — variant {name,payload}, slice {data,len}, EnumInfo
  {variants}, TypeInfo {tag0, EnumInfo}. tagged_union reflects field.ty
  (tagless already void); payloadless `enum` reflects void per variant.
- emit: unchanged — type_info is always comptime-evaluated, the existing
  comptime-only else arm (shared with declare/define) never fires.

0619 turns green: a source enum (circle:f64 / rect:i64 / empty) reflected
and reconstructed, constructs and matches like the original.
2026-06-16 22:52:53 +03:00
agra
2f0905b407 fix(0139): reject by-value self-referential types loudly (was a segfault)
A nominal aggregate that contains itself (or a mutual peer) BY VALUE has no
finite layout and infinite-recursed typeSizeBytes into a stack overflow —
for SOURCE enums/structs as well as comptime-constructed types.

New `checkInfiniteSize` pass (lower/decl.zig, Pass 1g — after type
registration, before body lowering): walks the by-VALUE containment graph
(pointer/slice/optional payloads break the cycle, so `*Self` stays valid);
on a back-edge it emits a loud diagnostic — "type 'X' is infinitely sized
(it contains itself by value); use a pointer ('*X') to break the cycle" —
and poisons the offending field to `.unresolved` so sizing can't recurse
before the build halts on the error. Covers source + declare/define types,
direct + mutual recursion.

examples/1178 locks the diagnostic; issue 0139 marked RESOLVED. This also
completes METATYPE PLAN F5's by-value-self-reference rejection. Full suite
green (675).
2026-06-16 22:24:31 +03:00
agra
7a9db03bcc green(metatype): declare(name) + self-reference (recursive enums via *Name)
declare now takes the type's NAME — `declare(name) -> Type` — because the
compiler needs it at compile time to register the forward type, which is
what makes self-reference resolve. EnumInfo drops `name` (it lives on
declare now); define completes the handle's body in place (the slot is
already named).

Self-reference mechanism (evalComptimeType): before lowering a comptime
type expression, preregisterForwardTypes scans it (and a called ctor fn's
body) for `declare("Name")` calls and registers each as an empty forward
nominal type AND binds it as a type alias. The alias is essential: a
`Name :: ctor()` decl makes `Name` a const_decl author, so a `*Name`
self-reference resolves through the forward-ALIAS path
(type_aliases_by_source), which a bare findByName registration doesn't
satisfy. With both in place `*Name` resolves to the forward slot at lower
time; the interp's declare returns that same slot; define fills it.

  List :: make_list();
  make_list :: () -> Type {
      h := declare("List");
      return define(h, .enum(.{ variants = .[
          EnumVariant.{ name = "cons", payload = *List },
          EnumVariant.{ name = "nil",  payload = void } ] }));
  }

Verified: cons/nil construct + match (direct and through the pointer),
multi-node list traversal via a recursive `count(*List)`. meta.sx
RecvResult/TryResult + examples 0614/0615/0617 updated to declare(name);
full suite green (673).
2026-06-16 22:02:48 +03:00
agra
12e2ff7ef4 docs+rename: erase the reify name everywhere — stream is METATYPE
The compiler concept is declare/define (comptime type construction); the
old "reify" framing is gone from the entire repo.

- Rename: PLAN-REIFY → PLAN-METATYPE, CHECKPOINT-REIFY → CHECKPOINT-METATYPE,
  PLAN-POST-REIFY → PLAN-POST-METATYPE (both rewritten around declare/define);
  examples 0614/0615/0617 → comptime-metatype-* (+ their expected/ triplets),
  headers rewritten.
- Scrub reify from design/execution-evolution-roadmap.md (§7 step 3 contracts,
  §8.1, §9 decisions, §10 gates) → declare/define / comptime type construction.
- core.sx prelude pointer + parser.test.zig surface lock updated to the
  declare/define builtins (define(handle, info) -> Type; EnumInfo.name).

No behavior change; renamed examples match their renamed snapshots. Full
suite green (673), all unit tests pass. Zero `reify` tokens remain in
src/docs/sx/examples.
2026-06-16 21:23:05 +03:00
agra
5f2419854e green: erase the sx reify sugar — declare/define are the only constructors
Per the directive to strip reify entirely: the sx `reify(info)` one-shot is
removed. `define(handle, info)` now RETURNS the (completed) handle, so the
one-shot constructor chains as a single expression:
    T :: define(declare(), .enum(.{ name = "T", variants = ... }));

- meta.sx: drop reify; RecvResult/TryResult use `define(declare(), …)`.
- interp .define returns the handle type_tag (was void); call.zig lowers it
  with `Type` result and sets the info arg's target type to TypeInfo so the
  intercepted call still infers the `.enum(…)` literal.
- returnExprMintsType: a type-fn body that returns `define(…)` (or a bodied
  non-generic Type-returning sx helper) is comptime-evaluated.
- examples 0614 (direct) + 0615 (type-fn) use `define(declare(), …)`.

Full suite green (673). Files/docs still carry the old reify naming — the
rename sweep is the next commit.
2026-06-16 21:12:32 +03:00
agra
8ae655687a green(reify): type-fn bodies comptime-evaluated; reify fully removed from the compiler
Second slice of the re-architecture — the compiler now has ZERO type-
construction code beyond declare/define.

- instantiateTypeFunction: a type-fn body returning a computed Type (a call
  to a non-generic, bodied, Type-returning fn) is comptime-evaluated with the
  type bindings active, then renamed to the mangled instantiation name for
  identity (renameNominalType). Replaces the old reify-call pattern-matching.
- DELETED: reifyType (lower/nominal.zig), findReturnReifyCall (lower/generic.zig),
  and the stale inline-position reify gate in resolveTypeCallWithBindings.
- evalComptimeType (was evalComptimeTypeNamed): pure eval, no rename; the
  type-fn caller renames explicitly. renameReifiedType → renameNominalType.
- The TYPE NAME now travels in the data: EnumInfo gains `name`, and define()
  names the slot from it (the compiler derives no name from a binding LHS).
  examples/0614/0615 carry `name = "..."`; RecvResult/TryResult set it too.
- field_type stays a reflection #builtin (reads a type); only construction
  moved out. All reify mentions stripped from compiler source.

examples 0614/0615/0617 run on the floor. Full suite green (673).
2026-06-16 21:03:16 +03:00
agra
442a70b8c9 green(reify): declare/define floor — reify is sx; E :: reify(...) comptime-evaluated
First slice of the re-architecture. The compiler gains two comptime
type-construction builtins — declare() (mint an empty/undefined nominal
slot) and define(handle, info) (decode a TypeInfo VALUE + complete the
slot) — executed by the interpreter against a new `mint` TypeTable handle
(setMintTable). reify becomes PLAIN sx in meta.sx:
  reify :: (info) -> Type { h := declare(); define(h, info); return h; }

`E :: f(...)` where f is a non-generic Type-returning fn (reify, and later
make_enum) is now comptime-evaluated via evalComptimeTypeNamed: wrap the
call in a throwaway comptime fn, run it through the interp with the mint
table enabled so declare/define mint the type, read back the type_tag, and
rename the anonymous slot to the binding name. The compiler has ZERO reify
knowledge at the decl site — the old `E :: reify` hook is deleted.

examples/0614 (inline reify) now runs on this floor. Full suite green (673).

INTERMEDIATE: reifyType + findReturnReifyCall still serve the type-fn path
(0615/0617) and will be deleted in the next slice (type-fn body
comptime-eval), after which the compiler has no reify code at all.
2026-06-16 20:39:02 +03:00
agra
ac8c689518 green(reify): field_type($T, i) -> Type over the type table
REIFY Phase 2.1. fieldTypeOf (lower/generic.zig, re-exported on Lowering)
returns the i-th member type of T: struct field / tagged-union + union
variant payload (.void for a tagless variant) / tuple element / array +
vector element. Out-of-range and memberless types poison to .unresolved
with a loud diagnostic (never a silent default). Wired into
resolveTypeCallWithBindings (replacing the Phase-2 bail); since it folds
to a TypeId at lower time it composes inside type_eq / type_name / any
type-arg slot.

examples/0616 green: struct fields (name via field_name + type via
field_type), type_eq fold, tagged-union payloads incl. quit -> void.
Suite green (672 examples, 447 unit).

type_info($T) -> TypeInfo (reflect into a value, inverse of reify) is
NOT done — still bails loudly; it's the larger Phase 2.2 step (widen the
TypeInfo data model + comptime value construction). Plan/checkpoint updated.
2026-06-16 19:06:57 +03:00
agra
18a4f9dd54 green(reify): type-fn over reify memoizes by mangled name (identity)
REIFY Phase 1.1 (Phase 1 complete). instantiateTypeFunction detects a
type-fn body that returns reify(...) (findReturnReifyCall) and routes it
to reifyType under the instantiation's name — mangled for inline use,
the alias name for `Foo :: Box(i64)` — with the type-arg bindings active
so reify payloads (`payload = T`) resolve against the instantiation args.
Placed before the general case, whose resolveTypeWithBindings would
route the reify call to the inline-position loud bail.

Registering under the mangled name lets the top-of-instantiation cache
return the SAME TypeId on a second instantiation, so Box(i64) resolved
at two independent sites is ONE type (Contract 1). examples/0615 green
(build()->consume() cross-site + `b : Box(i64) = .none`). Suite green
(671 examples, 447 unit).
2026-06-16 18:54:11 +03:00
agra
353109206b green(reify): implement reify(.enum) — mint a flat enum from TypeInfo
REIFY Phase 0.2 (Phase 0 complete). Lowering.reifyType (lower/nominal.zig)
reads the flat-enum TypeInfo literal off the AST, synthesizes an
ast.EnumDecl, and feeds it through the SAME type_bridge.buildEnumInfo
path source enums use — so the minted type is byte-identical to a
hand-written `enum { value: i64; closed; }` and flows through enum
codegen (layout / construct / match) UNMODIFIED (Contract 2).

Wired at the `E :: reify(...)` const-decl hook in lower/decl.zig
(replacing the Phase-0.0 loud bail). Unsupported argument shapes bail
loudly via reifyBail — never a silent default. The generic.zig inline
reify path now reports it's only supported in a `::` binding (Phase 0).

examples/0614 green: reify a {value: i64, closed} enum, construct
.value(3) and .closed, match both -> "value 3" / "closed". Full suite
green (670 examples, 447 unit).
2026-06-16 18:32:05 +03:00
agra
b25a2f60d6 feat(parser): reserved keyword as member name after .
After a leading `.` (enum literal `.enum`, field access `x.enum` /
`E.struct`, match arm `case .enum:`) a reserved keyword is unambiguously
the member/variant NAME — the dot rules out the keyword reading — so no
backtick escape is needed. A declaration of such a variant still needs
the backtick (enum { `enum: i64 }), since the decl site has no dot.

Adds Parser.dotMemberName() (identifier OR identifier-shaped keyword)
and routes the leading-dot enum-literal and postfix field-access sites
through it. readme updated. The reify example 0614 now uses the cleaner
reify(.enum(...)) spelling (still xfail — reify lands next commit).
2026-06-16 18:22:21 +03:00
agra
81669c72b7 lock(reify): meta.sx surface + bodyless #builtin decls + loud bails
REIFY Phase 0.0. Add the comptime type-metaprogramming surface as the
on-demand module modules/std/meta.sx (NOT the prelude — declaring its
data types in always-loaded core.sx interns them into every module's
type table and shifts every .ir snapshot):

  - EnumVariant / EnumInfo / TypeInfo data types. TypeInfo's variant uses
    the backtick raw escape `enum so it reads as the keyword.
  - reify / type_info / field_type as bodyless #builtin decls.

Each builtin bails LOUDLY when reached unimplemented (no silent default):
  - reify(...) in a :: type-alias position -> decl.zig .call branch
    (also the Phase 0.2 construction hook); poisons the alias .unresolved.
  - reify / field_type in any other type position ->
    generic.zig resolveTypeCallWithBindings.
  - type_info(...) in expression position -> call.zig tryLowerReflectionCall.

Unit test src/parser.test.zig (registered in root.zig) locks that the
decls parse. zig build test green (447 unit, 669 examples).
2026-06-16 17:44:19 +03:00
agra
b6a7378af4 feat(dist): bundled-zig link backend for hermetic macOS/Linux/Windows builds
Drive a bundled `zig` as `zig cc` for the AOT link step, supplying lld + CRT
+ libc (musl/glibc/mingw) so `sx build` produces native binaries with no host
toolchain. Default Linux output is static musl (portable-anywhere).

- src/zig_backend.zig: discover zig ($SX_ZIG / bundled-next-to-exe / PATH);
  bundled-vs-PATH provenance gates auto-activation.
- src/target.zig: selectZigLinker + emitZigLinkArgv + zigTargetTriple, dispatched
  before the per-OS branches; macOS/Linux/Windows in scope.
- src/ir/emit_llvm.zig: LLVMNormalizeTargetTriple so vendor-less zig triples
  (e.g. x86_64-windows-gnu) parse to the correct OS/object format (COFF not ELF).
- src/main.zig: --self-contained / --no-self-contained; linux-musl, linux-musl-arm,
  windows-gnu shorthands; de-vendor linux/linux-arm to match the corpus runner.
- examples/1660: Windows Win32 print-42 + exit(0) via kernel32 (ir-only off-Windows).

Auto-activates only for a bundled zig; a PATH-only zig engages under
--self-contained, so native dev/CI builds are never silently rerouted.

Docs: readme Cross-Compilation, design/bundled-zig-link-backend-design.md, current/PLAN-DIST.md.
2026-06-16 15:56:06 +03:00
agra
066ba54346 feat(asm): portable symbol refs — auto-inject :c operand modifier
A `%[name]` that references a symbol ("s") operand without an explicit
modifier now lowers to `${N:c}` (LLVM 'bare constant — no punctuation')
instead of `${N}`. This makes `bl %[fn]` / `call %[fn]` portable across
targets with no per-arch knowledge: x86 would otherwise render `$cb`
(an invalid call target, requiring a hand-written `:P`); aarch64 is
unaffected. Verified `:c` is equivalent to `:P` for x86-64 calls (both
emit R_X86_64_PLT32), and correct for branch targets, RIP-relative
addressing, and `$`-prefixed absolute immediates.

renderAsmTemplate injects `:c` only for symbol operands lacking an
explicit modifier (asmNamedIsSymbol helper); an explicit `%[name:X]`
still wins (escape hatch). x86 example 1659 drops its `:P` for the same
plain `%[fn]` as aarch64 1656. Snapshots regen to `${N:c}`. zig build
test green (668 corpus, 446 unit).
2026-06-16 09:04:23 +03:00
agra
10f4137cbd feat(asm): symbol operands ("s") — direct call/branch to a function
A `"s"` input operand feeds a function/global symbol; the template's
%[name] emits the platform-mangled name, so `bl %[fn]` / `call %[fn]`
branches DIRECTLY to it (PC-relative, no register load — one fewer
indirection than register-indirect `blr`).

Lowering: an `"s"` input lowers its RHS normally (a function name →
`ptr @fn`); the rejection added last commit is removed. Emit: a symbol
operand is passed with its OWN llvm type (LLVMTypeOf) and no coercion —
the function value is a `ptr`, and the old coerce-to-register-int path
mistyped it and failed the verifier. New asmIsSymbol helper.

Verified on aarch64: examples/1656 (sx → asm → bl _cb → sx → 42); the
emitted asm is a direct `bl <_cb>` (objdump-confirmed), IR constraint
`...,s,...`(ptr @cb). Flipped 1656 from the rejection lock to a runnable
aarch64 example. zig build test green (665 corpus, 446 unit).
2026-06-16 08:24:53 +03:00
agra
c187122531 test(asm): reject symbol "s" operands cleanly + lock (symbol-op prep)
A symbol operand (constraint "s") feeds a function/global symbol whose
mangled name the template emits — enabling a DIRECT `bl %[fn]` (one
fewer indirection than register-indirect `blr`). Until now `"s" = fn`
fell through to emit and produced an LLVM-verifier crash (param type
mismatch). Reject it at lowering with a clear diagnostic instead, and
lock that with examples/1656-platform-asm-symbol-operand.sx. The next
commit implements it and flips the example to run (→ 42).
2026-06-16 08:19:18 +03:00
agra
cb6c032c58 feat(asm): indirect-memory =*m place outputs
Implements indirect-memory (`=*m`) `-> @place` outputs — the last
substantive asm feature. Unlike a write-through `=` output (which
returns a value that is then stored), an indirect output passes the
place ADDRESS to the asm and the asm writes through it; there is no
return slot.

emitInlineAsm:
  - indirect outputs are excluded from the LLVM return type;
  - their pointer is passed as an opaque `ptr` call arg, placed FIRST
    (the arg-consuming constraint order is: output-section indirect
    pointers, then inputs, then read-write tied seeds);
  - each indirect arg gets an `elementtype(T)` call-site attribute
    (required in the opaque-pointer era), T = the pointee type;
  - the store-back loop skips indirect outputs (already written).
New asmIsIndirect helper. Lowering stops rejecting `*` (constraint kept
verbatim; `=*m` reaches the constraint string as-is). asmOperandIndex
is unchanged — indirect outputs still count as operands, so `%[name]`
${N} numbering holds.

Verified by running on aarch64: store-through-pointer (str x9, %[out]
→ 42, IR `=*m,~{x9}` with `ptr elementtype(i64)`) and a mixed case
(indirect + value output + input → `=*m,=r,r`, indirect ptr arg first,
${0}/${1}/${2} correct). 1652 flipped from the rejection lock to a
runnable aarch64 example (ir-only elsewhere). zig build test green
(661 corpus, 446 unit).
2026-06-16 07:09:17 +03:00
agra
2a954ceeb6 fix(0138): diagnose @scalar-const address-of (no storage)
A scalar `::` constant folds to its value and has no storage. The
unary `.address_of` lowering (src/ir/lower/expr.zig) skipped the
alloca path (is_alloca == false) and resolveGlobalRef (scalar consts
get no storage global), falling through to the generic addr_of arm,
which reinterpreted the folded value as a pointer:
`inttoptr (i64 <value> to ptr)`. That wild pointer segfaulted on
deref and emitted invalid stores for inline-asm `-> @const`.

Diagnose instead, in the address_of(identifier) path: a non-alloca,
non-ref-capture, non-pack-elem scope binding (local scalar const) and
a module_const_map name not backed by storage (module scalar const)
both report "cannot take the address of constant '<name>' — a scalar
'::' constant has no storage …" and return a placeholder Ref. Chose
diagnose over materializing read-only storage (consistent with the
fold-only scalar model). Array/struct consts keep real storage and
stay addressable (@K/@LIT unchanged).

Also gives the ASM stream's planned output-to-const rejection for
free — asm `-> @const` lowers through the same path. Regression:
examples/1177-diagnostics-addr-of-const-rejected.sx. Resolves 0138.
2026-06-16 06:29:36 +03:00
agra
4128416d48 feat(asm): read-write + place outputs
Implements read-write (`+r` / `+{reg}`) `-> @place` outputs. LLVM has
no `+` constraint, so a read-write place lowers to:

  - an output `=` constraint (return slot, stored back through the
    place after the call), with the leading `+` rewritten to `=`; plus
  - a TIED input constraint (the decimal index of that output) appended
    after the regular inputs, seeded with the place's loaded value
    passed as a call arg.

Tied inputs are appended last so existing operand indices (%[name] ->
${N}) are undisturbed; asmOperandIndex stays correct. Lowering no longer
rejects `+` (indirect `*` still rejected). emitInlineAsm grows the
arg/param arrays by the rw count, loads each seed, and emits the tied
constraint.

Verified by running: increment-in-place (41 -> 42) and a mixed case
(rw place + regular input + value output) producing the textbook
"=r,=r,r,0" constraint with correct ${N} indices. 1650 flipped from
the rejection lock to a runnable aarch64-pinned example (ir-only
elsewhere). zig build test green (658 corpus, 446 unit).
2026-06-15 23:07:38 +03:00
agra
967005621a feat(asm): Phase 2 — -> @place write-through outputs
An asm result can be STORED through a place (a local / struct field) instead of
returned; the place output does not join the result tuple.

- parser.zig: `-> @place` parses `@place` as an ordinary address-of expression
  → an out_place operand (the in-function form; reuses the existing `@` prefix).
- inst.zig: AsmOperand gains out_ty (the output slot's value type) so emit can
  build the combined return struct without re-deriving from Inst.ty.
- lower/expr.zig: out_place operand = the lowered @place address, out_ty = the
  pointee. Read-write (`+`) and indirect-memory (`*`) constraints rejected loudly
  (not yet implemented) rather than miscompiled.
- ops.zig emitInlineAsm: the LLVM return type is built from ALL outputs
  (out_value + out_place); after the call, out_place slots are stored through
  their address and out_value slots rebuild the sx result. Fast path when there
  are no place outputs (the struct return IS the result — pure-value asm IR
  unchanged).

Verified: write-to-local (42), struct field, mixed value+place (v=10 b=20), `+`
rejected. Locked with 1649-platform-asm-place-output (mixed, runs on aarch64).

zig build test green (657 corpus, 446 unit).
2026-06-15 22:47:34 +03:00
agra
4d75b9323c feat(asm): Phase F — global (module-scope) asm
A top-level `asm { "tmpl", };` block (template only) lowers to LLVM `module asm`;
a lib-less `extern` declaration calls into the symbols it defines (the import
direction reuses the existing C-FFI extern path — no new surface).

- ast.zig: asm_global node (AsmGlobal { template }).
- parser.zig: parseAsmGlobal, dispatched from parseTopLevel on kw_asm — rejects
  `volatile` and any operands/clobbers (template only). The in-function asm
  expression form stays in parsePrimary.
- module.zig: Module.global_asm list; lower/decl.zig captures each template in
  lowerMainAndComptime (the real top-level pass — lowerDecls is dead for
  top-level); emit_llvm.zig emit() appends each via LLVMAppendModuleInlineAsm in
  source order.
- the new node forced asm_global arms in sema.zig (analyzeNode +
  findNodeAtOffset) and semantic_diagnostics.zig (checkBindingNames).

Verified end-to-end: an aarch64 `_my_add` global routine, called via `extern`,
returns 42 — AOT only (the ORC JIT doesn't link module-asm symbols; global-asm
symbols live in the final linked binary). Locked with 1648-platform-asm-global
({ "aot": true, "target": "macos" } → AOT build+run on aarch64, ir-only else).

zig build test green (656 corpus, 446 unit).
2026-06-15 22:22:29 +03:00
agra
d3c6ffed5a feat(asm): Phase E — multi-output asm returns tuples
Replaces the N>1 "Phase E" bail with a shared asmResultType helper (lowering +
inferType) that derives the result type from the out_value operands: 0→void,
1→T, N→a named tuple (fields named via the §II.5 effective-name rule).

Key realization: toLLVMType(tuple) already produces a literal struct {T1,…,Tn} —
exactly what LLVM's multi-output inline asm returns — so emit needs NO change.
Building the op with a tuple result type makes the asm call return the struct,
which IS sx's tuple value (destructured by the normal tuple_get path).

inferType's .asm_expr arm now also delegates to asmResultType (single owner), so
`return asm`, `x := asm`, and `q, r := asm` all agree on the type.

Verified end-to-end on aarch64: split(0x1234)→(lo=52,hi=18), a udiv/msub
divmod→(3,2). IR: `call { i64, i64 } asm "divq ${4}",
"={rax},={rdx},{rax},{rdx},r,~{cc}"(…)` → extractvalue → tuple.

1640 → the x86_64 multi-output IR lock (ir-only); 1647 → a multi-output example
that runs on aarch64.

zig build test green (655 corpus, 446 unit).
2026-06-15 21:55:38 +03:00
agra
5a5e04c6d5 feat(asm): Phase C.1 + D — inline asm codegen (runs end-to-end)
lowerAsmExpr stops bailing and builds the inline_asm op: resolves each operand's
effective name (§II.5 — explicit [name] else the {reg} pin), interns
template/constraints/clobbers, lowers input Refs, derives the result TypeId
(0→void, 1→T). Adds the last deferred validation (every %[name] must name an
operand). Multi-output (N>1) bails with a named "Phase E" diagnostic.

emitInlineAsm (backend/llvm/ops.zig) ports Zig's airAssembly: assembles the LLVM
constraint string (outputs → inputs → ~{clobber}, ',' → '|'), rewrites the
template (%[name]→${N}, %%→%, $→$$, %=→${:uid}), then LLVMGetInlineAsm +
LLVMBuildCall2 (AT&T dialect). Dispatch wired in emit_llvm.zig (replacing the C.0
@panic tripwire).

inferType gains an .asm_expr arm (expr_typer.zig) so a bare `x := asm {…-> T}`
binding types correctly — without it the binding inferred .unresolved and
silently produced 0.

llvm_shim.c: LLVMInitializeNativeAsmParser() — the JIT must assemble inline asm
at run time.

Verified end-to-end on the aarch64 host: `mov`/`add` with register-class inputs
and a value output run (exit 42/99), `nop volatile` runs (exit 0). IR is
textbook: `call i64 asm "add ${0},${1},${2}", "=r,r,r"(…)`.

Locked with 1645 (aarch64 add, runs; ir-only on non-aarch64) + 1646 (:= binding).
Updated 1640 (now Phase-E bail) + 1642 (now runs).

zig build test green (654 corpus, 446 unit).
2026-06-15 21:39:54 +03:00
agra
6c08de8ec1 feat(asm): Phase C.0 — add inline_asm IR op (lock, no behavior change)
Adds the `inline_asm: InlineAsm` opcode to the IR Op union (inst.zig): interned
template + operand list (role/name/constraint/operand) + interned clobber names
+ has_side_effects; the result rides on Inst.ty (void / scalar / tuple).

The new variant forces coverage in the exhaustive Op switches:
- interp.zig: loud bailDetail — inline asm is never comptime-evaluable.
- print.zig: an IR-dump arm.
- emit_llvm.zig: a @panic TRIPWIRE — emit lands in Phase D, and until then
  lowerAsmExpr still bails, so no inline_asm op is ever created. Reaching emit
  would mean lowering switched over before emit was ready; crash loudly rather
  than miscompile.

No behavior change: lowering still bails, the op is constructed only in the new
`inline_asm op shape` unit test (inst.test.zig).

zig build test green (652 corpus, 446 unit).
2026-06-15 21:00:12 +03:00
agra
5f444aae26 feat(asm): Phase B.1 — operand-name validation (echo + duplicates)
Extends lowerAsmExpr with a pinnedRegister(constraint) helper and two §II.5
operand-naming checks, in the compile path before the codegen bail:

- reject the echo form `[eax] "={eax}"` — a label identical to the register its
  own constraint pins is redundant (the operand is already auto-named after the
  register); the useful form is a label that differs (`[quot] "={rax}"`);
- reject duplicate operand names (ambiguous %[name] / result field).

Locked with 1643-platform-asm-echo-name and 1644-platform-asm-duplicate-name.

zig build test green (652 corpus, 445 unit).
2026-06-15 20:41:41 +03:00
agra
1040b8c776 feat(asm): Phase B.0 — validate asm shape in the compile path
Restructures the .asm_expr lowering arm into lowerAsmExpr, which validates the
asm shape with specific named diagnostics BEFORE the not-yet-implemented codegen
bail, so the user sees the real problem first. Two checklist items enforced:

- template must be a compile-time-known string ("..." or #string), not a
  runtime expression;
- an asm with no value outputs must be `volatile` (else its effects could be
  deleted) — mirrors Zig's rule.

Valid shapes still bail with the "codegen not yet implemented" message. Result-
type derivation + the operand auto-naming rule stay deferred to Phase C, where a
real IR op makes the result type observable/testable.

Locked with 1641-platform-asm-missing-volatile (the volatile error) and
1642-platform-asm-nop-volatile (no-output + volatile accepted → codegen bail).

zig build test green (650 corpus, 445 unit).
2026-06-15 20:35:43 +03:00
agra
f8e029d719 feat(asm): Phase A.1 — parse asm { … } into AsmExpr; loud lowering bail
`asm volatile? { "tmpl", [name]? "constraint" (-> Type | = expr), …,
clobbers(.…) }` now parses into a flat-operand AsmExpr/AsmOperand (ast.zig +
parser.zig parseAsmExpr, dispatched from parsePrimary on .kw_asm). `volatile`
and `clobbers` are recognized contextually (not reserved). `-> @place`
write-through is rejected with a clear "Phase 2" parse error.

Codegen is not implemented yet (IR op + LLVM emit are Phases C–E), so lowering
bails LOUD + named via an explicit .asm_expr arm in lower/expr.zig (not the
generic unknown_expr else) — emitPlaceholder makes hasErrors() abort the build
on the message.

The new asm_expr tag forced (and got) arms in three exhaustive Node.Data
switches: sema.zig analyzeNode + findNodeAtOffset, semantic_diagnostics.zig
checkBindingNames — each recurses into template + operand payloads.

Design: adopted the operand auto-naming rule (design §II.5) — name auto-derived
from a {reg} pin, explicit [name] only when it differs or for register-class
operands, echo form rejected. Typing-stage rule; parser stores name: ?[]const u8.

Locked with examples/1640-platform-asm-parse.sx (multi-output divmod: named
operands, register pins, clobbers — parses then bails, called from main).

Also files issue 0137 (pre-existing, orthogonal: `sx run` with no `main`
segfaults via an unguarded JIT entry lookup in target.zig — not an asm bug).

zig build test green (648 corpus, 445 unit).
2026-06-15 20:21:25 +03:00
agra
3c9ecd0b42 feat(asm): Phase A.0 — add kw_asm keyword + lex test
`asm` now lexes as a dedicated `kw_asm` keyword (Token.Tag + keyword map entry).
`volatile` and `clobbers` stay out of the global keyword table — they are
recognized contextually only inside an `asm { … }` body (PLAN-ASM Deviation 4).

- token.zig: kw_asm tag + `.{ "asm", .kw_asm }` map entry.
- lsp/server.zig: classifyToken exhaustive switch gained the .kw_asm arm
  (the new enum value forced coverage — intended tripwire).
- lexer.test.zig (new, wired into root.zig barrel): locks `asm`->kw_asm and
  `volatile`/`clobbers`->identifier.

Lock commit (behavior-locking passing test). zig build test green (445 unit).
2026-06-15 18:32:34 +03:00
agra
0095584105 test(asm): Phase 0.1 — corpus ir-only branch for cross-target examples
When a `.build` target doesn't match the host, the runner can't execute the
example here, so it verifies via `sx ir --target` only: asserts exit + the `.ir`
snapshot (stdout) + diagnostics (stderr), never `.stdout`. An `.ir` snapshot is
REQUIRED in ir-only mode — its absence is a loud failure, never a silent pass.

- corpus_run.test.zig: ir_only flag (target set & !hostMatchesTarget); first
  dispatch arm runs `sx ir`, sets act_exit/act_err/act_ir; skip stdout in both
  update and verify modes; require ir_raw.
- lock fixture 1639-platform-target-cross (asm-free main, target x86_64-linux,
  checked-in .ir). Verified: corrupt .ir => IR mismatch; delete .ir => require
  failure.

Test-infra only; no compiler code. zig build test green (647 corpus, 444 unit).
2026-06-15 18:19:17 +03:00
agra
c88f4fbcef test(asm): Phase 0.0 — corpus target-gating + .build JSON config
Adds per-example build/run directives to the corpus runner via an optional
`expected/<name>.build` JSON sidecar (`BuildConfig { aot, target }`), replacing
the standalone `.aot` marker. Threads `--target` into the run/build/ir spawns
and gates the execute path on host arch+os match; a cross-target example fails
loudly ("ir-only mode not yet implemented") pending Phase 0.1.

- corpus_run.test.zig: BuildConfig + std.json parse (unknown-key => error),
  hostMatchesTarget (shorthand-expand + arch/os token match, arm64->aarch64),
  withTarget argv helper; unit tests for both.
- migrate 1226/1227 `.aot` markers -> `.build` { "aot": true }.
- lock fixture 1638-platform-target-host (`.build` { "target": "macos" }).

Test-infra only; no compiler code. zig build test green (646 corpus, 444 unit).
2026-06-15 17:37:35 +03:00
agra
d6a9c4f0c4 fix(diagnostics): locate import parse errors in the imported file
A parse error raised while resolving an `#import` was rendered against the
ROOT file's source — the caret landed on an unrelated line (often a comment)
even though the message named the correct imported file.

Two compounding causes:
- core.zig wired `diagnostics.import_sources` only AFTER import resolution
  returned, but a parse error aborts mid-resolution (before that wiring), so
  the renderer had no imported sources and fell back to the root file. Wire it
  (and seed the main-file source) BEFORE resolving.
- imports.zig emitted the diagnostic at the importer's `#import` span instead
  of the parser's actual error offset inside the imported file, and didn't pin
  the diagnostic's source_file to that file.

parser.zig now records `err_end` alongside `err_offset` for a proper caret
width. New `DiagnosticList.addFmtInFile` renders against an explicit source
file; imports.zig uses it with `importErrSpan(&p)`.

Regression test: examples/1176-diagnostics-import-parse-error-location
(importer + deliberately-broken companion; caret must land in the companion).
2026-06-15 15:09:40 +03:00
agra
f3c9747f5a chore(ffi-linkage): post-stream polish — vscode keywords + vestigial param + extension metadata
Two post-stream follow-ups flagged in CHECKPOINT-EXTERN-EXPORT.md, plus a
reproducible vscode-extension packaging setup:

- parser: drop the vestigial `RuntimeClassPrefix.is_extern` field and
  `parseRuntimeClassDecl`'s `is_extern` param. Always false since the
  `#foreign` token was deleted; the postfix `extern`/`export` keyword is the
  sole reference-vs-define decider. No behavior change (644 corpus / 442 unit).
- vscode grammar: highlight `extern`/`export` as `storage.modifier.sx`.
- vscode packaging: declare `@vscode/vsce` as a devDep + add `package` /
  `vscode:prepublish` scripts so the vsix rebuilds reproducibly (was an
  ambient tool). Add repository/homepage/bugs (Gitea), icon (swipelab logo,
  256x256), galleryBanner, README with cover banner. Rebuilt the vsix.
2026-06-15 12:57:07 +03:00
agra
dfae690b31 refactor(ffi-linkage)!: Phase 9.0 — delete the hash_foreign token (src is foreign-free)
Per user directive (total purge): remove the hash_foreign token entirely rather than
keep it for a friendly deprecation message. Deleted: the token enum (token.zig), the
lexer keyword entry + directive-list mention + lex test (lexer.zig), the 4 parser
rejection sites + 2 lookahead clauses + the runtime-class prefix #foreign peek arm
(parser.zig), and the lsp completion arm (server.zig). '#foreign' now lexes as an
invalid '#' token → a generic 'expected ;' parse error (no migration hint — the
accepted UX cost of zero-foreign). Deleted examples/1176-diagnostics-foreign-removed
(its purpose, the friendly rejection, no longer exists).

src/ now contains ZERO 'foreign' (case-insensitive). Suite green (645 corpus / 443
unit, 0 failed). Remaining for the 9.4 gate: issues/*.md prose + example filenames.
2026-06-15 10:59:59 +03:00
agra
811a280517 refactor(ffi-linkage): Phase 9.3 — purge 'foreign' from comments (src caps + examples + docs)
src/: ~21 capital-Foreign comments the case-sensitive verify grep missed
(Foreign-class→Runtime-class, Foreign path→Runtime path, Foreign decls→Extern decls,
FOREIGN function→extern function) across calls/inst/ffi_objc/jni_descriptor/emit_llvm/
c_import/lower.*/ops. src 'foreign' now = ONLY the hash_foreign token + 4 rejection
messages (9.0-delete targets). examples/*.sx comments → extern/runtime-class (1219
stdout regen; KEPT 1176). docs/inline-asm-design + debugger purged. Comments only —
no build impact. 9.0 ratified: DELETE hash_foreign token next.
2026-06-15 10:52:56 +03:00
agra
dc51c4b5bf refactor(ffi-linkage): Phase 9.3-src — purge 'foreign' from src/ comments + a user-facing diagnostic
Reword every 'foreign' comment to the extern/runtime-class vocabulary matching the
renamed identifiers (foreign call→extern call, foreign class→runtime class, foreign
path→runtime path, the #foreign-literal comment mentions → extern, etc.). Also fixes
two USER-FACING issues: the 'expected … #foreign … after type annotation' parse error
no longer advertises the removed keyword, and the Android 'no #jni_main' help
diagnostic now shows '#jni_class(…) extern' instead of the rejected '#foreign
#jni_class'. Removed the now-dead prefix-#foreign-vs-postfix conflict branch in
parseRuntimeClassDecl (the caller rejects #foreign before it runs).

src/ now contains 'foreign' ONLY in the hash_foreign token machinery + its 4
rejection messages — the deprecation mechanism (kept per the 9.0 recommendation; the
message MUST name #foreign to guide migration). Snapshot-neutral; suite green
(646 corpus / 444 unit, 0 failed).
2026-06-15 09:35:00 +03:00
agra
8cca3b9dde refactor(ffi-linkage): Phase 9.2d — rename foreign_path → runtime_path (coupled .sx↔.zig↔hook)
The JNI/runtime-class path (Decision 5, Runtime* family). Coordinated across the
hook boundary so the BuildOptions accessor + its registered hook string stay in sync:
- src/: RuntimeClassDecl.foreign_path→runtime_path, splitForeignPath→splitRuntimePath,
  foreignPathToJavaName→runtimePathToJavaName, jni_main_foreign_paths→
  jni_main_runtime_paths, hookJniMainForeignPathAt→hookJniMainRuntimePathAt, and the
  hook string 'BuildOptions.jni_main_foreign_path_at'→'…runtime_path_at'.
- library/: build.sx accessor jni_main_foreign_path_at→jni_main_runtime_path_at +
  bundle.sx call sites + the  local var → runtime_path + a comment.
- specs.md: the accessor name + <foreign_path_with_dots> doc refs.
- Regenerated 37 .ir snapshots: every program importing build declares the renamed
  @BuildOptions.jni_main_runtime_path_at hook stub — symbol-name change only (verified
  the .ir diff is ONLY this rename; reverted orthogonal empty-file normalization).
Suite green (646 corpus / 444 unit, 0 failed).
2026-06-15 09:20:30 +03:00
agra
a15a868391 refactor(ffi-linkage): Phase 9.2b-fix — use is_extern (not new is_reference) for the runtime-class ref flag
Per user feedback: don't introduce new terminology. The RuntimeClassDecl
reference-vs-define flag (set by the postfix 'extern' modifier, == old prefix
'#foreign #objc_class') is named is_extern, matching the keyword that drives it
and the existing is_extern on VarDecl/IR. Renamed is_reference→is_extern,
is_reference_eff→is_extern_eff; updated the field comment. Snapshot-neutral; green.
2026-06-15 09:06:19 +03:00
agra
d27be42a93 refactor(ffi-linkage): Phase 9.2c — rename extern-ref validators → Extern (linkage)
checkForeignRefs→checkExternRefs, validateForeignRefs→validateExternRefs,
collectForeignRefTargets→collectExternRefTargets — these police 'extern LIB' library
references (linkage axis), so Extern not Runtime. Snapshot-neutral; suite green.
2026-06-15 09:03:35 +03:00
agra
5c8af6eb73 refactor(ffi-linkage): Phase 9.2b — rename runtime-class fns + state → runtime_* / is_reference
The runtime-class object-model identifiers (Decision 5): parse/lower/find/resolve/
register/stamp fns Foreign→Runtime (parseRuntimeClassDecl, lowerRuntimeMethodCall,
findRuntimeMethodInChain, resolveRuntimeMethodReturnType, registerRuntimeClassDecl,
runtimeClassStructType, runtimeKindForOffset, …); state foreign_class_map→
runtime_class_map, current_foreign_class/_method→current_runtime_*, the
foreign_class_decl union variant→runtime_class_decl, foreign_method/static/instance/
class→runtime_*; and the reference-vs-define flag is_foreign→is_reference (+
is_foreign_eff→is_reference_eff) now that it only lives on RuntimeClassDecl.
Snapshot-neutral; suite green (646/444).

Remaining 9.2: the foreign_path family (coupled .sx hooks: jni_main_foreign_path_at
spans build.sx/bundle.sx/compiler_hooks.zig/specs.md) + the extern-ref validators
(checkForeignRefs etc. → Extern, linkage not runtime) + bare 'foreign' comments.
2026-06-15 09:01:04 +03:00
agra
3354446412 refactor(ffi-linkage): Phase 9.2a — rename runtime-class TYPE names → Runtime* (Decision 5)
Mechanical, collision-free PascalCase renames (object-model axis, not linkage):
ForeignClassDecl→RuntimeClassDecl, ForeignMethodDecl→RuntimeMethodDecl,
ForeignClassMember→RuntimeClassMember, ForeignFieldDecl→RuntimeFieldDecl,
ForeignRuntime→RuntimeKind, ForeignClassPrefix→RuntimeClassPrefix. Snapshot-neutral;
suite green (646/444). Remaining 9.2: snake_case state (foreign_class_map,
current_foreign_class, foreign_path [coupled to .sx hooks], the foreign_class_decl
union variant) + the parse/lower/resolve fn names + ForeignClassDecl.is_foreign flag.
2026-06-15 08:57:53 +03:00
agra
7ffdc7d2a2 refactor(ffi-linkage): Phase 9.1d — eliminate the foreign_expr AST node
The last linkage-family 'foreign' carrier. Migrated c_import.zig auto-synthesis
(#import c {#include}) to build the extern shape (empty-block body + extern_export
= .extern_) instead of a foreign_expr body — the Phase 5.0 fn-body flip applied to
auto-synth. With nothing left building it, deleted the foreign_expr union variant +
ForeignExpr struct (ast.zig) and every reader: the dead-arm switch cases (sema,
resolver, generic, call, semantic_diagnostics, lsp), the coalescing reads in
decl.zig (is_foreign local, cc/rename/dedup/variadic/visibility gates) + pack.zig,
and checkForeignRefs (now reads extern_lib only). 9.1 LINKAGE PURGE COMPLETE — all
that remains in src/ is the runtime-class family (9.2) + comments. Snapshot-neutral
(the #import c examples 1215/1216/1217 + sqlite 1624 exercise the synth path); suite
green (646 corpus / 444 unit, 0 failed).
2026-06-15 08:54:56 +03:00
agra
cd147942e4 refactor(ffi-linkage): Phase 9.1c — delete dead VarDecl legacy foreign fields
VarDecl carried BOTH the legacy is_foreign/foreign_lib/foreign_name AND the new
is_extern/extern_lib/extern_name (parallel forms coalesced during the migration).
The global #foreign parse path now rejects, so the legacy trio is write-dead and
read in only 3 coalescing sites (decl.zig). Simplified those readers
(vd.extern_name orelse vd.name; vd.is_extern) and deleted the dead fields. Build
confirms no other setter/reader. Snapshot-neutral; suite green (646/444).

Remaining linkage (9.1): foreign_expr (25, still built by c_import.zig auto-synth)
+ ForeignClassDecl.is_foreign (runtime-class, → 9.2). Runtime-class family (9.2,
Decision 5) is the big remaining src/ rename.
2026-06-15 08:46:59 +03:00
agra
b78e7ddeb1 refactor(ffi-linkage): Phase 9.1b — rename 'foreign symbol' diagnostic + panic to 'extern'
The dup-C-symbol diagnostic (decl.zig) and the resolveFuncByName panic (call.zig)
now say 'extern symbol' instead of 'foreign symbol' — the keyword-neutral internal
wording catches up to the extern-only surface. Intentional snapshot regen of 1172
(the only assertion of this message). Suite green (646/444).
2026-06-15 08:42:59 +03:00
agra
b838f6383f refactor(ffi-linkage): Phase 9.1a — rename collision-free linkage identifiers
Mechanical src/ rename of the linkage-family identifiers whose extern_* target is
collision-free: callForeign→callExtern, marshalForeignArg→marshalExternArg,
dedupeForeignSymbol→dedupeExternSymbol, foreign_name_map→extern_name_map,
is_foreign_c_api→is_extern_c_api. Snapshot-neutral (internal only); suite green
(646 corpus / 444 unit, 0 failed).

Deferred (need per-site analysis — target name already exists): is_foreign↔is_extern
(38 existing), foreign_lib/foreign_name↔extern_lib/extern_name (15/16 existing),
foreign_expr (still built by c_import.zig auto-synthesis). Runtime-class family
(ForeignClassDecl etc. → Runtime*, Decision 5) is Phase 9.2.
2026-06-15 08:39:59 +03:00
agra
3811311e12 feat(ffi-linkage)!: Phase 8.1 — parser hard-rejects #foreign (cutover)
The prefix #foreign linkage directive is removed. All four parse sites
(const-with-type, data global, fn body, runtime-class prefix) now reject it with
a migration message ('#foreign has been removed; use the postfix extern (import) /
export (define) linkage keyword instead'); added a span-aware failAt for the
runtime-class case (the lookahead consumes the token before the reject decision).
Greens the Phase 8.0 xfail 1176.

- Deleted obsolete tests: 1174 (#foreign+postfix conflict — unreachable now that
  #foreign alone is rejected) and 1620 (#foreign nosuchunit lib-ref — superseded by
  the extern twin 1231). Their assertions tested #foreign-specific behavior.
- Removed the GATE A→B unit test + lowerSrcToIr helper (lower.test.zig): it locked
  #foreign ≡ extern through the migration; with #foreign gone there is nothing to
  compare. Converted the in-source 'parse void function with foreign body' parser
  test to the surviving postfix 'extern' spelling (identical resulting AST).
- specs.md + readme.md drop #foreign; document extern/export as the sole C-linkage
  surface.

extern_export in parseFnDecl is now const (the fn-body arm that mutated it is gone).
Suite green (646 corpus / 444 unit, 0 failed). NOTE: comment-only #foreign in
examples + issues/*.md prose + internal foreign_* identifiers remain for Phase 9
(now unblocked: Decision 6 = purge everything).
2026-06-15 08:06:05 +03:00
agra
93e7b6f727 test(ffi-linkage): Phase 5.1 — annotate A→B gate post-flip + add fn-rename case
Phase 5.0 flipped the fn-decl and data-global #foreign parser paths onto the
same extern-named AST that postfix extern produces, so the A→B gate's fn/global
cases are now STRUCTURALLY identical (guaranteed by construction, not empirically
equal). Annotate the gate header to record this and keep it as a regression
tripwire against a future reader re-diverging the two spellings or a revert of
the flip. Add a fn-rename case (extern_name axis: c_abs -> "abs") to broaden
coverage beyond bare import. Test-only; suite green (647 corpus / 444 unit, 0
failed). PHASE 5.1 COMPLETE → PART B Phase 5 done; next Phase 6 (migrate stdlib).
2026-06-15 04:21:36 +03:00
agra
6b94bb6bba refactor(ffi-linkage): Phase 5.0 — flip fn-decl #foreign body marker onto the extern AST
The fn-body `#foreign [LIB] ["csym"]` marker now builds the SAME shape postfix
`extern` produces — extern_export = .extern_ + extern_lib/extern_name + an
empty-block body — instead of a `foreign_expr` body. With all four prereqs
landed (visibility, variadic, plain-free classification, lib-ref validation),
every downstream reader coalesces is_foreign with extern_export, so the IR and
runtime behavior are byte-identical (full corpus + the A->B gate stay green).

The surface keyword is no longer on the AST, so a `#foreign`-spelled decl now
yields `extern`-worded diagnostics — the single accepted churn (Decision 7):
example 1620's lib-ref error flips '#foreign library' -> 'extern library'.
Parser-surface diagnostics (conflict/expected-token) fire on the literal keyword
and are unaffected. c_import auto-synthesis still emits foreign_expr bodies (not
this step), so both shapes still coexist. Parser unit test updated to assert the
extern shape.

647 corpus / 444 unit, 0 failed. The const-with-type (dead) + runtime-class
(already coalesced) paths need no flip — Phase 5.0 parser routing is complete.
2026-06-15 04:03:51 +03:00
agra
ad6aed3d7a fix(ffi-linkage): Phase 5.0 prereq — validate extern LIB refs like #foreign
checkForeignRefs now reads a library reference from either spelling — the
legacy #foreign body (foreign_expr.library_ref) or the new extern keyword
(extern_lib) — and validates both against the declared #library / #import c
units. The diagnostic names the surface keyword the user wrote (#foreign vs
extern), so example 1620 (#foreign) is byte-unchanged and example 1231
(extern) gets the parallel 'extern library ... not declared'. Greens 1231.

647 corpus / 444 unit, 0 failed.
2026-06-15 03:53:03 +03:00
agra
3c94c14b5e fix(ffi-linkage): Phase 5.0 prereq — exclude extern imports from plain-free-fn classification
isPlainFreeFn / isPlainFreeFnDecl excluded a #foreign body but classified
an empty-block extern fn as a plain free function, so existing extern fns
were wrongly counted in the bare-call ambiguity verdict (and eligible for
the out-of-line-slot / shadow-author pass). Both predicates now also
exclude extern_export == .extern_ (an external C symbol with no
sx-lowerable body, name-keyed first-wins dispatch like #foreign); export
keeps a real body and stays plain-free. Greens example 1230 — same-name
extern authors compile like their #foreign twins (0729).

646 corpus / 444 unit, 0 failed.
2026-06-15 03:46:34 +03:00
agra
0fdc82154f fix(ffi-linkage): Phase 5.0 prereq — map extern C-variadic tail to ... like #foreign
Two gates were keyed on the `#foreign` (foreign_expr) body shape only:
- declareFunction: the is_variadic drop (decl.zig) — a variadic extern
  kept its trailing slice param in the IR signature.
- packVariadicCallArgs: the call-site early-out (pack.zig) — extras were
  slice-packed instead of passed through the C `...` slot.

Both now also fire for `extern_export == .extern_`, so a variadic
`extern` drops the trailing `..args: []T`, sets is_variadic, and passes
extras through the C ABI with default argument promotion — byte-identical
to its `#foreign` twin. Greens example 1229.

645 corpus / 444 unit, 0 failed.
2026-06-14 21:05:40 +03:00