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).
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).
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).
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).
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).
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).
`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).
`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).
CLAUDE.md §Testing + §Test-layout now describe the optional `<name>.build` JSON
config (aot + target keys, ir-only arch-gating, unknown-key-is-error) and list
it among the `expected/` files, replacing the stale standalone `.aot` marker
prose. Closes Phase 0 (corpus target-gating); next is Phase A (kw_asm keyword).
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).
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).
Add .gitattributes routing *.vsix through Git LFS and convert the committed
extension vsix to an LFS pointer. Keeps the reproducible build artifact in the
repo without growing normal history on each rebuild. Future-only — existing
vsix blobs remain in history (a `git lfs migrate` rewrite would be needed to
purge those, deferred since origin/master is shared).
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.
Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern,
foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl,
findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→
dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the
renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed
issues/0043-…-foreign-class-…→…-runtime-class-….
PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/
issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant
SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party).
Suite green (644 corpus / 443 unit, 0 failed).
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).
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.
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).
Reword to the extern/runtime-class vocabulary: 'Foreign Function Interface' heading →
'C Interop'; 'foreign class'→'runtime class'; '#import c foreign decls'→'extern decls';
'foreign function calls'→'extern function calls'; the host_ffi #foreign("c") ref →
extern; the bundling 'foreign calls'→'extern calls'. Docs-only; zero 'foreign' left in
specs.md/readme.md/CLAUDE.md.
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).
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.
checkForeignRefs→checkExternRefs, validateForeignRefs→validateExternRefs,
collectForeignRefTargets→collectExternRefTargets — these police 'extern LIB' library
references (linkage axis), so Extern not Runtime. Snapshot-neutral; suite green.
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.
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).
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.
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).
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.
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).
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.
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).
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).
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.
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).
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).
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.
Pure source rename across objc/objc_block/raylib/sdl3/wasm (~51 sites): fn-decl
markers (bare / 'objc' LIB ref) → 'extern …', and objc.sx's 2 import runtime
classes '#foreign #objc_class("X") {' → '#objc_class("X") extern {'. No bare
defined classes. Behavior-preserving. objc + objc_block validated directly by the
50 marked 13xx corpus examples (incl. import classes 1300/1301 + defined classes
1339/1349); raylib/ffi-sdl3/wasm (no marked importers on host) verified by
byte-identical 'sx ir' probes pre/post. Empty snapshot diff; suite green (647
corpus / 444 unit, 0 failed).