587 Commits

Author SHA1 Message Date
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
17e3b91eb9 test(asm): x86_64 cross-arch siblings for place + symbol operands
Adds ir-only x86_64 examples mirroring the aarch64 feature examples, so
each emit path is locked on both arches:
- 1657 read-write `+`  → "incq ${0}", "=r,0" (tied input)
- 1658 indirect `=*m`   → "movq $$42, ${0}", "=*m"(ptr elementtype i64)
- 1659 symbol `"s"`     → "call ${2:P}", direct call to an exported sx fn

Each is x86-pinned (ir-only on this aarch64 host — the .ir is the
assertion; runs on x86_64-linux, main returns 0 on success / 1 if the
asm misbehaved). x86 templates validated by cross-emitting an object
(LLVM's integrated assembler accepts them; objdump confirms 1659 is a
direct `call` reloc to cb). Note: x86 direct calls need the `P` operand
modifier (`%[fn:P]`); aarch64 `bl %[fn]` needs none. Pure additive
locks, no compiler change. zig build test green (668 corpus, 446 unit).
2026-06-16 08:36:33 +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
1346a2d020 test(asm): round-trip example — asm calls back into an sx function
Adds examples/1655-platform-asm-callback-into-sx.sx: a global-asm
trampoline (_caller) that `bl _cb` back into an `export`ed sx function.
Demonstrates the sx → asm → sx round trip and that `export` (external
linkage + stable C symbol + C ABI) is what makes the callback symbol
resolvable — `callconv(.c)` alone leaves it internal and it DCE's away.
Runs under the JIT on aarch64-macos (→ 42); ir-only elsewhere. Locks
current behavior; no compiler change.
2026-06-16 07:55:05 +03:00
agra
ab7fc393b6 test(asm): pin loud failure of #run into a module-asm symbol
Adds examples/1654-platform-asm-global-comptime-call.sx — the comptime
guard. A module-asm symbol only exists after assemble+link; the comptime
interpreter resolves extern calls via host dlsym, where it's absent, so
`#run my_add(…)` fails with a clear diagnostic ("comptime extern call:
symbol not found via dlsym") rather than misfiring. Runtime calls work
(1648/1653). dlsym-miss precedes asm assembly, so arch-independent — no
.build. Locks current behavior; no compiler change.
2026-06-16 07:29:38 +03:00
agra
e954f044d8 test(asm): global asm runs under the JIT (sx run), not just AOT
Adds examples/1653-platform-asm-global-jit.sx — a module-scope asm { … }
block executed via `sx run` (no `aot`). sx run compiles the module to an
in-memory object (the integrated assembler assembles the `module asm`
into it), then ORC relocates and runs it, so a module-asm symbol IS
resolvable at JIT main execution — the long-assumed "AOT only" limit was
stale. Sibling of 1648 (same feature via AOT). Locks current behavior
(exit 42); no compiler change.
2026-06-16 07:24:09 +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
2a43713d7f test(asm): lock indirect-memory =*m rejection (Phase G prep)
Adds examples/1652-platform-asm-indirect-mem.sx exercising a `=*m -> @x`
indirect-memory place output. Currently rejected loudly at lowering
("not yet implemented"); this locks that behavior as a passing test.
The next commit implements indirect-memory outputs and flips this
example to run end-to-end (store-through-pointer → 42).
2026-06-16 07:05:05 +03:00
agra
cdd920b692 test(asm): x86_64 Linux syscall-write example (ir-only lock)
Adds examples/1651-platform-asm-x86-syscall-write.sx — the canonical
inline-asm use case: `write(2)` via a raw x86_64 `syscall` (SYS_write
in rax, fd/buf/count pinned to rdi/rsi/rdx, rcx+r11+memory clobbered,
byte count returned in rax). Exercises register-pinned inputs, a pinned
value output, a pointer input (*u8 -> rsi), and clobbers(.…) lowering
together.

x86-pinned via .build { "target": "x86_64-linux" }: ir-only on this
aarch64 host (the .ir snapshot locks the exact constraint string
`={rax},{rax},{rdi},{rsi},{rdx},~{rcx},~{r11},~{memory}` — the §II.11
silent-miscompile risk zone), runs natively on x86_64-linux printing
"ok\n" (hand-authored .stdout, asserted only in execute mode).

Pure additive test coverage — no compiler change (lock commit).
zig build test green (660 corpus, 446 unit).
2026-06-16 06:38:13 +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
335ac52374 test(asm): lock read-write + place-output rejection (Phase G prep)
Adds examples/1650-platform-asm-rw-place.sx exercising a `+r -> @x`
read-write place output. Currently rejected loudly at lowering
("not yet implemented"); this locks that behavior as a passing test.
The next commit implements read-write outputs and flips this example
to run end-to-end (increment-in-place → 42).
2026-06-15 23:00:48 +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
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
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
b52d424369 refactor(ffi-linkage): Phase 9.3 — rename *-foreign* example files → extern/runtime names
git-mv the 10 foreign-named example families to extern/runtime-class names + update
every #import/#include/#source ref, stale comment ref, and the 1172 stderr snapshot
(path + 'extern symbol' message). Renames: 0729…-foreign→…-extern, 1172-diagnostics-
foreign-symbol-conflict→…-extern-symbol-conflict, 1205/1207 ffi-foreign-global→
ffi-extern-global, 1216/1217 ffi-…-foreign-(in-method|result-chain)→…-extern-…,
1219-ffi-foreign→1219-ffi-extern, 1306 objc-foreign-class-chained→objc-runtime-class-
chained, 1318 objc-property-foreign→objc-property-extern-class. DEDUP: deleted
1218-ffi-foreign-cvariadic (identical to 1229-ffi-extern-cvariadic; updated 1229's
twin ref) + the orphaned 1620 dir. Also purged editors/vscode tmLanguage (#foreign
dropped from the directive highlighter) + 1220.h/issues-0030.sx comment refs. Suite
green (644 corpus / 443 unit, 0 failed).
2026-06-15 11:14:35 +03:00
agra
9719432e79 refactor(ffi-linkage): Phase 9.3 — purge remaining 'foreign' from library/docs/example comments
Capital-Foreign + stale-identifier comment refs: library (Foreign Java types→Runtime,
foreign-class→runtime-class, foreign_class_map→runtime_class_map); docs/debugger
(foreign call→extern call); docs/fork-c ledger (foreign_class_map, protocol/foreign→
runtime-class); docs/inline-asm-design Deviation-6 obsolete #foreign-vs-extern design
RESOLVED to the landed extern/export reality; example comments (parseForeignClassDecl→
parseRuntimeClassDecl, checkForeignRefs→checkExternRefs, Foreign decls→Extern). Docs/
comments only — no build impact.
2026-06-15 11:03:29 +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
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
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
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
8180faf839 test(ffi-linkage): Phase 8.0 — xfail #foreign-removed rejection diagnostic
Add examples/1176-diagnostics-foreign-removed.sx pinning the DESIRED Phase 8 cutover
behavior: a bare '#foreign' decl must be rejected with a clear migration message
('#foreign has been removed; use the postfix extern/export'). RED — '#foreign' still
parses (routes onto extern) so the decl compiles and exits 0 instead of erroring.
The very next commit (8.1, parser hard-reject) greens it.
2026-06-15 07:40:12 +03:00
agra
d132aab232 refactor(ffi-linkage): Phase 8 pre-cutover — migrate multi-file example companions
The Phase 7 batches globbed top-level examples/*.sx and missed #foreign decls in
SUBDIRECTORY companion files. Migrate the 4 incidental fn decls (behavior-preserving):
0729/a.sx + b.sx (same-name 'absval' libc authors), 1617/c.sx (pcap_lib_version),
1623/mod.sx (unit_in_mod_answer) — all '#foreign LIB "csym";' → 'extern LIB
"csym";'. Parents all marker'd → corpus-validated; empty snapshot diff. Comment
lines left for Phase 9.3. Suite green (647/444).
2026-06-15 07:36:57 +03:00
agra
720556b24e refactor(ffi-linkage): Phase 8 pre-cutover — migrate keyword-neutral diagnostic tests
Migrate the two #foreign-bearing diagnostic tests whose assertions survive the
cutover, with INTENTIONAL snapshot regens (reviewed):
- 1172 (foreign-symbol-conflict): decl '#foreign libc "getenv"' → 'extern libc
  "getenv"'. Still tests the dup-C-symbol conflict; the 'foreign symbol already
  bound' message is the keyword-neutral INTERNAL wording (renamed to 'extern symbol'
  in Phase 9.1), so it persists — only the echoed source line + caret moved.
- 1228 (non-transitive C-import visibility): its identity was the #foreign≡extern
  equivalence lock, now historical (structural via the A→B gate + unified AST). The
  identifier 'c_foreign_abs' itself contained 'foreign' (would fail the Phase 9.4
  gate), so converted c.sx/b.sx/main to two foreign-free extern symbols
  (c_abs_one/c_abs_two); still pins per-symbol non-transitive visibility.

Reverted the orthogonal 0→1-byte empty-stdout normalization on 1228/1231 (known
writeGolden idempotency quirk, not a behavior change). Suite green (647/444).
2026-06-15 07:34:23 +03:00
agra
2cce6a3a26 refactor(ffi-linkage): Phase 8 pre-cutover — migrate identity ffi-foreign-* test decls
Migrate the DECLS of the 7 identity-#foreign feature tests to extern/export
(1205-global/-helper, 1207, 1218-cvariadic, 1219, 1306, 1318): fn/global markers →
extern, the 2 objc import classes (1306/1318) → postfix '#objc_class("X") extern {'.
Behavior-preserving (A→B gate + existing extern twins guarantee identical output);
empty snapshot diff, corpus-validated. Comment-only #foreign in these files is left
for the Phase 9.3 doc/comment purge (comments aren't parsed → not cutover-critical).
Suite green (647 corpus / 444 unit, 0 failed).
2026-06-15 07:28:20 +03:00
agra
1a8991ab27 refactor(ffi-linkage): Phase 7.4 — migrate straggler examples #foreign→extern
16 fn/global examples across categories (0415/0602/0603/1024/1025/1605/1607-1609/
1611/1616/1619/1622/1628/1635/1636): bare '#foreign'→'extern'. All cls=0 (no class
forms). Marker'd ones (1605/1609/1611 + the rest) corpus-validated; the 3 unmarked
uikit importers (1607/1608/1616) verified byte-identical via 'sx ir' probes.
Empty snapshot diff; suite green (647 corpus / 444 unit, 0 failed).

LEFT comment-only/provenance #foreign (0716/0729 + issues/0030-extern-global +
extern-test files 1223-1231/1332/1348/1349/1426) and the keep-list (identity
ffi-foreign-* + foreign-asserting diagnostics 1172/1174/1219/1228/1620) for Phase 8.
2026-06-15 07:03:53 +03:00
agra
2888f6fc00 refactor(ffi-linkage): Phase 7.3 — migrate 14xx ffi-jni examples #foreign→extern
13 JNI examples migrated (1410-1419/1423/1424/1425): import runtime classes
'#foreign #jni_class("X") {' → '#jni_class("X") extern {'. 1417 (all-runtimes)
also exercises #jni_interface/#objc_class/#objc_protocol/#swift_class/#swift_struct/
#swift_protocol — all take the postfix modifier (verified by probe), migrated via a
generalized '#foreign #<directive>("X") {' → '… extern {' rewrite. No 14xx snapshot
asserts on 'foreign'; empty snapshot diff, corpus-validated.

KEPT comment-only #foreign in 1426 (jni-extern-class test, no decls). Suite green
(647 corpus / 444 unit, 0 failed).
2026-06-15 07:00:32 +03:00
agra
a68f7c2e64 refactor(ffi-linkage): Phase 7.2 — migrate 13xx ffi-objc examples #foreign→extern
18 obj-c examples migrated (1308/1311-1317/1319/1320/1321/1341-1347): import
runtime classes '#foreign #objc_class("X") {' → '#objc_class("X") extern {'
(prefix→postfix) + fn/comment '#foreign'→'extern'. No 13xx snapshot asserts on
'foreign' text → all behavior-preserving; empty snapshot diff, corpus-validated.

Per the keep-list policy: KEPT identity-#foreign tests 1306/1318 (filename
ffi-*-foreign*); LEFT comment-only #foreign in the extern/export test files
1332/1348/1349 (no decls). Bare defined #objc_class examples (no #foreign) untouched
— not a purge target. Suite green (647 corpus / 444 unit, 0 failed).
2026-06-15 06:53:33 +03:00
agra
731fb8de64 refactor(ffi-linkage): Phase 7.1 — migrate incidental 12xx ffi examples #foreign→extern
12 plain-C examples that use #foreign incidentally (as FFI plumbing, output
unchanged): 1200/1206/1209-1215/1220/1221/1222. Blanket keyword swap; all fn/global
markers (no class forms in 12xx). Empty snapshot diff; corpus validates directly
(all marker'd). Suite green (647 corpus / 444 unit, 0 failed).

KEPT on #foreign (deferred to Phase 8 cutover): identity-#foreign feature tests
(filename ffi-foreign-*: 1205/1207/1216/1218/1219), the equivalence test 1228, and
the diagnostics that assert on #foreign source/message (1172/1174/1620). Comment-only
provenance prose (1223/1229/1230/1231) left intact per Decision-6-recommended.
2026-06-15 06:49: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
38c32400f5 test(ffi-linkage): Phase 5.0 prereq — xfail extern undeclared-library ref unvalidated
An `extern LIB "csym"` ref must name a declared #library / #import c unit,
like its `#foreign LIB` twin (example 1620). Today checkForeignRefs reads
only foreign_expr.library_ref and skips the extern keyword's extern_lib, so
a bogus `extern nosuchunit "abs"` compiles silently (the symbol resolves
via the default image and runs). Expected pins the DESIRED compile-time
diagnostic; the next commit extends checkForeignRefs to green it. Fourth
extern/#foreign divergence and a prerequisite for the fn-decl migration.

647 corpus (1231 xfail), 444 unit.
2026-06-15 03:53:03 +03:00
agra
270652186e test(ffi-linkage): Phase 5.0 prereq — xfail extern same-name authors wrongly ambiguous
Two flat imports each declare `absval` via `extern libc "abs"` (the
`extern` twin of example 0729's `#foreign` form). Like its #foreign twin,
this must compile + run (prints 7), not error as an ambiguous bare-call
collision.

Today `isPlainFreeFn` / `isPlainFreeFnDecl` exclude a `#foreign` body but
classify an empty-block `extern` fn as a plain free function, so the two
extern authors ARE counted in the bare-call ambiguity verdict and the call
errors. A third extern/#foreign divergence (after visibility + variadic)
and a prerequisite for migrating the fn-decl `#foreign` path onto `extern`.

646 corpus (1230 xfail), 444 unit.
2026-06-15 03:43:18 +03:00
agra
9a2c78d6b9 test(ffi-linkage): Phase 5.0 prereq — xfail extern C-variadic tail loses its ...
A trailing `..args: []T` on an `extern` fn must map to the C `...` tail
like its `#foreign` twin (example 1218). Today the variadic handling in
both declareFunction (is_variadic drop) and packVariadicCallArgs
(call-site early-out) is gated on `#foreign` only, so a variadic
`extern` keeps the trailing slice param and slice-packs the extras —
garbage at the C ABI (probe: sum_ints(3,10,20,30) → 53316585, not 60).

Example 1229 pins the DESIRED correct output; the next commit extends
both gates to cover extern and greens it. Prerequisite for migrating the
fn-decl `#foreign` path onto `extern`.

645 corpus (1229 xfail), 444 unit.
2026-06-14 21:03:13 +03:00
agra
717c35d26d test(ffi-linkage): Phase 5.0 prereq — xfail lib-less extern/#foreign C-import visibility equivalence
Cross-module example (main → b → c) referencing c's lib-less C imports
transitively. The non-transitive C-import gate (lower/decl.zig
c_import_bare) must police the legacy `#foreign` form and the new
`extern` keyword IDENTICALLY — same 'C function not visible' diagnostic,
not the generic top-level-name wording. Today the extern twin escapes the
c_import_bare gate (body is an empty block, not foreign_expr) and is only
caught by the general isNameVisible gate, yielding the generic message.
Expected snapshot pins the DESIRED equivalent wording; the next commit
aligns the gate to green it. Prerequisite for migrating the fn-decl
`#foreign` path onto `extern`.

443/444 corpus (1228 xfail), 444 unit.
2026-06-14 20:47:48 +03:00
agra
0d39a1e168 test(ffi-linkage): conform 5 empty-stderr goldens to canonical 1-byte form
Eliminates the recurring -Dupdate-goldens churn: these 5 were 0-byte
outliers while 484 other empty goldens use the writeGolden-produced
1-byte "\n" form. The corpus runner trims trailing newlines on both
sides during verify, so both forms passed — but regen always rewrote
them to 1-byte. Conforming them makes -Dupdate-goldens idempotent.
2026-06-14 16:16:58 +03:00
agra
847a027fb1 test(ffi-linkage): xfail extern+export mutual-exclusion diagnostic (Phase 4) 2026-06-14 15:46:33 +03:00
agra
dd927c2e94 test(ffi-linkage): xfail #foreign+postfix conflict diagnostic (Phase 4) 2026-06-14 15:37:25 +03:00
agra
91d70bd864 test(ffi-linkage): lock postfix extern (jni) + export (objc defined) aggregates (Phase 3.1) 2026-06-14 15:13:23 +03:00
agra
0bde545f24 test(ffi-linkage): xfail postfix extern on #objc_class aggregate (Phase 3.0) 2026-06-14 15:00:37 +03:00
agra
66d9169e59 test(ffi-linkage): xfail export "csym" rename (Phase 2.2)
example/1227 exposes the sx fn `sx_triple` to C under the symbol `triple_c`
via `export "triple_c"`; the companion C calls `triple_c` by that name.
RED: the define path emits the fn under its sx name (`sx_triple`) and
ignores the parsed `extern_name`, so the C reference to `triple_c` is
undefined at AOT link. The next commit consumes the rename on the define
path (gap iii) and greens it.
2026-06-14 14:48:35 +03:00