Commit Graph

664 Commits

Author SHA1 Message Date
agra
1d311b871e test(json): pin s64 MIN/MAX writer bytes; move scratch to .sx-tmp
Close the coverage gap from attempt 1: example 0713 now builds integer
fields holding s64 MIN (-9223372036854775808) and s64 MAX
(9223372036854775807) — plus zero, a small negative, and a small positive —
and asserts the EXACT emitted bytes. This permanently pins the edge that
write_int is specifically engineered for (folding positives into negative
space so MIN's non-representable-positive magnitude serializes correctly).

s64 MIN is expressed as (0 - 9223372036854775807 - 1) because its magnitude
is not a representable positive s64 literal.

Test hygiene: stream to a repo-local, gitignored .sx-tmp/ path (created if
missing) instead of a fixed /tmp name, and unlink it right after read-back
so nothing leaks. Writer/model logic and src/ are untouched.
2026-06-04 01:08:14 +03:00
agra
4552ed61f6 std/json: value model + zero-alloc writer with stable key order
Add library/modules/std/json.sx — the JSON value model and writer
(reader lands in a later step).

Value model: a tagged union over null/bool/integer(s64)/string/array/
object. Objects are an ORDERED list of (key,value) pairs preserving
INSERTION ORDER (no hash map, never sorted/deduped). Integers only — no
fraction/exponent this milestone.

Heap discipline:
  - Scalars carry no heap; string values are VIEWS into caller memory
    (never copied into the node).
  - Composite nodes (Array/Object) own growable child storage, allocated
    through an EXPLICIT allocator parameter on the builder methods
    (arr.add(v, alloc) / obj.put(key, val, alloc), mirroring List.append)
    — never the implicit context allocator.
  - The writer adds ZERO output allocations: it emits into a caller-
    provided Sink, either a fixed []u8 buffer (overflow raises, never
    truncates) or streaming straight to an fs.File through a small caller
    staging buffer (no whole-document string; peak memory O(staging)).
    Integer digits format in a stack [20]u8; s64 MIN is handled by
    formatting in negative space. Sink/IO/overflow surface on the !
    error channel.

examples/0713-modules-json-writer.sx builds a nested object + array +
string with every escape kind + negative int + bool + null, then asserts
the EXACT bytes (insertion order, escaping) from both the buffer sink and
the file-streaming sink, plus the overflow-raises path.
2026-06-04 00:47:30 +03:00
agra
9bf07e0c5f Merge branch 'flow/sx-foundation/F1.2' into dist-foundation 2026-06-04 00:20:31 +03:00
agra
f9bc593bb8 F1.2: std.hash zero-heap [64]u8 hex API + chunked file + pinned vectors
Make the SHA-256 digest path allocation-free (foundation heap-discipline):

- final() and sha256_hex() now return the 64-char lowercase hex digest as
  a [64]u8 by value on the stack; the cstring(64) heap allocation is gone.
- sha256_file() streams the file in fixed 64KB stack chunks via open_file/
  File.read/File.close (defer-closed on every path) instead of slurping it
  with read_file; peak memory is O(chunk), not O(filesize).

Tests (compare via a zero-copy string view over the [64]u8):
- 0710 updated to the by-value API (output unchanged).
- 0711 known-answer vectors: "", "abc", NIST-56/112, padding boundaries
  {0,55,56,57,63,64,65,119,120}, and 1000 / 1,000,000 'a' repeats, each
  pinned to its published digest (cross-checked with shasum -a 256).
- 0712 streaming equivalence (one-shot == byte-at-a-time == split-mid-block
  == split-on-boundary) plus sha256_file(temp) == in-memory digest.

src/ untouched. zig build && zig build test && tests/run_examples.sh green.
2026-06-04 00:08:46 +03:00
agra
ee1e097335 Merge branch 'flow/sx-foundation/F1.1' into dist-foundation 2026-06-03 22:47:40 +03:00
agra
8f9691c206 F1.1: std.hash — streaming SHA-256 in library/modules/std/hash.sx
Add a pure-sx streaming SHA-256 (FIPS 180-4) stdlib module, importable
as `#import "modules/std/hash.sx";`. All 32-bit word arithmetic is done
in s64 and masked back with `& MASK32`, so digests are deterministic and
platform-independent — no shelling out, no native crypto.

API:
- init() -> Sha256          (by-value *self pattern)
- update(*Sha256, string)   (multi-block + partial-block buffering)
- final(*Sha256) -> string  (32-byte digest as lowercase hex)
- sha256_hex(string) -> string             (one-shot)
- sha256_file([:0]u8) -> ?string           (digest of a file via fs.read_file)

Verified against FIPS/NIST known-answer vectors and `shasum -a 256`:
"" , "abc", the 56- and 112-byte multi-block vectors, 1000×'a', and the
64/65-byte block boundaries; chunked update() matches the one-shot call.

examples/0710-modules-sha256.sx pins the KAT vectors + the streaming
invariant; gate green (zig build, zig build test, run_examples 370/0/0/0).
2026-06-03 22:38:58 +03:00
agra
a89a5f8d18 Merge branch 'flow/sx-foundation/F0.1' into dist-foundation 2026-06-03 22:18:43 +03:00
agra
6433eb6155 fix(diagnostics): point reserved-type-name binding errors at the binding (issue 0076)
The reserved-type-name binding diagnostic fired correctly but underlined the
enclosing statement / if / while / for / match / protocol / #objc_class block
because every binding-name check reused the parent `node.span`.

Thread each binding name's own span through the AST and parser, and pass it to
`checkBindingNames`:

- ast: add name spans to VarDecl, DestructureDecl, If/WhileExpr, ForExpr
  (capture + index), MatchArm, Catch/OnFailStmt, Protocol/ForeignMethodDecl.
- parser: populate each span at the binding site from the name token's loc;
  destructure reuses each target identifier's own span.
- semantic_diagnostics: every checkBindingName call now passes the binding's
  own span — no site falls back to node.span. fn/lambda params already used
  Param.name_span.

Carets now land on the offending identifier itself. New regression
examples/1125 asserts the protocol default-body and sx-defined #objc_class
method param spans; 0125/1119-1124 expected updated to the precise carets.
2026-06-03 22:06:56 +03:00
agra
fcc76b9391 fix(diagnostics): make reserved-type-name binding check exhaustive (issue 0076)
The reserved/builtin-type-name binding diagnostic was a hand-walked subset
of binding-bearing AST nodes with a silent `else => {}`, so each review
found another syntactic binding form that bypassed it and hit the original
LLVM verifier abort: destructure names (`s2, x := …`), `impl` method
params/locals, and `if` / `while` / `for` / match-arm / `catch` / `onfail`
captures.

Rewrite `checkBindingNames` (src/ir/semantic_diagnostics.zig) as an
EXHAUSTIVE `switch` over every `Node.Data` tag with NO `else` arm — a future
binding-bearing node type now fails to compile until it is handled here, so
coverage is enforced by the compiler instead of a hand-maintained list. The
check stays in the pre-lowering semantic pass rather than moving to the
`Scope.put` scope-registration choke point: lowering is lazy, so an
uncalled function's bindings never reach `Scope.put`, yet they must still be
rejected at their declaration (e.g. the never-called `takes_u8` in 1119).
No lowering special-case; `lower.zig` unchanged.

Regression tests (fail-before: LLVM abort or silent accept → pass-after:
clean diagnostic, exit 1):
- 1121 control-flow: destructure, if/while bindings, for capture+index,
  match-arm capture
- 1122 impl-block method: reserved param AND reserved local
- 1123 catch + onfail tag bindings
- 1124 destructure name reserved in an imported module
Existing 0125 / 1119 / 0135 / 1120 tests kept; full suite 368 passed.
2026-06-03 20:09:46 +03:00
agra
df6e830bec fix(diagnostics): reject reserved type-name bindings in every module (issue 0077)
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.

Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.

Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.

Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.

Resolves issues 0076 (coverage extension) and 0077.
2026-06-03 19:32:49 +03:00
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