Add a pure-sx streaming SHA-256 (FIPS 180-4) stdlib module, importable
as `#import "modules/std/hash.sx";`. All 32-bit word arithmetic is done
in s64 and masked back with `& MASK32`, so digests are deterministic and
platform-independent — no shelling out, no native crypto.
API:
- init() -> Sha256 (by-value *self pattern)
- update(*Sha256, string) (multi-block + partial-block buffering)
- final(*Sha256) -> string (32-byte digest as lowercase hex)
- sha256_hex(string) -> string (one-shot)
- sha256_file([:0]u8) -> ?string (digest of a file via fs.read_file)
Verified against FIPS/NIST known-answer vectors and `shasum -a 256`:
"" , "abc", the 56- and 112-byte multi-block vectors, 1000×'a', and the
64/65-byte block boundaries; chunked update() matches the one-shot call.
examples/0710-modules-sha256.sx pins the KAT vectors + the streaming
invariant; gate green (zig build, zig build test, run_examples 370/0/0/0).
The reserved-type-name binding diagnostic fired correctly but underlined the
enclosing statement / if / while / for / match / protocol / #objc_class block
because every binding-name check reused the parent `node.span`.
Thread each binding name's own span through the AST and parser, and pass it to
`checkBindingNames`:
- ast: add name spans to VarDecl, DestructureDecl, If/WhileExpr, ForExpr
(capture + index), MatchArm, Catch/OnFailStmt, Protocol/ForeignMethodDecl.
- parser: populate each span at the binding site from the name token's loc;
destructure reuses each target identifier's own span.
- semantic_diagnostics: every checkBindingName call now passes the binding's
own span — no site falls back to node.span. fn/lambda params already used
Param.name_span.
Carets now land on the offending identifier itself. New regression
examples/1125 asserts the protocol default-body and sx-defined #objc_class
method param spans; 0125/1119-1124 expected updated to the precise carets.
The reserved/builtin-type-name binding diagnostic was a hand-walked subset
of binding-bearing AST nodes with a silent `else => {}`, so each review
found another syntactic binding form that bypassed it and hit the original
LLVM verifier abort: destructure names (`s2, x := …`), `impl` method
params/locals, and `if` / `while` / `for` / match-arm / `catch` / `onfail`
captures.
Rewrite `checkBindingNames` (src/ir/semantic_diagnostics.zig) as an
EXHAUSTIVE `switch` over every `Node.Data` tag with NO `else` arm — a future
binding-bearing node type now fails to compile until it is handled here, so
coverage is enforced by the compiler instead of a hand-maintained list. The
check stays in the pre-lowering semantic pass rather than moving to the
`Scope.put` scope-registration choke point: lowering is lazy, so an
uncalled function's bindings never reach `Scope.put`, yet they must still be
rejected at their declaration (e.g. the never-called `takes_u8` in 1119).
No lowering special-case; `lower.zig` unchanged.
Regression tests (fail-before: LLVM abort or silent accept → pass-after:
clean diagnostic, exit 1):
- 1121 control-flow: destructure, if/while bindings, for capture+index,
match-arm capture
- 1122 impl-block method: reserved param AND reserved local
- 1123 catch + onfail tag bindings
- 1124 destructure name reserved in an imported module
Existing 0125 / 1119 / 0135 / 1120 tests kept; full suite 368 passed.
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.
Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.
Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.
Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.
Resolves issues 0076 (coverage extension) and 0077.
A value binding (local/global `var` or a parameter) spelled as a
reserved/builtin type name parses as a `.type_expr` rather than an
`.identifier` (parser.zig, via `Type.fromName`), so the address-of
family in lower.zig never saw a scoped local and mis-lowered it —
loading the aggregate and passing it by value to a `ptr` parameter
(LLVM verifier abort, or a silent `*self`-mutation-losing copy).
Add a declaration-site diagnostic in semantic_diagnostics.zig
(`UnknownTypeChecker.checkBindingName`): reject any parameter name or
`var` binding name (`:=` / typed-local / global forms) whose spelling
collides with a reserved type name. `isReservedTypeName` defers to the
parser's own classifier (`types.Type.fromName`) so the rejected set
never drifts from the set that would parse as a type — the named
builtins (bool/string/void/f32/f64/usize/isize/Any) and `[su]N` over
sx's 1-64 range. Bare value names (`s`, `self`, `index`) are untouched.
No lowering special-case; the `.identifier`-only address-of paths are
correct once type-shaped names can never be bound. The rejected
attempt-1 `bareVarName` approach was never landed.
Tests:
- 0125-types-type-named-var-rejected: `:=` form (s2) rejected
(repurposed from the old test that asserted the now-illegal behavior).
- 1119-diagnostics-reserved-type-name-as-identifier: parameter (u8),
typed-local (s64, bool), `:=` (string) forms rejected.
- 0135-types-self-streaming-nonreserved: positive — `*self` streaming
with non-reserved names accumulates correctly via both call styles.
- 0904-optionals: renamed incidental locals s1/s2 -> filled/empty.
The trace docs predated the current formatter. Corrected against the real
output (library/modules/trace.sx to_string + examples/expected/1025-errors-
trace-format.stderr):
- error-handling.md: replace the obsolete trace example ("error trace:" /
"raised error.X" / "at func (file:line)") with the real format —
"error return trace (most recent call last):" + per-frame "func at
file:line:col" + source line + caret.
- debugger.md: drop the stale "(planned)" marker on the trace formatter
(it is implemented); the tag-name table note now cites the failable-main
reporter's "unhandled error reached main: error.X" line, not a
nonexistent "raised error.X" trace line.
The `type_name` / `type_eq` reflection builtins resolved their Type arg's IR
type via `getRefIRType(...) orelse TypeId.s64`, then gated `== .any`. A failed
must-succeed lookup silently became `.s64` (`!= .any`), classifying a boxed
`Any` arg as bare i64 and reading the wrong value with no diagnostic.
Add the sibling classifier `LLVMEmitter.reflectArgRepr`, which routes the
lookup through `argIRTypeOrFail` (the issue-0074 `.unresolved` resolver) and
returns `{ boxed, bare, unresolved }`. The three emit sites in ops.zig
(`type_name` + `type_eq` x2) now switch on it: `.boxed` extracts the Any value
field, `.bare` uses the value directly, `.unresolved` hits a hard `@panic`
tripwire — never silently treated as bare. Real args always resolve, so the
happy path is byte-identical (suite stays 361/0, zero snapshot churn).
Secondary `lower.zig` `null_literal`/`undef_literal => target_type orelse .void`
confirmed intentional (typeless-literal default deliberately handled by
emitConstNull/emitConstUndef as null-ptr / undef-i64) — left with an invariant
comment, not the `.unresolved` tripwire.
Regression test in emit_llvm.test.zig asserts the loud path: fail-before with
`orelse .s64` yields `.bare`; pass-after yields `.unresolved`.
Discovered during the 0074 fix + a codebase-wide silent-type-fallback sweep.
getRefIRType(...) orelse TypeId.s64 at ops.zig:1023/1049/1055 (type_name/type_eq).
Blocker; to be resolved before the arch-refactor stream closes.
Four FFI call-arg lowering sites resolved an argument's IR type via
`getRefIRType(arg_ref) orelse .void` — a silent fallback to the load-bearing
real type `.void`. A failed lookup there is a codegen invariant violation, but
`.void` is treated by downstream `toLLVMType` → `abiCoerceParamType` →
`coerceArg` as a legitimate void-typed foreign argument, corrupting the call
ABI with no diagnostic.
Add one shared resolver `LLVMEmitter.argIRTypeOrFail` that returns the
dedicated `.unresolved` sentinel on a failed lookup — never `.void`/`.s64` — so
the failure cannot masquerade as a real type and trips `toLLVMType`'s existing
hard `@panic` tripwire at the call site. Route all four sites through it:
- src/ir/emit_llvm.zig JNI constructor (NewObject) arg loop
- src/backend/llvm/ops.zig objc_msgSend arg loop
- src/backend/llvm/ops.zig JNI non-virtual call arg loop
- src/backend/llvm/ops.zig JNI Call<Type>Method arg loop
Happy path is byte-identical (every real arg already has a resolved type); FFI
examples stay green with zero snapshot churn.
Regression test (fail-before/pass-after) in src/ir/emit_llvm.test.zig asserts an
unresolvable FFI arg ref now yields `.unresolved`, not the old silent `.void`.
The interp's .trace_frame op only yields the packed value; the separate
sx_trace_push call op is executed by the interp as a foreign call via
host_ffi/dlsym, so the prior 'no sx_trace_push call runs' / 'never calls
sx_trace_push' phrasing was wrong. The packed low word is the op's
span.start (a source byte offset), not an IR instruction offset; renamed
every ir_offset/offset reference to span.start.
The compiled backend builds each trace Frame global as an LLVM named-struct
constant over the cached getFrameStructType() layout (file, line, col, func,
line_text) via LLVMConstNamedStruct -- a type-safe LLVM struct, not the sx
Frame TypeId / normal struct-emission path. Also correct the file field to
the source basename (full paths live in DWARF).
The .trace_frame op is niladic: it carries no operand and no GlobalId.
The compiled backend yields the interned Frame global's address as the
op's value (reflection.emitTraceFrame); the interpreter yields a packed
(func_id, ir_offset) as the op's value and never calls sx_trace_push
(recovered later by .trace_resolve). The sx_trace_push call is a separate
call op emitted by lower.zig at each push site, consuming the op's value.
Reword every passage that stated the old/wrong model: the niladic
invariant is about the op (not the push site emitting only one
instruction); reflection yields the op's value rather than lowering a
push; the interp returns the packed value rather than calling the foreign
sx_trace_push via host_ffi dlsym.
Name the niladic op `.trace_frame` (no `.trace_frame_push` op exists) in
the trace-path roadmap, matching the rest of the doc and src/ir/inst.zig.
Describe the `.trace_frame` arm as building/interning the Frame global and
yielding its address as the op's value; the separate sx_trace_push call is
emitted by the lowerer via normal call lowering, not by the arm itself.
Refresh the debugging architecture reference for the A7.2 relocation:
DWARF emission lives in src/backend/llvm/debug.zig (DebugInfo) and the
interned Frame / tag-name tables in src/backend/llvm/reflection.zig
(Reflection); emit_llvm.zig is the orchestrator that owns LLVMEmitter and
dispatches to them. Behavior is unchanged; only the file-and-function map,
the 'what's emitted' home, and the debugEnabled() owner are corrected.
Remove the legacy parallel type model's compiler-like surface. The
compiler pipeline resolves/lowers/lays out against canonical
src/ir/types.zig (TypeId/TypeTable); src/types.zig.Type is now strictly
editor-indexing + parse-time name metadata.
- src/types.zig: delete the type-resolution surface (widen, bitWidth,
isImplicitlyConvertibleTo) and every helper left dead once it was gone
(eql, isInt/isFloat/isSigned/isUnsigned, isTuple/isVector, and the
already-unused classification predicates isEnum/isUnion/isString/
isStringLike/isAny/optionalChild/sliceElementType/manyPointerElementType/
vectorElementType/isFunctionType/isClosureType/isCallable). Keep the Type
union plus the display/name-classification helpers sema/lsp/parser use
(fromName, fromTypeExpr, toName, displayName, isStruct/isOptional/isSlice/
isPointer/isManyPointer/isArray, pointerPointeeType). Seal the file with a
doc comment.
- src/sema.zig: inferExprType no longer calls Type.widen for arithmetic;
it approximates the display type as the left operand's (no second
resolver in the editor index).
- src/ir/type_bridge.zig: delete the dead bridgeType (legacy Type -> TypeId)
function + its sole sx_types import; resolveAstType and the AST->TypeId
path are untouched.
- src/ir/ir.zig: drop the bridgeType re-export.
- src/ir/type_bridge.test.zig: drop the two bridgeType tests (function gone).
Gate: zig build, zig build test (exit 0), tests/run_examples.sh 361/0,
zero examples/expected churn.
Remove the last compiler dependency on sema as semantic truth and stop
publishing as-you-type sema diagnostics from the LSP.
- core.zig: drop dead `Compilation.analyze()`, the `sema_result` field,
and the sema->diagnostics merge; drop the now-orphaned sema import.
The CLI pipeline (parse -> resolveImports -> generateCode) never called
analyze(), so this removes only dead code.
- lsp/server.zig: rename `analyzeAndPublish` -> `refreshEditorIndex` and
delete its sema-diagnostic publish (and the now-unused `semaToLspDiags`).
The editor index (doc.sema) is still refreshed for nav/refs/completion/
tokens. On-save/on-open diagnostics still come solely from the canonical
compiler pipeline in `runProjectCheck` (unchanged).
- Document sema as an editor-indexing API (doc.sema field comment).
Intended behavior change: as-you-type sema diagnostics no longer publish;
on-save canonical diagnostics are the sole source. CLI compile output and
the 361-example suite are unchanged (361/0, zero snapshot churn).
Move the final inline emitInst handler groups (terminators, box/unbox-Any,
reflection, switch-branch, closure-creation, vector, block-param, misc) into
the Ops facade in src/backend/llvm/ops.zig. emitInst is now pure dispatch:
every arm delegates to self.ops().*, leaving only setInstDebugLocation plus
one-line delegations.
Widen the shared infra the moved bodies reach (emitFailableMainRet, getBlock,
anyTag, isSignedTypeEx, coerceToI64/coerceToI64Signed/coerceFromI64,
emitFieldValueGet) to pub on LLVMEmitter; helper and ref-tracking sections
stay put. Pure relocation: emitted LLVM IR byte-identical, zero snapshot churn.
Relocate the struct, enum, union, array/slice, tuple, and optional
opcode handler bodies out of emitInst into the existing Ops facade.
Each moved arm now delegates via self.ops().emit<Op>(...); shared infra
stays on LLVMEmitter, with resolveAggregate/resolveGepStructType widened
to pub as the GEP handlers require. Pure relocation, behavior-preserving:
zero snapshot churn (361/0).
Relocate the Calls (objc_msg_send / jni_msg_send / call / call_indirect)
and Call-extensions (call_builtin / compiler_call / call_closure) emitInst
handler groups out of emit_llvm.zig into the existing Ops facade. Each
emitInst arm now delegates via self.ops().emit<Op>(...). Behavior-preserving
pure relocation; emitted LLVM IR is byte-identical (361/0 examples, no
snapshot churn).
Shared call infra stays on LLVMEmitter, widened pub only as the moved
bodies require: extractSlicePtr, loadJniFn, getObjcMsgSendValue, the math
F32/F64 declarators + types, getOrDeclareWrite/getWriteType, ffiCtors,
materializeByvalArg, emitCStringGlobal, emitJniConstructor, and the Jni
slot-offset constants. emitJniConstructor remains in emit_llvm.zig (A7.3
decision); the moved jni arm calls it via self.e.emitJniConstructor(...).
Relocate the `// ── Memory ──`, `// ── Globals ──`, `// ── Conversions ──`,
and `// ── Pointer ops ──` opcode handler bodies out of `emitInst` in
src/ir/emit_llvm.zig into the existing `Ops` facade in
src/backend/llvm/ops.zig. Each `emitInst` arm now delegates via
`self.ops().emit<Op>(...)`. Widen `emitConversion`, `coerceArg`, and
`getRefIRType` to `pub` (the only helpers the moved bodies call).
Pure relocation: zero snapshot churn.
Move the Constants/Arithmetic/Bitwise/Comparisons/Logical opcode handler
bodies out of emitInst into a new Ops facade in src/backend/llvm/ops.zig.
emitInst's scalar arms now delegate via self.ops().*; the shared infra they
call (mapRef/resolveRef/matchBinOpTypes/emitCmp/emitCmpOrdered/emitStrCmp/
emitStringConstant/reflection + isFloatOrVecFloat/isSignedType) stays on
LLVMEmitter, widened to pub as needed. Pure relocation: zero snapshot churn.
Move getOrCreateJniSlots (the cls/methodid slot-cache builder) out of
emit_llvm.zig into the FfiCtors backend *LLVMEmitter facade. Behavior-preserving
— self.* -> self.e.* only.
- FfiCtors gains getOrCreateJniSlots (pub). The jni_slots cache + mangleJniKey
stay on LLVMEmitter; mangleJniKey is widened to pub (the facade calls it back,
like lazyDeclareCRuntime/emitPrivateCString), and JniSlotPair is widened to pub
(the facade returns it; the call site consumes it). 1 call site routed via
ffiCtors().
- emitJniConstructor intentionally NOT moved in this slice: it is emission-heavy
(resolveRef/mapRef/coerceArg/getRefIRType/extractSlicePtr/loadJniFn/
emitCStringGlobal — 100+ internal callers for the first two), so relocating it
would pub-expose the emitter's core value-emission machinery. Consistent with
A7.2 keeping emitFieldValueGet in emit_llvm.zig. Pending an explicit decision.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(JNI anchors 1402/1408/1418/1425 green, no churn).
Backend-FFI .ir inventory + scaffolding for the Obj-C/JNI runtime-constructor
extraction (Phase A7.3). No code moved.
Inventory (recorded in ARCH-SAFETY.md): the existing FFI .ir set already pins the
core constructor emission — emitObjcSelectorInit (sel_registerName via 1309/1329/
1332), emitObjcClassInit (objc_getClass), emitObjcDefinedClassInit class
registration + ivars + method IMP table (objc_allocateClassPair / class_addIvar /
class_addMethod / objc_registerClassPair via 1309/1332), and getOrCreateJniSlots /
emitJniConstructor (GetMethodID via 1402/1418/1408).
Gaps closed (2 new .ir snapshots) for the ARCH-SAFETY-named metadata not covered
by 1309:
- 1319-ffi-objc-property-sx-defined: property getter/setter IMPs (_get/_set/
class_addMethod x8).
- 1314-ffi-objc-class-dealloc-roundtrip: alloc/dealloc IMPs.
Both path-free + idempotent (verified across two captures; trailing newline
trimmed). Suite count unchanged (snapshots on existing examples).
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn
beyond the 2 new .ir).
Move the DWARF debug-info emission out of emit_llvm.zig into a DebugInfo backend
*LLVMEmitter facade (field `e`). Behavior-preserving relocation — self.* ->
self.e.* only.
- src/backend/llvm/debug.zig (DebugInfo): debugEnabled + diFileFor (private) +
initDebugInfo / beginFunctionDebug / endFunctionDebug / setInstDebugLocation /
finalizeDebugInfo (pub). The mutable DI state (di_builder/di_cu/di_files/
di_scope/current_func_file) + the shared source map (import_sources/main_file)
stay on LLVMEmitter; the facade reads/writes them via self.e.*.
- Routed the 5 pass-order call sites in LLVMEmitter.emit (init/finalize/
begin/end/setInstDebugLocation) through a new debugInfo() accessor.
- setDebugContext stays on LLVMEmitter (shared-state setter; callers in main.zig/
core.zig/test). sourceForFile stays on LLVMEmitter and is widened to pub — it is
shared with reflection's trace-frame emission (emitTraceFrame), not debug-only.
- No DI logic / module-flag / DWARF-version / scope-line change.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn).
Move the LLVM type-mapping and C-ABI coercion helpers out of emit_llvm.zig into
the first src/backend/llvm/ modules. Behavior-preserving relocation — the only
rewrites are module plumbing and self.* -> self.e.* facade access.
- src/backend/llvm/types.zig (TypeLowering): toLLVMType + toLLVMTypeInfo.
- src/backend/llvm/abi.zig (AbiLowering): abiCoerceParamType / abiCoerceParamTypeEx
/ needsByval / materializeByvalArg.
- Both are backend *LLVMEmitter facades (field `e`) — the backend analogue of the
IR-side *Lowering facades, NOT a *Lowering facade. They reach the cached LLVM
handles, IR type table, module data layout, builder, and the memoizing
composite-type getters via self.e.*.
- LLVMEmitter stays the facade: toLLVMType (~97 callers) + abiCoerceParamType /
abiCoerceParamTypeEx / needsByval / materializeByvalArg kept as thin wrappers
delegating through new typeLowering()/abiLowering() accessors. Zero caller
churn. toLLVMTypeInfo deleted (sole caller moved).
- Widened getStringStructType / getAnyStructType / getClosureStructType to pub
(the moved toLLVMTypeInfo calls them back; their memoization stays on
LLVMEmitter). verifySizes stays in emit_llvm.zig (size-assertion pass, not type/
ABI lowering). No ABI/type logic, branch order, diagnostic text, or snapshot
changed. Circular import (emit_llvm <-> backend/llvm) resolves via the pointer
facade.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(1202 .ir + the 2 ABI unit tests unchanged, no churn).
Codex review of d6078c2 flagged a blank line at EOF in the new
examples/expected/1202-ffi-cc-c-large-aggregate.ir. Collapse the trailing
newlines to a single one so `git diff --check` is clean. Test-safe: the runner
reads both expected and actual IR through $(...) command substitution, which
strips trailing newlines, so the comparison is unaffected (1202 still ok).
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0.
Test-first scaffolding for LLVM backend modularization (Phase A7.1) before the
type/ABI helpers move into src/backend/llvm/{types,abi}.zig. Visibility-only
change to the targets — no behavior change. Closes the ARCH-SAFETY "no generic
ABI snapshot" gap.
- 2 new emit_llvm.test.zig tests:
- abiCoerceParamType across every C-ABI size bucket: <=8 -> i64, 9-16 ->
[2 x i64], >16 -> ptr, HFA (all-float/all-double, <=4 fields) -> unchanged,
string -> ptr, slice -> ptr, scalar -> unchanged. Built via a local
internStruct helper (field slice in the module arena -> no testing-allocator
leak); asserts against emitter.cached_* + LLVMArrayType2.
- needsByval: true only for >16-byte non-HFA struct; false for <=16 / HFA /
string / slice / non-struct.
- 1 new .ir snapshot: 1202-ffi-cc-c-large-aggregate (the canonical callconv(.c)
>16-byte byval example that directly documents abiCoerceParamType) — pins the
byval param path end-to-end (5 byval + entry reload + 2 sret from Arena.init).
Path-free + idempotent (verified across two captures). Suite count unchanged
(snapshot added to an existing example).
- Widened abiCoerceParamType + needsByval to pub (visibility only;
abiCoerceParamTypeEx/materializeByvalArg/verifySizes stay private — move with
callers in sub-step 2). No logic touched.
- Recorded the A7.1 coverage inventory + residual gaps (wasm32 usize->i32 branch,
fn-ptr large-aggregate 1203/1204) in ARCH-SAFETY.md.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn
beyond the new 1202 .ir).
Relocate the two pure JNI decision helpers out of lower.zig into
jni_descriptor.zig (already the JNI helper module), alongside the descriptor
derivation. Behavior-preserving move — no facade, since neither takes *Lowering.
- jniMangleNativeName(allocator, foreign_path, method_name) and
isJniReturnTypeSupported(table, ret_ty) moved verbatim as pub free fns; added a
types import + TypeId alias to jni_descriptor.zig.
- Rerouted lower.zig's 2 call sites (synthesizeJniMainStub; the JNI return-type
guard at lower.zig:6000) through jni_descriptor.* — lower.zig already imported
the module.
- Moved the 2 unit tests lower.test.zig -> jni_descriptor.test.zig (re-pointed to
desc.*; a standalone TypeTable.init replaces the Module setup). Dropped the
now-unused lower_mod alias.
- Stayed in lower.zig per PLAN A6.2 step 5/6: jniMapParamType (trivial resolveType
wrapper), synthesizeJniMainStub(s), lowerJniCall, lowerJniConstructor,
lowerSuperCall, getJniEnvTlFids. Java rendering stays in jni_java_emit.zig.
Phase A6 complete.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(9 JNI .ir snapshots + 26 14xx examples green, no churn).
Test-first scaffolding for the JNI FFI domain (Phase A6.2) before the pure
helpers move out of lower.zig. Visibility-only change — no behavior change.
- 2 new lower.test.zig tests for the pure JNI helpers lacking unit coverage:
- jniMangleNativeName: `/`->`_` separator, `_`->`_1` escape (path AND method),
`Java_` prefix, `_sx_1` infix (2 cases lock all rules).
- isJniReturnTypeSupported: void/bool/s32/s64/f32/f64 + pointer/many-pointer
-> true; other widths (s8/s16/u8/u32/u64) + by-value struct -> false.
- JNI descriptor derivation (writeType/deriveMethod) is already extracted into
jni_descriptor.zig (15 tests) — not part of A6.2.
- Widened jniMangleNativeName -> pub (file-scope free fn; isJniReturnTypeSupported
already pub). Reached from the test via ir_mod.lower.*. No logic touched.
- Recorded the A6.2 coverage inventory + residual emission-bound gaps
(synthesizeJniMainStub*/lowerJniCall/lowerJniConstructor/lowerSuperCall/
getJniEnvTlFids stay in lower.zig; jniMapParamType is a trivial resolveType
wrapper) in ARCH-SAFETY.md.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(no .ir churn; 9 JNI .ir snapshots green).
Move the pure Obj-C decision helpers out of lower.zig into src/ir/ffi_objc.zig
behind an ObjcLowering *Lowering facade (Principle 5, like the A4/A5 resolvers).
Behavior-preserving relocation — the only non-self.l rewrites are facade
plumbing.
Moved verbatim (self. -> self.l. for Lowering members):
- deriveObjcSelector (selector derivation)
- objcTypeEncodingFromSignature + appendObjcEncoding + bailObjcEncoding +
the ObjcEncodingStack type
- objcPropertyKind + the ObjcPropertyKind enum
- isObjcClassPointer
- objcDefinedStateStructType + objcStateAllocatorType
Emission-heavy code stays in lower.zig per PLAN A6.1 step 6: emitObjc* IMP
builders, lowerObjc*Call, registerObjc*, declareObjc*, the lookupObjc* property/
state lookups, and the Self-substitution resolvers.
- Call sites rerouted through a new objc() accessor: 15 in lower.zig, 1 in
expr_typer.zig, 39 in lower.test.zig (the A6.1 scaffolding tests now drive the
facade). No Lowering wrappers kept. Barrel-wired ffi_objc + ObjcLowering.
- No new visibility widening beyond sub-step 1's two pubs — the facade reads
self.l.{alloc,module,program_index,diagnostics} (fields) + the already-pub
resolveType. lower.zig -478 (->16615); ffi_objc.zig 428.
- Doc-only re-home: the property-IMP getter/setter comment was attached (a
pre-existing artifact) to the moving ObjcPropertyKind enum, two decls away from
its real subject emitObjcDefinedClassPropertyImps (which had no doc). Re-homed
it there so the move neither orphans a `///` block (Zig errors on a dangling doc
comment) nor misattributes it to ensureArcRuntimeDecls.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(48 13xx Obj-C examples + 4 Obj-C .ir snapshots green, no churn).
Codex review of 0012228 noted isObjcClassPointer's contract is
`fcd.runtime == .objc_class or fcd.runtime == .objc_protocol`, but the new tests
only exercised the class case. Test-only fix (no visibility/behavior change —
still exactly the two pub widenings from the parent commit):
- isObjcClassPointer: add a *NSCopying case where NSCopying is a registered
.objc_protocol foreign class -> true (alongside the .objc_class *NSString case).
- objcPropertyKind: add a *NSCoding protocol-pointer field -> strong default
assertion, since it uses the same class/protocol object-pointer predicate.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0.