Commit Graph

689 Commits

Author SHA1 Message Date
agra
5cb1691265 docs(debugger): correct trace-frame op name and sx_trace_push attribution (A9.2)
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.
2026-06-03 14:03:44 +03:00
agra
badf2af298 docs(debugger): point DWARF/Frame wiring at backend/llvm helpers (A9.2)
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.
2026-06-03 13:52:38 +03:00
agra
d319cef367 Merge branch 'flow/sx-plan-arch/A8.2' into arch-refactor 2026-06-03 13:30:25 +03:00
agra
e13dbfeb94 refactor(types): shrink src/types.zig to editor/parse metadata (A8.2)
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.
2026-06-03 13:21:00 +03:00
agra
d998e2809e Merge branch 'flow/sx-plan-arch/A8.1' into arch-refactor 2026-06-03 13:05:02 +03:00
agra
f52a24a0fb refactor(sema): seal sema.zig as editor indexing only (A8.1)
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).
2026-06-03 12:56:28 +03:00
agra
b7fce30f42 Merge branch 'flow/sx-plan-arch/A7.4e' into arch-refactor 2026-06-03 12:47:40 +03:00
agra
0e7bae563a refactor(backend): drain remaining emitInst handlers into ops.zig (A7.4 slice e)
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.
2026-06-03 12:41:39 +03:00
agra
3152abcb57 Merge branch 'flow/sx-plan-arch/A7.4d' into arch-refactor 2026-06-03 12:33:13 +03:00
agra
1be16511ec refactor(backend): move aggregate handlers into ops.zig (A7.4 slice d)
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).
2026-06-03 12:03:45 +03:00
agra
e58d2a1eed Merge branch 'flow/sx-plan-arch/A7.4c' into arch-refactor 2026-06-03 11:53:10 +03:00
agra
5388895b3e refactor(backend): move call + call-extension handlers into ops.zig (A7.4 slice c)
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(...).
2026-06-03 11:45:30 +03:00
agra
e1d86e0144 Merge branch 'flow/sx-plan-arch/A7.4b' into arch-refactor 2026-06-03 11:33:06 +03:00
agra
b4faefa607 refactor(backend): move memory/globals/conversion/pointer handlers into ops.zig (A7.4 slice b)
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.
2026-06-03 11:26:31 +03:00
agra
fb19cf9e83 Merge branch 'flow/sx-plan-arch/A7.4a' into arch-refactor 2026-06-03 11:20:45 +03:00
agra
312d2e90ed refactor(backend): extract scalar instruction handlers into ops.zig (A7.4 slice a)
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.
2026-06-03 11:11:10 +03:00
agra
2f7c99fd11 refactor(backend): extract JNI slot cache into ffi_ctors.zig (A7.3 slice 2a)
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).
2026-06-03 10:42:58 +03:00
agra
e8c33bfc00 refactor(backend): extract Obj-C runtime constructors into src/backend/llvm/ffi_ctors.zig (A7.3 slice 1)
Move the Obj-C module-init constructor emission out of emit_llvm.zig into a
FfiCtors backend *LLVMEmitter facade (field `e`). Behavior-preserving relocation
— self.* -> self.e.* only.

- src/backend/llvm/ffi_ctors.zig (FfiCtors): emitObjcSelectorInit (cached SEL
  init), emitObjcClassInit (objc_getClass class-object cache), and
  emitObjcDefinedClassInit (class-pair registration: ivars, method IMP table,
  +alloc/-dealloc IMPs, #implements protocol conformances). Emit-time caches
  (ir_mod.objc_*_cache) + global_map + cached LLVM handles read via self.e.*.
- 3 call sites in LLVMEmitter.emit routed via a new ffiCtors() accessor.
- Shared infra stays in emit_llvm.zig, widened to pub (the facade calls back):
  lazyDeclareCRuntime (11 callers), emitPrivateCString (11 callers),
  injectCtorIntoMain (the moved defined-class ctor's callee). No @llvm.global_ctors
  shape / IMP-table / ivar / protocol-conformance change.

Pins: 1309 (class-method lowering), 1319 (property getter/setter IMPs), 1314
(alloc/dealloc IMPs), 1332 (sret + addMethod) all green.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn).
2026-06-03 10:35:15 +03:00
agra
836583d7f7 test(backend): pin JNI constructor (FindClass/<init>/NewObject) before A7.3 extraction (A7.3 scaffolding coverage fix)
Codex review of 91651e3 noted no .ir snapshot pinned emitJniConstructor's distinct
FindClass -> GetMethodID("<init>") -> NewObject shape; 1402/1418/1408 cover regular/
static GetMethodID slot caching, not constructor emission.

Add examples/expected/1425-ffi-jni-main-03-ctor.ir (FindClass x4 / GetMethodID x4
/ NewObject x2 / <init>), path-free + idempotent, trailing newline trimmed. Suite
count unchanged (snapshot on an existing example).

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(git diff --check clean; only the intended ctor snapshot added).
2026-06-03 10:26:45 +03:00
agra
91651e3e56 test(backend): pin Obj-C property + alloc/dealloc IMP ctors before A7.3 extraction (A7.3 scaffolding)
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).
2026-06-03 09:39:16 +03:00
agra
46b874074b refactor(backend): extract reflection metadata + trace frames into src/backend/llvm/reflection.zig (A7.2 reflection)
Move the type/field/tag reflection name-array builders and the error-trace Frame
builders out of emit_llvm.zig into a Reflection backend *LLVMEmitter facade
(field `e`). Behavior-preserving relocation — self.* -> self.e.* only.

- src/backend/llvm/reflection.zig (Reflection): getOrBuildTypeNameArray /
  getOrBuildFieldNameArray / getOrBuildTagNameArray (pub) + emitTraceFrame (pub)
  + buildStringConst (private trace helper). The memoized state
  (type_name_array(_len) / field_name_arrays / tag_name_array / frame_str_cache)
  stays on LLVMEmitter; the facade reads/writes via self.e.*.
- Routed the 5 call sites through a new reflection() accessor (type_name /
  field_name / error_tag_name builtins, emitFailableMainRet's tag-name lookup,
  and the .trace_frame push).
- Kept in emit_llvm.zig per the A6.1 "emission-heavy stays" precedent:
  getFrameStructType (composite-type getter, widened to pub — emitTraceFrame calls
  it back), emitFieldValueGet (field-value reflection EMISSION, not an array
  builder), emitFailableMainRet. getStringStructType/getAnyStructType already pub.
- No reflection-array layout, trace-Frame field order, or linkage change.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (reflection
anchors 0030/0118/0517/0520 + trace anchors 1024/1025/1026 all ok, no churn).
2026-06-03 09:29:27 +03:00
agra
f92a743c85 refactor(backend): extract DWARF debug info into src/backend/llvm/debug.zig (A7.2 debug)
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).
2026-06-03 09:22:40 +03:00
agra
71f1cb2fb0 refactor(backend): extract LLVM type/ABI lowering into src/backend/llvm/ (A7.1 step 2)
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).
2026-06-03 09:10:27 +03:00
agra
e50caa4628 test(backend): trim trailing blank lines in 1202 .ir snapshot (A7.1 scaffolding fix)
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.
2026-06-03 08:59:26 +03:00
agra
d6078c2e6b test(backend): lock LLVM type/ABI shapes before A7.1 extraction (A7.1 scaffolding step 1)
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).
2026-06-03 08:53:51 +03:00
agra
91b300580b Merge Phase A6 (FFI domain extraction) into master
Extracts the pure FFI decision helpers out of lower.zig into domain modules,
behavior-preserving, test-first (scaffolding commit locks behavior, extraction
commit moves code), gated on zig build / zig build test / tests/run_examples.sh.

- A6.1 — Obj-C decision helpers → src/ir/ffi_objc.zig (ObjcLowering, a *Lowering
  facade): deriveObjcSelector, objcTypeEncodingFromSignature (+appendObjcEncoding/
  bailObjcEncoding/ObjcEncodingStack), objcPropertyKind (+ObjcPropertyKind enum),
  isObjcClassPointer, objcDefinedStateStructType (+objcStateAllocatorType).
- A6.2 — pure JNI helpers → src/ir/jni_descriptor.zig (plain pub free fns, no
  facade): jniMangleNativeName, isJniReturnTypeSupported. (Descriptor derivation
  was already there.)

Emission-heavy code (emitObjc*/lowerObjc*Call, synthesizeJniMainStub*/lowerJni*)
stays in lower.zig per PLAN A6 step 6; Java rendering stays in jni_java_emit.zig.

Regression anchors: 48 13xx Obj-C + 26 14xx JNI examples, 4 Obj-C + 9 JNI .ir
snapshots, ObjC/JNI helper unit tests. Gate at merge tip: zig build,
zig build test, tests/run_examples.sh -> 361/0.
2026-06-03 08:35:38 +03:00
agra
20c767e336 refactor(ir): move pure JNI helpers into jni_descriptor.zig (A6.2 step 2)
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).
2026-06-03 08:28:41 +03:00
agra
0a4a240e31 test(ir): lock pure JNI decision helpers before A6.2 extraction (A6.2 scaffolding step 1)
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).
2026-06-03 08:14:46 +03:00
agra
9bde1dd590 refactor(ir): extract ObjcLowering (ffi_objc.zig) for pure Obj-C decision helpers (A6.1 step 2)
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).
2026-06-03 08:00:42 +03:00
agra
b5119e8587 test(ir): cover Obj-C protocol pointers in isObjcClassPointer/objcPropertyKind (A6.1 scaffolding review fix)
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.
2026-06-03 07:45:10 +03:00
agra
0012228796 test(ir): lock pure Obj-C decision helpers before A6.1 extraction (A6.1 scaffolding step 1)
Test-first scaffolding for the Obj-C FFI domain (Phase A6.1) before the pure
helpers move into src/ir/ffi_objc.zig. Visibility-only change to the targets —
no behavior change.

- 3 new lower.test.zig tests for the pure helpers the ARCH-SAFETY A6.1 row names
  that lacked direct unit coverage:
  - deriveObjcSelector: niladic (bare name) / single-keyword (name:) /
    multi-keyword (_ -> : + trailing) / #selector(...) override (verbatim,
    keyword_count = #colons).
  - objcPropertyKind: assign default (primitive), strong default (object ptr),
    explicit weak/copy/assign win over the default.
  - isObjcClassPointer: pointer-to-foreign-Obj-C-class true; plain-struct ptr /
    *void / builtin false.
- objcTypeEncodingFromSignature (x6) + objcDefinedStateStructType (x3) already
  covered — no new tests.
- Widened deriveObjcSelector + objcPropertyKind to pub (they become facade
  methods in step 2; the ObjcPropertyKind enum stays private — tests compare via
  enum-literal == .strong). No logic touched.
- Recorded the A6.1 coverage inventory + residual gaps (resolveObjcParentName,
  class-method metadata, property/state lookups — example-guarded) in
  ARCH-SAFETY.md.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(no .ir churn; Obj-C snapshots 1309/1329/1332/1347 green).
2026-06-03 07:15:56 +03:00
agra
d346bbb677 Merge arch-refactor through Phase A5 into master
Brings the compiler-architecture refactor from the Phase A2 merge (7cc8057)
up through Phase A5. Each phase converged a compiler question onto a single
canonical owner, behavior-preserving, test-first (scaffolding commit locks
behavior, extraction commit moves code), gated on
zig build / zig build test / tests/run_examples.sh at every step.

- A2.4 — unknown-type diagnostic pass → semantic_diagnostics.zig.
- Issues 0068–0073 — forward type aliases, global/module-const initializers,
  non-constant global diagnostics, closure-in-defer lowering segfault.
- A3.1 — non-call expression typing → ExprTyper (expr_typer.zig).
- A3.2 — call result typing + classification → CallResolver/CallPlan (calls.zig);
  lowering shares the namespace/value boundary.
- A4.1 — generic substitution + mono keys → GenericResolver (generics.zig).
- A4.2 — protocol/impl lookup + registration + planning → ProtocolResolver
  (protocols.zig).
- A4.3 — coercion/xx classification → CoercionResolver (conversions.zig).
- A5.1 — error-set convergence → ErrorAnalysis (error_analysis.zig).
- A5.2 — path-sensitive error-flow diagnostics → ErrorFlow (error_flow.zig).

Regression anchors incl. examples 1046–1053 for the error streams.
Gate at merge tip: zig build, zig build test, tests/run_examples.sh -> 361/0.
2026-06-03 07:01:19 +03:00
agra
1f354f6da0 refactor(ir): extract ErrorFlow (error_flow.zig) for path-sensitive error-flow diagnostics (A5.2 step 2)
Move the diagnostic-only Pass 1e (ERR E1.7 cleanup-absorption + E1.8 value-slot
liveness) out of lower.zig into src/ir/error_flow.zig behind an ErrorFlow
*Lowering facade (Principle 5, like ErrorAnalysis/CoercionResolver). Behavior
preserved exactly — pure relocation.

Moved verbatim (self. -> self.l. for Lowering members; sibling calls stay on the
facade; provenHas is a file-local free fn): checkErrorFlow, analyzeFnBody,
flowWalk, flowStmt, flowIf, flowMatch, flowExpr, applyRefinement,
provenAdd/provenClone/provenIntersect, registerFailableDestructure,
checkCleanupBody/checkCleanupNode/cleanupReject, plus the FlowCtx/ProvenSet types.

- lowerRoot routes the single call site through
  self.errorFlow().checkErrorFlow(decls); no Lowering wrapper kept (only the
  pipeline calls it, no unit-test caller). New errorFlow() accessor.
- The pass takes AST decls + ProgramIndex + diagnostics only — independent of IR
  Builder state (PLAN-ARCH A5.2 success criterion).
- New pub: exprIsFailable (only widening; inferExprType/errorChannelOf already
  pub). lower.zig -389 (->17030); error_flow.zig 407. Barrel-wired in ir.zig.
- No .test.zig: diagnostic-pass altitude (functions return only bool + emit
  diagnostics) — guarded by example anchors 1046-1053 (incl. scaffolding
  1051/1052/1053). Phase A5 complete.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(anchors 1046-1053 all ok, no .ir churn).
2026-06-03 06:54:13 +03:00
agra
2d2bfafa29 test(ir): pin both lambda arms of the error-flow pass (A5.2 scaffolding review fix)
Codex review of 95895a3 found 1051 reached neither lambda arm it claimed to
pin: the lambda arrived only as a var_decl initializer, which routes through
checkCleanupNode's `.var_decl` arm -> cleanupReject(lambda) -> early-return
(a lambda literal is not failable), so the `.lambda` stop never ran; and its
accepted-direction `if !err` guard would still pass with flowExpr's lambda
recursion removed.

Scaffolding-only fix (no compiler change):
- 1051: add a bare lambda STATEMENT `() -> !E { failing(); };` in the cleanup
  body so checkCleanupNode sees a `.lambda` node directly and stops (the bare
  failable inside is accepted; were the arm to recurse it would reject like
  1052). Output byte-identical — only the .sx gained the statement.
- 1053-errors-nested-lambda-liveness-reject (exit 1): an E1.8 value-slot read
  inside a never-called nested lambda, rejected only because flowExpr recurses
  via `.lambda => analyzeFnBody`. Remove that arm and the diagnostic vanishes
  -> suite fails. This is the discriminating negative 1051 lacked.

Gate: zig build test, bash tests/run_examples.sh -> 361/0.
2026-06-03 06:42:51 +03:00
agra
95895a3bb2 test(ir): lock error-flow diagnostics before A5.2 extraction (A5.2 scaffolding step 1)
Test-first scaffolding for the path-sensitive error-flow pass
(checkErrorFlow/analyzeFnBody/flowWalk/flowIf/checkCleanupBody) before it
moves into src/ir/error_flow.zig. No compiler change — both examples lock
current behavior.

- 1051-errors-cleanup-closure-boundary (accepted): a closure literal inside a
  `defer` body is its own function boundary — the E1.7 cleanup rule and the
  parser's try/raise ban both stop at the lambda, and E1.8 value-slot liveness
  runs per-boundary. Pins checkCleanupNode's `.lambda` stop + flowExpr's
  `.lambda` recursion. Constructible since issue 0073 (0310).
- 1052-errors-cleanup-transitive-reject (exit 1): the E1.7 cleanup check is
  transitive — bare failables nested in an `if` (both branches), a nested
  block, and a `while` body all reject. Pins checkCleanupNode's recursive arms,
  distinct from 1049's direct-body case.

No .test.zig/.ir: diagnostic-pass altitude (checkErrorFlow/A2.4 precedent) —
the pass returns no fact object and emits no IR.

Gate: zig build, zig build test, run_examples.sh -> 360/0.
2026-06-03 06:31:18 +03:00
agra
08f263c6e4 fix(ir): open a fresh defer window when lowering a lambda body (issue 0073)
A closure literal declared inside a `defer` body segfaulted the compiler.
Root cause: lowerLambda never opened its own `func_defer_base` window. Every
other function-lowering entry (lowerFunction / monomorphizeFunction /
monomorphizePackFn) saves func_defer_base, sets it to defer_stack.items.len, and
restores it — lowerLambda didn't. So a lambda's `return` drained the ENCLOSING
function's defers; when the defer body itself declared the lambda, draining
re-lowered the lambda, which returned, which drained again → infinite recursion
→ stack-overflow SIGSEGV (the failable variant surfaced one frame out, in
expandCallDefaults→lookupFn reading a clobbered scope).

Fix: lowerLambda now saves func_defer_base + the defer_stack length, sets the
base to the current length (a fresh window), and restores both on exit — so a
lambda's `return` drains only its own defers.

Regression: examples/0310-closures-closure-literal-in-defer.sx — a closure
declared and called inside a `defer`; verifies `body` then `defer closure: 42`
at scope exit (exit 0). Issue 0073 marked RESOLVED; repro promoted from
issues/0073-*.sx.

zig build, zig build test, tests/run_examples.sh (358/0) all green.
2026-06-02 23:29:49 +03:00
agra
eb20b2ddb5 docs(issues): file 0073 — closure literal inside a defer body segfaults lowering
Minimal repro (issues/0073-...sx): a non-failable, uncalled closure literal
declared inside a `defer` body crashes the compiler with a SIGSEGV in
lowerLambda (src/ir/lower.zig). Isolation shows the trigger is "a closure
literal lowered inside a defer body" — not failability, not whether it's called
(closures and failable closures lower fine outside a defer). Pre-existing
lowering bug, unrelated to the A5 error-analysis extraction; surfaced while
writing an A5.2 cleanup-absorption test example.

Filed per the IMPASSIBLE RULE: work paused pending a fix in another session.
2026-06-02 23:20:19 +03:00
agra
667192c718 refactor(ir): extract ErrorAnalysis (error_analysis.zig) for error-set convergence (A5.1 step 2)
Error-set convergence now lives in src/ir/error_analysis.zig behind a *Lowering
facade (ErrorAnalysis), mirroring the other domain extractions. Moved verbatim:
- convergeInferredErrorSets (whole-program inferred-`!` SCC fix-point),
- convergeClosureShapeSets,
- collectErrorSites / collectClosureShapes (the AST collectors).

Added ErrorFacts (the PLAN-ARCH shape: inferred_error_sets + shape_inferred_sets)
+ a facts() view over the maps, which stay on Lowering for now (consumers read
them via self.*). recordClosureShape and its deep type/shape helper web stay in
Lowering; it reaches the moved collectErrorSites via self.errorAnalysis().

Lowering keeps convergeInferredErrorSets / convergeClosureShapeSets as thin pub
wrappers (the lowering pipeline + the E1.4b unit test call them); collectErrorSites
/ collectClosureShapes are deleted (no fallback). New pub: isErrorTagLiteralNode /
callTargetName / astIsPureBareInferred / astPureNamedSet / containsTag /
namedSetTags / recordClosureShape (the moved collectors / facade reach them).
lower.zig net -216 lines.

The 2 convergence unit tests (transitive SCC across a try edge; closure-shape
union) moved from lower.test.zig to error_analysis.test.zig and now drive the
facade directly; the E1.4b test stays in lower.test.zig via the wrapper. Module
named error_analysis.zig, NOT errors.zig (src/errors.zig is the DiagnosticList).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn.
2026-06-02 23:11:18 +03:00
agra
9153f958ea test(ir): lock error-set convergence before A5.1 extraction (A5.1 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/error_analysis.zig — no code
change to the convergence targets (convergeInferredErrorSets /
convergeClosureShapeSets / collectErrorSites / collectClosureShapes).

Adds 2 unit tests via the already-pub convergence functions (no new exposure):
- convergeInferredErrorSets transitive/SCC: a `caller :: () -> ! { try raiser(); }`
  with no direct raise converges to raiser's {Foo} across the try edge — the
  whole-program fixpoint A5.1 must preserve. (Today's E1.4b test only covered a
  direct raiser + the empty-set warning.)
- convergeClosureShapeSets: a bare-`!` closure literal `() -> ! { raise error.Bar }`
  inside a host fn unions {Bar} into one shape_inferred_sets entry.

Adds 2 .ir snapshots (first .ir for these error forms), vetted clean
(idempotent, path-free, no #run): 1006-errors-inferred-error-sets (inferred-set
error-channel shapes) and 1009-errors-catch (catch lowering). 1004-errors-try
was already pinned.

PLAN-ERR is complete/idle, so the A5 overlap risk is low (the target functions
are stable, not in-flight). The sub-step-2 module will be named
src/ir/error_analysis.zig, NOT errors.zig (src/errors.zig is the DiagnosticList).

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 22:57:39 +03:00
agra
f3bda369f6 refactor(ir): extract CoercionResolver (conversions.zig) for coercion planning (A4.3 step 2)
Coercion classification now lives in src/ir/conversions.zig behind a *Lowering
facade (CoercionResolver), mirroring CallResolver / GenericResolver /
ProtocolResolver. Two pure classifiers:
- classify(src, dst) -> CoercionPlan (15 kinds: no_op / unbox_any / box_any /
  closure_to_fn_reject / tuple_elementwise / optional_unwrap / void_to_optional /
  optional_wrap / erase_protocol / int_to_float / float_to_int / ptr_int_bitcast /
  widen / narrow / none) — the built-in coercion ladder.
- classifyXX(src, dst) -> XXPlan (unbox_any / no_op / erase_protocol /
  protocol_to_pointer / coerce) — the xx-operator head.

coerceToType and lowerXX now `switch (classify…)` then emit; branch order
mirrors the originals exactly and every arm reproduces the prior lowering — the
f32/f64 Any match dispatch, buildProtocolErasure (lowerXX) vs buildProtocolValue
(coerceToType), tuple/optional recursion, and the user-Into fallback + pointer
materialization + recursion-guard/diagnostics (which stay in lowerXX /
tryUserConversion). IR emission stays entirely in Lowering; the classifiers are
pure. lowerXX keeps the operand's lowered Ref type as src_ty. `.none` means no
built-in applies (pass through; the Into fallback runs) — no silent default.

New pub: isFloat / isIntEx / typeBitsEx / resolveConcreteTypeName (the classifier
reads them); coercionResolver() accessor. lower.zig net -54 lines.

conversions.test.zig drives CoercionResolver directly: the full classify ladder
(no-op, Any box/unbox, widen/narrow, int<->float, ptr<->int, optional
wrap/unwrap, void->optional, tuple, closure-reject, .none for two unrelated
structs), erase_protocol for a concrete source, and classifyXX (all 5 kinds incl.
protocol-to-pointer vs coerce and pointer-materialization -> coerce).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn.
2026-06-02 22:45:56 +03:00
agra
50dd2cc3d8 test(ir): lock coercion forms before A4.3 extraction (A4.3 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/conversions.zig — no code
change to the coercion targets (lowerXX / coerceToType / coerceOrErase /
buildProtocolErasure / tryUserConversion / failable-adapter selection).

Adds 4 .ir snapshots (first .ir for 01xx/09xx/10xx), each captured surgically
via `sx ir | normalize_ir`, path-free, idempotent, and print-free at IR-gen time
(0114-types-build-block-convert was rejected — it prints `--- void / 0 args ---`
+ sx source at IR-gen):
- 0107-types-int-cmp-in-float-ternary   numeric int<->float coercion
- 0903-optionals-optional-roundtrip     optional wrap/unwrap
- 0904-optionals-any-to-string-optional xx unbox_any + optional
- 1004-errors-try                       error-channel adapter/coercion

Protocol erasure + user Into are already pinned by the 04xx snapshots
(0400/0413/0414/0416); duplicate-conversion rejection by the 0410/0411/0412
anchors.

Adds 1 unit test via the public surface (no new exposure, mirroring A4.1/A4.2
sub-step 1): optionalOfFlattened — the optional wrap/flatten coercion rule
(T -> ?T; ?T -> ?T, never ??T; contrasted with the non-flattening optionalOf).
The lowerXX/coerceToType/coerceOrErase/buildProtocolErasure decisions are private
+ emission-bound, so their CoercionPlan unit tests land with the extracted module
in sub-step 2.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 22:32:01 +03:00
agra
137285f33d refactor(ir): factor protocol/impl planning into ProtocolResolver (A4.2 planning increment)
Factor the lookup/planning half of the protocol emission functions into
protocols.zig, keeping IR emission in Lowering (PLAN-ARCH A4.2 final increment):
- protocolMethodInfos(proto) — the dispatch method table = which methods
  getOrCreateThunks must thunk. getOrCreateThunks now does PLANNING via this +
  EMISSION (createProtocolThunk loop) in Lowering.
- findVisibleImpls(entries, out) — moved verbatim (pure BFS over the import
  graph; the cross-module visibility selection behind the 0410 path).
  tryUserConversion calls it via the resolver.
- matchPackImpl(src_ty, pack_key) -> ?PackImplMatch — the pure pack-impl
  matching loop (prefix + return match) + convert-method find, returning the
  matched entry + convert fd + src params/ret. tryPackImplMatch consumes it; the
  binding + monomorphise + call emission stays in Lowering.

Emission untouched: createProtocolThunk, buildProtocolValue, and the
monomorphise+call tails of tryUserConversion / tryPackImplMatch remain in
Lowering. The reentrancy guard, key-build, and the Into no-visible / duplicate /
recursive diagnostics stay in tryUserConversion (byte-for-byte). lower.zig net
-94 lines. No new pub exposure (uses the existing ParamImplEntry /
PackParamImplEntry / formatTypeName surface).

protocols.test.zig +3: protocolMethodInfos (method table + null-for-unknown, no
silent empty default); findVisibleImpls (falls open with no graph; filters to
here + transitive imports); matchPackImpl (selects on prefix+return; null for
non-closure source / unknown key).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 diagnostics are byte-for-byte preserved.
2026-06-02 22:23:01 +03:00
agra
e6cbb60d8f refactor(ir): move protocol/impl registration into ProtocolResolver (A4.2 registration increment)
Move the registration functions behind the protocols.zig facade, per PLAN-ARCH
A4.2 ("then registration", keeping IR emission in Lowering):
- registerProtocolDecl (protocol struct + dispatch method table + vtable type),
- registerImplBlock (concrete impl -> <Target>.<method> in fn_ast_map + default-
  method synthesis),
- registerParamImpl (parameterised impl -> param_impl_map / param_impl_pack_map
  + the same-file duplicate diagnostic),
- synthesizeDefaultMethod (facade-private; its only caller moved too).

Moved verbatim with self. -> self.l. facade rewrites. Emission stays in
Lowering: the registry calls self.l.declareFunction (the extern-stub primitive)
but the thunk/value builders (createProtocolThunk / buildProtocolValue /
tryUserConversion / getOrCreateThunks) are NOT moved.

Lowering keeps registerProtocolDecl as a thin pub wrapper (scan pass + 7
unit-test callers); registerImplBlock / registerParamImpl /
synthesizeDefaultMethod deleted (no fallback), the 2 scan call sites routed
through protocolResolver(). New pub: declareFunction (8 callers, emission infra),
ParamImplEntry / PackParamImplEntry (the registry constructs them; stay as
Lowering nested types). State maps remain on Lowering; the facade reads/writes
self.l.* (migrate once planning lands).

protocols.test.zig +2: registerImplBlock records Circle.draw in fn_ast_map (and
packArgConformsTo then sees it); registerParamImpl flags a same-file duplicate
impl Into(s64) for IntCell (the 0412-class, unit level).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 rejection diagnostics are byte-for-byte preserved.
2026-06-02 22:10:40 +03:00
agra
81d332dfb0 refactor(ir): extract protocol/impl lookup into protocols.zig (A4.2 step 2)
Move the pure protocol/impl conformance lookups into one module,
src/ir/protocols.zig, behind a *Lowering facade (ProtocolResolver), mirroring
GenericResolver / CallResolver. Per PLAN-ARCH A4.2 ("move pure lookup first;
keep emission in Lowering"), this increment moves only the read-only queries:
- getProtocolInfo (is a type a registered protocol + its method table),
- hasImplPlain (have the (protocol, type) thunks been materialized),
- packArgConformsTo (impl-declaration-level conformance for ..xs: P).

Registration (registerProtocolDecl / registerImplBlock / registerParamImpl) and
all IR emission (createProtocolThunk / buildProtocolValue / tryUserConversion /
getOrCreateThunks) stay in Lowering for the later increments. The state maps
(protocol_thunk_map / param_impl_map on Lowering, protocol_decl_map /
protocol_ast_map in ProgramIndex) stay put; the facade reads them via self.l.* —
no map migration.

Lowering keeps getProtocolInfo as a thin pub wrapper (~9 callers incl.
calls.zig); hasImplPlain + packArgConformsTo are deleted (no fallback), their 3
call sites (computeHasImpl x2, the pack-conformance check x1) routed through
self.protocolResolver(). formatTypeName widened to pub (the lookups use it);
protocolResolver() accessor added.

protocols.test.zig (wired into the barrel) drives ProtocolResolver directly:
getProtocolInfo (registered vs builtin/plain-struct + wrapper delegation),
hasImplPlain (thunk-map materialization), packArgConformsTo (non-parameterised
requires <ty>.<m> in fn_ast_map; trivially-true for an erased protocol value;
false for unknown protocol).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
snapshot churn; the 0410/0411/0412 rejection anchors still pass.
2026-06-02 21:56:03 +03:00
agra
df386a422e test(ir): lock protocol/impl lookup before A4.2 extraction (A4.2 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/protocols.zig — no code change
to the refactor targets (registerProtocolDecl / registerImplBlock /
registerParamImpl / hasImplPlain / tryUserConversion / tryPackImplMatch /
createProtocolThunk / buildProtocolValue).

Adds 4 .ir snapshots (only 0400 existed for 04xx), each captured surgically via
`sx ir | normalize_ir`, path-free, idempotent, and print-free at IR-gen time
(the 0524 contamination lesson):
- 0413-protocols-parameterized-protocol-value  parameterized protocol
                                               (registerParamImpl + tryUserConversion)
- 0414-protocols-generic-struct-protocol-erase generic-struct erasure
                                               (createProtocolThunk + buildProtocolValue)
- 0416-protocols-auto-type-erasure             auto erasure (buildProtocolValue + thunk)
- 0528-packs-protocol-pack-methods             pack-variadic impl (tryPackImplMatch)

With existing 0400 (impl-for-builtin) they pin erasure (auto/generic/builtin) +
parameterized + pack-variadic + dispatch; the 0410/0411/0412 runtime anchors
already pin cross-module visibility + duplicate-impl rejections.

Adds 1 unit test via the public surface (no new exposure, mirroring A4.1
sub-step 1): registerProtocolDecl -> getProtocolInfo builds the dispatch method
table (method names, param_types with self excluded, concrete vs Self return
with ret_is_self + *void encoding). The impl-lookup / conversion plan-object
tests (hasImplPlain, tryUserConversion, tryPackImplMatch — private today) land
with the registry in sub-step 2.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 21:44:01 +03:00
agra
7ab5d7bee9 test(ir): cover buildTypeBindings strategy-2 inference (A4.1 coverage closeout)
Adds the one deferred A4.1 coverage item: a focused unit test for
GenericResolver.buildTypeBindings inferring a type param from value args
(strategy 2) with widest-match — add(1,2) => T=s64, and add(1.0,2) / add(1,2.0)
=> T=f64 regardless of argument order.

Previously this inference path was guarded only by the 0200 .ir snapshot; the
unit test pins it directly against the new generics.zig API. Test-only.

zig build test and tests/run_examples.sh (357/0) green.
2026-06-02 21:34:58 +03:00
agra
3ca68189c0 refactor(ir): extract GenericResolver (generics.zig) for substitution + mono keys (A4.1 step 2)
Generic substitution and monomorphization-key construction now live in one
module, src/ir/generics.zig, behind a *Lowering facade (GenericResolver),
mirroring CallResolver / ExprTyper. Moved verbatim:
- mangleTypeName + mangleParamList (the mono-key fragment builder),
- mangleGenericName (generic mono key), appendComptimeValueMangle (comptime-value
  fragment),
- buildTypeBindings (call-site type-param inference), inferGenericReturnType
  (generic return resolution).

inferGenericReturnType now uses a scoped TypeBindingScope (enter/exit with defer)
instead of a manual type_bindings save/restore — the PLAN-ARCH A4.1 "scoped
substitution env" shape; a generics.test.zig assertion confirms the prior
bindings are restored (the issue-0048/0050 leak class, for this field).

Lowering keeps a thin pub mangleTypeName wrapper delegating to
genericResolver().mangleTypeName, because ~30 cross-cutting callers (impl-map
keys, conversion keys, shape keys) reach it well beyond generics. mangleParamList
(sole caller was mangleTypeName) moved fully. The other 4 originals are deleted
(no fallback); their 6 call sites now go through self.genericResolver()
(calls.zig via self.l.genericResolver()).

matchTypeParam / extractTypeParam / isTypeParamDecl widened to pub (the moved
substitution logic calls them); genericResolver() accessor added. The 2
mangleTypeName / inferGenericReturnType unit tests moved from lower.test.zig to
generics.test.zig (driving GenericResolver directly) and wired into the barrel.

monomorphizeFunction / monomorphizePackFn intentionally stay in lower.zig (they
save/restore three fields across nested mono and call emission helpers) — a
heavier scoped-env adoption deferred to an optional sub-step 3.

zig build, zig build test, and tests/run_examples.sh (357/0) all green — no .ir
snapshot churn, confirming the move preserved mono-key/substitution output.
2026-06-02 21:28:31 +03:00
agra
e1f167a1c3 test(ir): replace contaminated 0524 IR snapshot with clean 0513 (A4.1 scaffolding fix)
The 0524-packs-generic-fn-pack-state-leak example has a #run that prints at
IR-gen time, and tests/run_examples.sh captures `sx ir ... 2>&1`, so its .ir
snapshot was contaminated with #run stdout (`0: len=0` ...) instead of pure IR.

Remove 0524.ir — pack-state isolation (the issue-0048/0050 class) stays guarded
by 0524's existing runtime .stdout/.exit, where a leaked outer pack_arg_types
would corrupt the printed len= sequence.

Replace it with 0513-packs-pack-mixed-comptime.ir, which is print-free at
IR-gen time (clean, idempotent, path-free) and additionally locks the
comptime-value mono-key path (appendComptimeValueMangle): the IR shows
tagged(7,..) vs tagged(9) producing distinct monos
@tagged__ct_7__pack_s64_s64_s64 / @tagged__ct_9__pack.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 21:13:50 +03:00
agra
91e99f80c7 test(ir): lock generic substitution + mono keys before A4.1 extraction (A4.1 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/generics.zig — no code change
to the refactor targets (buildTypeBindings / mangleGenericName / monomorphize* /
inferGenericReturnType / mangleTypeName).

Adds the first non-FFI generic/pack .ir snapshots (closing the ARCH-SAFETY §3
gap for this phase), each captured surgically via `sx ir | normalize_ir`,
path-free and idempotent:
- 0200-generics-generic            generic fn, type-param inference + mono
- 0201-generics-generic-struct     generic struct instantiation
- 0507-packs-pack-mono-dedup       mono-key dedup (same shape => one mono)
- 0518-packs-pack-value-dispatch   pack value dispatch (monomorphizePackFn)
- 0524-packs-generic-fn-pack-state-leak  pack-state isolation (issue-0048/0050
                                         class; guards the future scoped-env change)

Adds 2 unit tests via the existing public surface (no new pub exposure,
mirroring the A3.2 sub-step-1 cadence):
- mangleTypeName: pins the mono-key fragment encoding per type shape
  (s64 / ptr_X / opt_X / SL_X / mptr_X / AR_n_X / vec_n_X / struct-name / tu_X_Y).
- inferGenericReturnType: explicit type-arg path binds $T and resolves the
  -> T return (pair(s64,..) => s64, pair(f64,..) => f64).

The internal substitution/mono-key unit tests (comptime-value mangle,
buildTypeBindings strategies, scoped-env isolation) land with the generics.zig
extraction in sub-step 2, as A3.2's plan-object tests landed with CallPlan.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 21:05:33 +03:00
agra
1007e23561 refactor(ir): source lowerCall's namespace/value boundary from CallResolver (A3.2 convergence step 3)
lowerCall re-derived the namespace-vs-value (receiver-prepend) decision with a
19-line block duplicating the exact identifier/type_expr + scope/global walk
that CallResolver already owns (objectIsValue, the negation of is_namespace).
This boundary determines whether the receiver is prepended, so it must agree
with the plan's free_fn_ufcs (prepends) vs namespace_fn (does not)
classification from fa59a9d.

Make CallResolver.objectIsValue pub and set
  is_namespace = !self.callResolver().objectIsValue(fa.object)
so plan and lowering share one boundary definition and can never drift.
`!objectIsValue` matches the old block case-for-case (non-identifier => value;
identifier/type_expr in scope/global => value; else => namespace), so this is a
behavior-identical substitution.

Deeper switch(plan.kind) routing of lowerCall is intentionally NOT done here: it
is not behavior-preserving as-is. `plan` is typing-only and coarser than
`lowerCall` — its method/namespace arms carry comptime / generic /
generic-template / #compiler / type-constructor dispatch `plan` does not model,
and its value-receiver kinds (struct_method/protocol_dispatch/foreign_instance)
do not gate on objectIsValue, so a type-name receiver (Point.make()) could be
mis-classified vs the namespace/static call lowerCall actually performs. Driving
prepend decisions off plan.kind would mis-prepend; objectIsValue is the correct
single source, hence routing the boundary specifically. PLAN-ARCH A3.2 success
criteria met (shared classifier; no duplicated return-type logic; plan tests;
stable .ir snapshots).

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 20:53:13 +03:00