Commit Graph

654 Commits

Author SHA1 Message Date
agra
f49a49cd07 fix(diagnostics): reject reserved/builtin type names used as identifiers (issue 0076)
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.
2026-06-03 19:00:39 +03:00
agra
4ab3608f77 Merge branch 'docs/trace-output-repair' 2026-06-03 16:55:00 +03:00
agra
99a5c781a0 docs: fix stale error-trace output format + markers
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.
2026-06-03 16:54:36 +03:00
agra
973543ddf8 Merge branch 'arch-refactor' 2026-06-03 16:34:16 +03:00
agra
1148362353 Merge branch 'flow/sx-plan-arch/fix-0075' into arch-refactor 2026-06-03 16:12:39 +03:00
agra
aca077d720 fix(reflection): replace silent .s64 arg-type fallback with loud .unresolved (issue 0075)
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`.
2026-06-03 16:05:31 +03:00
agra
759e3caa5e Merge branch 'flow/sx-plan-arch/fix-0074' into arch-refactor 2026-06-03 15:55:39 +03:00
agra
633c0a2540 docs(issues): file 0075 — silent .s64 type fallback in reflection builtins
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.
2026-06-03 15:55:32 +03:00
agra
4537538bb2 fix(ffi): replace silent .void arg-type fallback with loud .unresolved (issue 0074)
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`.
2026-06-03 15:43:27 +03:00
agra
6f4b872254 Merge branch 'flow/sx-plan-arch/A9.2' into arch-refactor 2026-06-03 15:19:02 +03:00
agra
a7ddbeb85b docs(error-handling): trace locations come from embedded Frame metadata, not DWARF (A9.2) 2026-06-03 15:02:09 +03:00
agra
e5d9d1fec1 docs(debugger): correct interp push-call model and span.start term (A9.2)
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.
2026-06-03 14:49:23 +03:00
agra
0e5b79ddab docs(debugger): call getFrameStructType a literal (anonymous) struct type (A9.2) 2026-06-03 14:36:08 +03:00
agra
e907fc9e01 docs(debugger): describe Frame global build as LLVMConstNamedStruct over getFrameStructType (A9.2)
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).
2026-06-03 14:28:28 +03:00
agra
e6c51359fe docs(debugger): align trace-push mechanism to one ground-truth model (A9.2)
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.
2026-06-03 14:17:24 +03:00
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