Commit Graph

725 Commits

Author SHA1 Message Date
agra
d0ebc55f99 comptime VM: VM-native metatype CONSTRUCTION — declare/define + tagged-union enum_init (P3.4 step 7)
The metatype type-construction builtins now run natively on the flat-memory
VM, so the construction examples run HANDLED end-to-end (no call_builtin
fallback to the legacy interp).

- Tagged-union enum_init WITH payload: allocate zeroed, write the tag at
  offset 0, copy the payload at tag_size ({ header, [N x i8] } layout).
- New .call_builtin exec arm -> callBuiltinVm (VM-native mirror of the legacy
  execBuiltinInner): declare(name) mints an empty forward nominal slot (shared
  declareNominal, also used by declare_type); define(handle, info) reads the
  TypeInfo tagged-union VALUE from flat memory and mints via defineFromInfo,
  a faithful port of legacy defineEnum/defineStruct/defineTuple (all-void enum
  -> real .enum per issue 0142, dup-name rejection, updatePreservingKey vs
  replaceKeyedInfo). Unmodeled builtins bail -> legacy fallback (dual-path).
- Refactored the []{name,ty} decode out of registerTypeVm into a shared
  decodeMemberSlice (+ decodeTypeSlice for bare-Type tuple elements).
- Correctness guard: enum_init/define assume a tag-headed layout, wrong for a
  backing_type tagged union (laid out as the backing struct) — both now bail
  loudly on backing_type != null rather than silent-clobber.

Examples 0614/0620/0621/0624/0632 run fully HANDLED on the VM; 0622/0623 run
define HANDLED then fall back at the still-unported type_info. VM output
byte-matches legacy for all 7. 697/0 both gates + all unit tests (added:
tagged-union enum_init payload layout).
2026-06-18 15:48:48 +03:00
agra
eb68d9ed94 comptime VM: real lowering-time Context — allocating + List-building type-fns run on the VM (issue 0141)
The VM can now evaluate a comptime type-fn that allocates at lowering time (the
0141 family) — the legacy interp cannot. Four changes:

- runComptimeTypeFunc (lower/comptime.zig): force the CAllocator->Allocator thunks
  to exist (getOrCreateThunks, idempotent, guarded) BEFORE eval. A type-fn const
  runs at scanDecls (Pass 1), before Pass 1c builds the default-context global +
  thunks, so the comptime allocator was otherwise null.
- materializeDefaultContext: build a REAL context at lowering time when the global
  is absent — find the two thunks by name and lay their func-refs into the inline
  Allocator value at the head of Context, so context.allocator.alloc_bytes
  dispatches call_indirect -> thunk -> native VM malloc.
- aggType: deref a pointer base_type (the List write path emits struct_gep with
  base_type = *Struct; fieldOffset panicked on the pointer — now derefs, no panic).
- subslice: handle a [*]T many-pointer / *T base (a List's items field — the base
  IS the data pointer).

Verified end-to-end (manual probe): a compiler-API type-fn building its []Member in
a List(Member) runs HANDLED on the VM and mints (green=7) — the 0141 List-growth
pattern. Can't be a corpus test yet (gate-OFF/legacy can't allocate at lowering
time — the dual-path bind), so locked in via VM unit tests (many-pointer subslice;
struct_gep with a pointer base_type). 697/0 both gates + all unit tests.
2026-06-18 15:04:55 +03:00
agra
66005af478 comptime VM: port the WRITE side (declare_type/pointer_to/register_type) -> first HANDLED lowering-time type-fns
declare_type / pointer_to / register_type are now serviced natively in
Vm.callCompilerFn, mirroring the legacy compiler_lib handlers (mint via
@constCast(table) — the lowering-time mint target is &module.types). register_type
reads the []Member slice from flat memory: ref_types is threaded through invoke ->
callCompilerFn so the slice element type (Member = {name: string, ty: Type}) gives
the field offsets + stride; each {name, ty} is decoded and minted with the same
kind branching + dup/payload rejections + idempotent re-fill as legacy.

Key unblock: the synthesized comptime type-fn wrapper was built with return type
.any, so regToValue bailed at the VM<->legacy boundary; changed to .type_value
(the legacy path reads via asTypeId regardless). The compiler-API write type-fns
(0631 register-graph, 0635 multi-edge import) now run HANDLED end-to-end on the VM
at lowering time — parity-correct, on the zeroed lowering-time context (fixed
member arrays, no allocation). The metatype make_enum/define examples still fall
back cleanly through call_builtin(define).

697/0 both gates + EXIT=0.
2026-06-18 14:19:54 +03:00
agra
554871ba0b comptime VM: model .type_value natively (word); harden struct_init vs arrays
kindOf(.type_value) -> .word; new const_type exec arm -> word = TypeId.index();
regToValue maps a .type_value word back to a .type_tag Value at the legacy
boundary. The VM now runs comptime evals involving Type values instead of
bailing.

This reached a latent VM panic: struct_init assumed a .@"struct" result type and
union-access-panicked on an array literal (EnumVariant.[...]). It is the generic
aggregate-literal op, so it now dispatches on the result kind (struct/array/
tuple) and bails loudly on anything else — never panics (CLAUDE.md no-panic).

697/0 both gates (make_enum type-fns run further on the VM, then bail cleanly at
the define call_builtin -> legacy mints; no mutation before bail). VM unit test
added (const_type -> word -> regToValue -> .type_tag).
2026-06-18 14:05:16 +03:00
agra
94f60c51c0 comptime VM: flip Type to .type_value; migrate the .any refs that mean a Type value
type_resolver "Type" -> .type_value; const_type result + emitConstType now a
bare 8-byte i64 handle (not a 16-byte Any box). Migrated every .any ref meaning
"a Type value", leaving real boxed-Any refs:

- "Any holds a Type" meta-marker tag .any -> .type_value at all 4 consumers
  (reflectArgTypeId, reflectTypeId, the comptime type_tag-as-struct path,
  resolveTypeCategoryTags "type").
- reflection-builtin return types (type_of/declare/define) -> .type_value;
  runtime type_of(any) reads the tag as a .type_value (no re-box).
- expr_typer: a bare type-name expr is .type_value (backtick is_raw exempt).
- reflectionArgIsType accepts .type_value OR .any (a reflection arg can be a
  bare Type or a boxed Any).
- comptime switch_br accepts a .type_tag discriminant (type-category match).
- a bare function name in a Type slot -> const_type(its function type), not a
  func-ref (fixes a JIT crash); old string-box kept only for genuine Any params.
- field-not-found diagnostic + formatTypeName render .type_value as "Type".

Fixed 3 unit tests asserting the old .any behavior. 697/0 both gates (gate ON
bails cleanly to legacy since the VM doesn't model Type values yet) + 494 unit
tests. 24 snapshots regenerated (22 .ir const_type shape; 2 .stderr Any->Type).
2026-06-18 13:54:56 +03:00
agra
6844fb90e7 comptime VM: dedicated Type builtin TypeId (8B), distinct from .any — foundation (dead)
Add TypeId.type_value (slot 19) + matching TypeInfo.type_value variant: an
8-byte type handle, distinct from the 16-byte boxed .any. All types.zig layout
handlers wired (size/align 8, display "Type", hash/eql); toLLVMTypeInfo -> i64.

Reserve builtin headroom: first_user 19 -> 100 (slots 20-99 padded with the
unresolved tripwire) so future builtins don't renumber user TypeIds / churn
sx ir snapshots. 22 IR snapshots regenerated (pure renumber to 100-base).

type_resolver still returns .any for "Type" — nothing produces .type_value
yet, so no behavior change. 697/0 both gates.
2026-06-18 13:03:21 +03:00
agra
6473a4e227 comptime VM: lowering-time default context (P3.4 step 1)
materializeDefaultContext now falls back to a zeroed Context (found by name) when
the __sx_default_context global is absent — i.e. at lowering time, where the global
isn't emitted yet. A type-fn that never touches the allocator runs past context
setup; one that allocates reads a null alloc_fn (zeroed) and call_indirect on the
null func-ref bails to legacy (a real lowering-time context with the CAllocator
thunk func-refs is a follow-up).

Measurement (SX_COMPTIME_FLAT_TRACE): the bail moved deeper — make_enum now bails
at const_type (the Type-literal op, unported); register_type type-fns bail at the
welded write call. No table mutation before either bail (write fns bail before
minting), so parity holds: both gates 697/0, no crashes.

Next: model the const_type op + the Type-return bridge + the VM-native write side,
which together let a type-fn run end-to-end on the VM.
2026-06-18 12:12:10 +03:00
agra
9d041b5136 comptime VM: wire the VM at the lowering-time site + measure (P3.4)
Route runComptimeTypeFunc (the type-fn fold — the third comptime call site)
through comptime_vm.tryEval behind -Dcomptime-flat/SX_COMPTIME_FLAT with legacy
fallback, mirroring the two emit-time folds. Extract the shared post-check
(checkComptimeTypeResult — the declared-but-never-defined zero-field guard) so the
VM and legacy paths share it.

Measurement (SX_COMPTIME_FLAT_TRACE): every metatype/compiler-API type-fn bails
CLEANLY at "no __sx_default_context global to materialize the implicit context" —
at lowering time the default-context global doesn't exist yet (it's built at emit
time), so the VM bails at context materialization, before running the body (no
partial mint, no crash -> legacy mints). The hardening holds: no crashes across
the corpus on the lowering-time VM path.

So the first lowering-time blocker is the implicit context, not Type modeling.
Both gates 697/0. Near-pure fallback today — permanent scaffolding that lights up
as the default-context handling + Type modeling + VM-native write side land.
2026-06-18 11:55:59 +03:00
agra
34734d415b comptime VM: harden against malformed lowering-time IR (P3.4-prep)
Prerequisite for wiring the VM at the lowering-time comptime site
(runComptimeTypeFunc), where IR can be malformed (an unresolved name lowers to a
dangling / Ref.none operand — the 0737 crash). Close the remaining panic vectors
so the VM bails (-> legacy fallback) instead of aborting:

- Vm.refTy(ref_types, r): a bounds-checked accessor replacing every raw
  ref_types[ref.index()] in exec — the type-side companion to Frame.get's
  bad_ref value-side guard.
- aggType is now a bailing method (Error!TypeId) routed through refTy.
- the block-dispatch loop bounds-checks the branch target before indexing
  func.blocks.items (a malformed br target). global_get was already guarded.

No behavior change: gate OFF and -Dcomptime-flat both 697/0. Unit test added
(a cmp_lt with a Ref.none operand bails, not panics).
2026-06-18 11:45:40 +03:00
agra
9e3aabcf76 comptime VM: Phase 3 — register_type write side + payloadless-enum fixes
The mutating compiler-API, minting types LAZILY at lowering time (single pass,
the existing runComptimeTypeFunc path — so the write side is legacy-only; the
VM isn't wired at lowering time, and the read-side readers stay dual-path):

  declare_type(name) -> Type            forward nominal handle (≈ declare)
  pointer_to(t) -> Type                 build *T references
  register_type(handle, kind, members)  ONE kind-branching fill (≈ unified define)

register_type branches on kind IN THE COMPILER (subsuming define's per-kind
dispatch); codes match type_kind: 1 struct, 2 actual .@"enum", 3 tagged_union,
4 tuple. Members are {name: string, ty: Type}. A non-generic `-> Type` builder is
now flagged is_comptime (decl.zig) so its dead body permits the welded calls.

Graph support: forward declare_type handles + pointer_to express a mutually-
recursive A<->B graph (*A, *B, B-by-value) before bodies are filled. register_type
is idempotent — re-filling a nominal slot (a minting module reached via two import
edges) re-mints identically rather than erroring (nominalIdent reads identity from
any nominal kind).

Fixes (issue 0142):
- A fully payloadless comptime-minted enum was minted as an all-void tagged_union,
  whose IR size disagrees with its LLVM size -> verifySizes panic. Now mints a real
  .@"enum" (register_type kind 2 AND the metatype defineEnum).
- Bare `EnumType.variant` qualified construction of a payloadless variant wasn't
  supported (failed for hand-written enums too — the type name lowered to a Type
  value). Added in lowerFieldAccess via isPayloadlessVariant; payload-carrying
  variants keep their call form.

Examples: 0631 (graph + actual enum + reflection), 0632 (make_enum all-void),
0633/0634/0635 (namespaced / bare / multi-edge import of a minted type), 0187
(qualified variant construction). Unit tests added.

Parity 697/697 (gate OFF and -Dcomptime-flat).
2026-06-18 10:47:36 +03:00
agra
27bc301651 comptime VM: Phase 3 — type_kind + type_field_value readers (read side complete)
The last two read-only readers the metatype's type_info(T) needs, each backed by
a TypeTable query both the legacy handler and the VM call (no drift):

  type_kind(t: TypeId) -> i64            (kindCode; stable discriminant, total — never bails)
  type_field_value(t: TypeId, idx) -> i64 (memberValue; enum explicit value or ordinal)

kindCode codes (compiler-owned, stable): 0 other / 1 struct / 2 enum /
3 tagged_union / 4 tuple / 5 union / 6 array / 7 vector / 8 error_set.

With these, the READ side is complete: find_type + type_kind + type_field_count +
type_field_{name,type} + type_nominal_name + type_field_value cover everything
reflectTypeInfo reads — a comptime sx fn can fully reflect a struct/enum/tuple
into data with no #builtin.

Example 0630 reflects Color / WindowFlags(flags) / Point. VM unit test added.

Revised forward direction: the write side will be ONE register_type(info) fn that
branches on the kind in the compiler (subsuming define's per-kind dispatch), not a
per-kind register_struct.

Parity 691/691 (gate OFF and -Dcomptime-flat).
2026-06-18 09:47:23 +03:00
agra
d23e208430 comptime VM: Phase 3 — field-level reflection readers
Three more read-only compiler-API readers on the TypeId-handle shape, each backed
by a new TypeTable query that both the legacy handler and the VM call (no drift):

  type_nominal_name(t: TypeId) -> StringId     (nominalName; loud-bail for unnamed types)
  type_field_name(t: TypeId, idx: i64) -> StringId   (memberName)
  type_field_type(t: TypeId, idx: i64) -> TypeId     (memberType)

All loud-bail on out-of-range idx / no-member — no silent default. First multi-arg
compiler fns (callCompilerFn now reads arg 1 = idx); added Vm.argHandle/argTypeId
range-checked arg readers and moved find_type/type_field_count onto them. Names use
the type_* family to avoid colliding with the std metatype builtins (field_name /
type_name in core.sx); the new TypeTable.nominalName is distinct from the existing
typeName(id) display-string renderer.

Example 0629 reflects Pair { lo: Point; hi: Point } — each field name + the nominal
name of a field's type, #run-folded, VM-HANDLED natively. VM unit test added.

Parity 690/690 (gate OFF and -Dcomptime-flat).
2026-06-18 09:34:36 +03:00
agra
a9302a8b50 comptime VM: Phase 3 — find_type + type_field_count reflection readers
First read-only compiler-API reflection readers, bound the same way as the
intern/text_of seed (compiler_lib.bound_fns + Vm.callCompilerFn, native on flat
memory, no marshaling). A type handle is a plain u32 TypeId (like StringId), so
both stay clean scalar host-calls:

  find_type(name: StringId) -> TypeId          (TypeTable.findByName; unresolved/0 if absent)
  type_field_count(t: TypeId) -> i64           (new TypeTable.memberCount; loud-bail, no silent 0)

memberCount is the single source both the legacy handler and the VM read, so the
two paths can't drift. find_type returns a non-optional TypeId using the
unresolved(0) sentinel for not-found rather than ?Type — a Type value is
.any-typed (which the flat-memory VM does not represent) and an optional can't
cross the legacy<->VM eval boundary; unresolved is the project-blessed "no type"
marker.

Example 0628 chains intern -> find_type -> type_field_count (+ a not-found
lookup), folded at #run, VM-HANDLED natively. VM unit test added.

Parity 689/689 (gate OFF and -Dcomptime-flat).
2026-06-18 09:25:26 +03:00
agra
0367d96d9b comptime VM: host wiring, full corpus parity, build flag, Phase 3 seed
Phase 1.final of the flat-memory comptime VM — wire the host through it,
reach corpus parity, and gate it behind a build flag — plus the first
Phase 3 (compiler-API) step. Default OFF; legacy interpreter unchanged.

Host wiring + hardening:
- Machine accessors return error.OutOfBounds (no debug panic) on bad
  addresses; Frame.get/set bounds-check and bail (no panic) on a malformed
  operand ref (e.g. a ret Ref.none from an unresolved name).
- tryEval routed at both comptime call sites in emit_llvm — the const-init
  fold and the #run side-effect path — with per-eval legacy fallback;
  yields .void_val for void/noreturn entries. Both sites sx_trace_clear()
  before the legacy fallback so a partial VM run that pushed trace frames
  doesn't double-push on re-run.

VM coverage (all corpus const-inits except the inline-asm global):
- Implicit context materialized from the __sx_default_context global; the
  full allocator protocol runs on the VM (context.allocator.alloc ->
  call_indirect -> CAllocator thunk -> libc_malloc -> native flat malloc).
- Native libc memory builtins (malloc/calloc/free/memcpy/memmove/memset)
  on flat memory; f32 stored/loaded as the 4-byte single; signed sub-64-bit
  loads sign-extended; global_get (lazy + memoized); func_ref/call_indirect
  (func-ref encoded fid+1, 0 reserved for null); string/slice fat-pointer
  field access; is_comptime; the failable/error cluster (error_set tuples,
  trace_frame + native sx_trace_push/clear -> raise/catch/or + return traces).

Build flag + Phase 3 seed:
- -Dcomptime-flat (build_opts module) OR SX_COMPTIME_FLAT env enables the VM;
  zig build test -Dcomptime-flat runs the full corpus on the VM (688/0).
- intern/text_of serviced natively on flat memory via Vm.callCompilerFn
  (compiler_welded boundary) — the seed the rest of the compiler-API grows on.

Parity 688/688 gate ON and OFF. Unit tests added throughout. The
lowering-time #insert wiring was explored and reverted (lowering-time IR can
be malformed; full malformed-IR hardening is a prerequisite, deferred).
2026-06-18 08:27:58 +03:00
agra
b8f3d6fd78 comptime VM: flat-memory machine + executor + Reg<->Value bridge + tryEval
Phase 1 of the flat-memory comptime VM (current/PLAN-COMPILER-VM.md),
built standalone + unit-tested with the legacy interpreter still live and
the corpus untouched (688 green).

src/ir/comptime_vm.zig:
- Machine: one linear byte memory (comptime stack+heap) with a bump/stack
  allocator (mark/reset), scalar readWord/writeWord (1/2/4/8 LE) + byte
  views; addr 0 reserved as null_addr. Frame: a Ref-indexed register file
  (Reg = raw u64: immediate scalar bits OR an Addr). Target-aware layout
  comes from the type table, so cross-compilation stays correct.
- Vm executor over the SAME SSA IR, mirroring the legacy interp's scalar
  semantics (i64 wrapping/signed, f64). Ported: constants, arithmetic,
  comparison, logical, conversions, control flow (br/cond_br/ret + block
  params); structs (alloca/load/store/struct_init/get/gep at target
  offsets); tuples; arrays (index_get/gep, length); slices+strings as
  {ptr,len} fat pointers (const_string, data_ptr, subslice,
  array_to_slice, str_eq/ne, index-through-slice); optionals (pointer and
  {T,i1} shapes); payloadless enums; deref/addr_of; direct + recursive
  call over the shared flat memory (depth-guarded). The value model: a
  word for scalars/pointers, by-address for aggregates (a struct's value
  IS its Addr). Any unported op bails loudly (error.Unsupported + detail).
- Reg<->Value boundary bridge (valueToReg / regToValue) + tryEval, the
  hybrid-wiring entry point: run a comptime fn on the VM, return a legacy
  Value or null to fall back. Transitional, for the legacy interop edge.

Registered in the ir.zig barrel.
2026-06-17 19:29:50 +03:00
agra
18af8eb845 comptime-API: strip the byte-weld; pivot to a flat-memory comptime VM
The byte-weld (sx structs whose layout was validated to mirror the
compiler's Zig records) plus the serialization/marshaling bridge was the
wrong direction: it bolted a parallel layout regime and hand-built
byte-copies onto a comptime value model that fundamentally isn't bytes.

Strip the struct-weld machinery:
- compiler_lib.zig loses the type registry (weldStruct / bound_types /
  BoundType / FieldLayout / findType / SxField / LayoutMismatch /
  validateStructLayout); it is now just the intern/text_of function
  host-call bridge (kept as the Phase-3 compiler-call seed).
- nominal.zig loses validateWeldedStruct / weldedFieldOrderStr + the
  sd.abi == .zig validation call.
- Remove the struct-weld unit tests and examples 0625/0627 (welded
  structs) + 1183/1186 (weld-layout diagnostics).
- The #library / abi / extern syntax stays.

Record the new direction: a bytecode VM over flat, byte-addressable
memory so comptime values are native bytes (no weld/validation/marshal),
target-aware (preserves cross-compilation) and sandboxed. See
current/PLAN-COMPILER-VM.md (Phase 0 strip -> Phase 1 flat-memory value
model -> Phase 2 bytecode -> Phase 3 compiler-API on flat memory).
design/comptime-compiler-api.md gets a SUPERSEDED banner. Also drop the
"~500 lines / split the step" rule from CLAUDE.md.
2026-06-17 19:29:36 +03:00
agra
40d075ca98 compiler-API: welded structs by reflection + memory-order validation
Replace the explored byte-layout-override engine (offset-ordered LLVM structs /
weld plans / byte-blobs — all unnecessary) with a much simpler design: a welded
`struct abi(.zig) extern compiler { … }` is a bodied header declaring its fields
in the bound compiler type's MEMORY order. The compiler reflects the real Zig
type (field names via @typeInfo, offsets via @offsetOf, size via @sizeOf —
nothing hand-maintained) and validates the header matches, with loud diagnostics.

On pass it is an ordinary struct whose natural layout already equals the Zig
layout — no reorder, no padding, no index/remap tables, no special LLVM path — so
@ptrCast'ing it to the compiler's own type and dereferencing is byte-identical.
When types.zig shifts, the header stops matching and the developer gets a specific
message to fix it.

- compiler_lib.zig: weldStruct reflects field names and bakes bound_types fields
  in ascending-offset (memory) order; deleted computeWeldPlan/WeldPlan/WeldElement.
- nominal.zig validateWeldedStruct: precise diagnostics — field-not-found,
  wrong-field-order (+ expected memory order), type-layout (size) mismatch,
  total-size mismatch.
- Examples: 0627 (StructInfo in memory order, byte-identical, usable),
  1186 (source-order StructInfo -> wrong-field-order diagnostic); 1183 refreshed.
- Design doc + checkpoint updated.
2026-06-17 15:45:23 +03:00
agra
88c4cbcfa5 test harness: add -Dname to scope the corpus to specific examples
`zig build test -Dname=examples/0625-foo.sx[,examples/0626-bar.sx]` runs ONLY the
named example(s) — full repo-relative .sx paths, comma-separated (a leading `./`
is tolerated). Empty = run everything (unchanged default).

Why: a full `-Dupdate-goldens` re-runs and rewrites all ~690 snapshots, so one
flaky/host-divergent example (AOT links, cross-arch `target` examples) can clobber
a good snapshot. `-Dname` regenerates only the named example(s) and touches
nothing else. It also busts the cached test-run result — the corpus enumerates
.sx/expected files at runtime, so a bare snapshot edit alone is otherwise served
from cache.

- build.zig: new `name` option threaded onto corpus_paths.
- corpus_run.test.zig: `nameMatchesFilter` + a per-example skip in the run loop.
- CLAUDE.md: document the targeted-regen workflow under Snapshot integrity.
2026-06-17 14:55:06 +03:00
agra
cd5b958d19 comptime compiler-API: Phase 1 foundation + Phase 2.1 weld plan
Introduce the welded comptime `compiler` library (`#library "compiler"` +
`abi(.zig) extern compiler`), per design/comptime-compiler-api.md, and unify
`callconv(...)` into the new `abi(...)` annotation.

abi(...) replaces callconv(...):
- New ABI enum { default, c, zig, pure }; `abi(.c|.zig|.pure)` parses in the
  postfix slot before extern/export (and standalone). `kw_callconv` -> `kw_abi`.
- Migrated 52 sx files, the call-convention-mismatch diagnostic, and docs
  (readme/specs) from `callconv(.c)` to `abi(.c)`.

Phase 1 — welded compiler library (parse -> registry -> validation -> bridge):
- `abi(.zig) extern compiler` parses on fn decls (carries abi/extern_lib) and
  struct decls (StructDecl.abi/extern_lib).
- `#library "compiler"` is the comptime-only internal surface — never dlopen'd.
- src/ir/compiler_lib.zig: the binding registry (the safety boundary). `Field`
  welded to StructInfo.Field with layout baked from the real Zig type
  (@offsetOf/@sizeOf); `findType`/`findFn`. Welded structs are layout-validated
  at registration (field set + total size) as a header checked against the impl.
- Host-call bridge: a `fn abi(.zig) extern compiler` dispatches under the
  comptime interp to its registered Zig handler (intern/text_of round-trip),
  never dlsym. IR Function.compiler_welded; validated in declareFunction.
- Comptime-only enforcement: a runtime call to a welded fn is a clean
  build-gating error (emitCall), not an undefined-symbol link failure.

Phase 2.1 — byte-layout weld foundation:
- Decision: full byte-layout weld (sx struct laid out byte-identically to the
  bound Zig type). Registered StructInfo (first non-natural / Zig-reordered
  layout). `computeWeldPlan` — pure offset-ordered element plan + padding +
  sx-field->LLVM-element remap; unit-tested. Emit/interp wiring is the next
  sub-step (2.2+, see current/CHECKPOINT-COMPILER-API.md).

Examples: 0625/0626 (welded struct + fn round-trip), 1183/1184/1185
(layout-mismatch, unexported-fn, runtime-call diagnostics).
2026-06-17 13:31:11 +03:00
agra
d87d86df8a feat(metatype): comptime-eval generic type-fn body locals
A generic ($T) -> Type type-fn comptime-evaluated only its return
EXPRESSION, so a local declared before the return ('vs := …; return
make_enum(…, vs)') was unresolved. Now a body with a prelude (statements
before the return) has its full body evaluated: createComptimeFunction-
WithPrelude lowers the pre-return statements into the comptime function's
scope before the return expr, so the locals resolve.

- comptime.zig: createComptimeFunctionWithPrelude (prelude stmts +
  expr); evalComptimeTypeBody (extract prelude + return expr, scan the
  whole body for declare() forward types); runComptimeTypeFunc factored
  out of evalComptimeType (shared bail/declare-never-defined handling).
- generic.zig: route a type-fn body WITH a prelude through
  evalComptimeTypeBody; no-prelude bodies stay on evalComptimeType (zero
  change for RecvResult/TryResult etc.).

Non-generic builders (whole body already evaluated) and the List-growth
path are unaffected. Suite green (684).
2026-06-17 07:40:09 +03:00
agra
9f3f746c4b feat(metatype): widen type_info/define to tuple types
TypeInfo gains a `tuple(TupleInfo) variant (TupleInfo{elements: []Type},
positional/unnamed) — completing the reflect/construct triad with enum
and struct.

- meta.sx: TupleInfo + `tuple TypeInfo variant.
- interp: reflectTypeInfo builds .tuple (tag 2) as bare type_tag elements
  (no name pairs); defineType dispatches tag 2 -> defineTuple, which
  decodes []Type and completes the declare slot as a structural .tuple
  via replaceKeyedInfo (kind change). Tuples are structural so the
  declared name is vestigial, but the slot is still completed in place so
  define returns the handle (consistent with enum/struct).
- call.zig: the lower-time type_info guard now admits .tuple.

define(declare("P"), .tuple(.{elements=.[i64,f64]})) builds a tuple, and
define(declare("T"), type_info((i64,bool,f64))) round-trips one. Suite
green (683).
2026-06-17 07:05:55 +03:00
agra
aaac019715 feat(metatype): widen type_info/define to struct types
TypeInfo gains a `struct(StructInfo) variant (StructField{name,type});
the metatype system now reflects AND constructs structs, not just enums.

- meta.sx: StructField / StructInfo / `struct TypeInfo variant.
- interp: reflectTypeInfo builds .struct (tag 1) for a source @"struct";
  define dispatches on the TypeInfo tag (defineType) -> defineEnum (0) /
  defineStruct (1). defineStruct mirrors defineEnum (dup-field-name check
  included) but completes the declare slot AS a struct via replaceKeyedInfo
  (a kind change re-keys the intern map; updatePreservingKey asserts no
  key change, true only for the enum path).
- call.zig: the lower-time type_info guard now admits @"struct".

define(declare("P"), .struct(.{ fields = .[ … ] })) builds a struct, and
define(declare("C"), type_info(SrcStruct)) round-trips one. Suite green
(682); enum path (0619) unchanged.
2026-06-17 06:54:17 +03:00
agra
14f30f341c fix(metatype): reject declare() never completed by define()
A bare declare("X") with no define left a zero-field nominal slot that
panicked at codegen (verifySizes: llvm_size != ir_size). evalComptimeType
now detects a zero-variant tagged_union result and emits a clean
build-gating diagnostic naming the type — a zero-variant enum is never a
legitimate construction result (defineEnum rejects empty variant lists
too). Self-reference (a declared slot completed by define) is unaffected.
2026-06-17 06:29:23 +03:00
agra
b2db2c54ed fix(metatype): reject duplicate variant names in define
Two same-named variants in a constructed enum silently succeeded —
construction (.a) and matching would ambiguously pick one. defineEnum
now bails when a variant name repeats, naming it. The name is dynamic so
it sets last_bail_detail directly (bailDetail takes a comptime string);
evalComptimeType renders it as a build-gating diagnostic.
2026-06-17 05:22:23 +03:00
agra
d22037c4a7 fix(interp): comptime subslice over non-string aggregates
`arr[lo..hi]` at comptime bailed for any non-string base — the interp's
.subslice op only handled string-backed values. Worse, the open-ended
`hi` came from a .length op that misread a 2-element array as a {ptr,len}
fat pointer (returning the 2nd element, not the count), so even lo/hi
weren't valid ints.

Fix, interp-only (runtime already handles arrays via LLVMTypeOf):
- Thread the base operand's IR type onto the Subslice op (base_ty); the
  interp uses it to tell a bare array (elements = aggregate fields) from a
  {data,len} slice (elements in the data field) — indistinguishable by
  Value shape alone.
- Fold an open-ended slice's hi to the array's static length for fixed
  arrays at lower time (runtime emitLength folds the same constant, so the
  IR result is unchanged — no snapshot churn — but the comptime interp no
  longer hits the ambiguous .length op).
- subsliceElements() resolves the element list (array/slice, inline or
  slot_ptr-backed) and subslice returns a proper {data,len} slice value.

Suite green (678), no .ir changes.
2026-06-17 05:11:33 +03:00
agra
37ec3da8cb fix(0140): surface comptime type-construction bail as a diagnostic
evalComptimeType did `interp.call(...) catch return null`, dropping the
interp's last_bail_detail; callers poisoned to .unresolved with no
diagnostic, so the sentinel reached LLVM emission and panicked
("unresolved type reached LLVM emission"), or hid behind a downstream
cascade.

Clear last_bail_detail before the call; on the catch emit a build-gating
.err at the construction expr's span ("comptime type construction
failed: {detail}", mirroring the #run surfacing in emit_llvm.zig), then
return null to keep the .unresolved poison — now gated by a real message
so no unresolved type reaches emission unannounced.

Empty-variant define now prints 'comptime define(): enum has no
variants' and exits 1 (no panic); make_enum-style computed-slice
failures show their root reason at the construction site.
2026-06-17 04:31:38 +03:00
agra
1ffda415c2 feat(metatype): implement type_info($T) reflection (enum round-trip)
type_info reflects an enum / tagged-union INTO a TypeInfo value — the
inverse of define's decode — so define(declare(n), type_info(T)) mints
a byte-identical copy with NO literal variant list.

- inst.zig: new BuiltinId.type_info (comptime-only, like declare/define).
- lower/call.zig: replace the 'not yet implemented' bail. Resolve $T at
  lower time, reject non-enum/non-tagged-union loudly with a good span,
  emit callBuiltin(.type_info, [const_type], TypeInfo).
- interp.zig: reflectTypeInfo builds the exact nested-aggregate Value
  defineEnum decodes — variant {name,payload}, slice {data,len}, EnumInfo
  {variants}, TypeInfo {tag0, EnumInfo}. tagged_union reflects field.ty
  (tagless already void); payloadless `enum` reflects void per variant.
- emit: unchanged — type_info is always comptime-evaluated, the existing
  comptime-only else arm (shared with declare/define) never fires.

0619 turns green: a source enum (circle:f64 / rect:i64 / empty) reflected
and reconstructed, constructs and matches like the original.
2026-06-16 22:52:53 +03:00
agra
2f0905b407 fix(0139): reject by-value self-referential types loudly (was a segfault)
A nominal aggregate that contains itself (or a mutual peer) BY VALUE has no
finite layout and infinite-recursed typeSizeBytes into a stack overflow —
for SOURCE enums/structs as well as comptime-constructed types.

New `checkInfiniteSize` pass (lower/decl.zig, Pass 1g — after type
registration, before body lowering): walks the by-VALUE containment graph
(pointer/slice/optional payloads break the cycle, so `*Self` stays valid);
on a back-edge it emits a loud diagnostic — "type 'X' is infinitely sized
(it contains itself by value); use a pointer ('*X') to break the cycle" —
and poisons the offending field to `.unresolved` so sizing can't recurse
before the build halts on the error. Covers source + declare/define types,
direct + mutual recursion.

examples/1178 locks the diagnostic; issue 0139 marked RESOLVED. This also
completes METATYPE PLAN F5's by-value-self-reference rejection. Full suite
green (675).
2026-06-16 22:24:31 +03:00
agra
7a9db03bcc green(metatype): declare(name) + self-reference (recursive enums via *Name)
declare now takes the type's NAME — `declare(name) -> Type` — because the
compiler needs it at compile time to register the forward type, which is
what makes self-reference resolve. EnumInfo drops `name` (it lives on
declare now); define completes the handle's body in place (the slot is
already named).

Self-reference mechanism (evalComptimeType): before lowering a comptime
type expression, preregisterForwardTypes scans it (and a called ctor fn's
body) for `declare("Name")` calls and registers each as an empty forward
nominal type AND binds it as a type alias. The alias is essential: a
`Name :: ctor()` decl makes `Name` a const_decl author, so a `*Name`
self-reference resolves through the forward-ALIAS path
(type_aliases_by_source), which a bare findByName registration doesn't
satisfy. With both in place `*Name` resolves to the forward slot at lower
time; the interp's declare returns that same slot; define fills it.

  List :: make_list();
  make_list :: () -> Type {
      h := declare("List");
      return define(h, .enum(.{ variants = .[
          EnumVariant.{ name = "cons", payload = *List },
          EnumVariant.{ name = "nil",  payload = void } ] }));
  }

Verified: cons/nil construct + match (direct and through the pointer),
multi-node list traversal via a recursive `count(*List)`. meta.sx
RecvResult/TryResult + examples 0614/0615/0617 updated to declare(name);
full suite green (673).
2026-06-16 22:02:48 +03:00
agra
12e2ff7ef4 docs+rename: erase the reify name everywhere — stream is METATYPE
The compiler concept is declare/define (comptime type construction); the
old "reify" framing is gone from the entire repo.

- Rename: PLAN-REIFY → PLAN-METATYPE, CHECKPOINT-REIFY → CHECKPOINT-METATYPE,
  PLAN-POST-REIFY → PLAN-POST-METATYPE (both rewritten around declare/define);
  examples 0614/0615/0617 → comptime-metatype-* (+ their expected/ triplets),
  headers rewritten.
- Scrub reify from design/execution-evolution-roadmap.md (§7 step 3 contracts,
  §8.1, §9 decisions, §10 gates) → declare/define / comptime type construction.
- core.sx prelude pointer + parser.test.zig surface lock updated to the
  declare/define builtins (define(handle, info) -> Type; EnumInfo.name).

No behavior change; renamed examples match their renamed snapshots. Full
suite green (673), all unit tests pass. Zero `reify` tokens remain in
src/docs/sx/examples.
2026-06-16 21:23:05 +03:00
agra
5f2419854e green: erase the sx reify sugar — declare/define are the only constructors
Per the directive to strip reify entirely: the sx `reify(info)` one-shot is
removed. `define(handle, info)` now RETURNS the (completed) handle, so the
one-shot constructor chains as a single expression:
    T :: define(declare(), .enum(.{ name = "T", variants = ... }));

- meta.sx: drop reify; RecvResult/TryResult use `define(declare(), …)`.
- interp .define returns the handle type_tag (was void); call.zig lowers it
  with `Type` result and sets the info arg's target type to TypeInfo so the
  intercepted call still infers the `.enum(…)` literal.
- returnExprMintsType: a type-fn body that returns `define(…)` (or a bodied
  non-generic Type-returning sx helper) is comptime-evaluated.
- examples 0614 (direct) + 0615 (type-fn) use `define(declare(), …)`.

Full suite green (673). Files/docs still carry the old reify naming — the
rename sweep is the next commit.
2026-06-16 21:12:32 +03:00
agra
8ae655687a green(reify): type-fn bodies comptime-evaluated; reify fully removed from the compiler
Second slice of the re-architecture — the compiler now has ZERO type-
construction code beyond declare/define.

- instantiateTypeFunction: a type-fn body returning a computed Type (a call
  to a non-generic, bodied, Type-returning fn) is comptime-evaluated with the
  type bindings active, then renamed to the mangled instantiation name for
  identity (renameNominalType). Replaces the old reify-call pattern-matching.
- DELETED: reifyType (lower/nominal.zig), findReturnReifyCall (lower/generic.zig),
  and the stale inline-position reify gate in resolveTypeCallWithBindings.
- evalComptimeType (was evalComptimeTypeNamed): pure eval, no rename; the
  type-fn caller renames explicitly. renameReifiedType → renameNominalType.
- The TYPE NAME now travels in the data: EnumInfo gains `name`, and define()
  names the slot from it (the compiler derives no name from a binding LHS).
  examples/0614/0615 carry `name = "..."`; RecvResult/TryResult set it too.
- field_type stays a reflection #builtin (reads a type); only construction
  moved out. All reify mentions stripped from compiler source.

examples 0614/0615/0617 run on the floor. Full suite green (673).
2026-06-16 21:03:16 +03:00
agra
442a70b8c9 green(reify): declare/define floor — reify is sx; E :: reify(...) comptime-evaluated
First slice of the re-architecture. The compiler gains two comptime
type-construction builtins — declare() (mint an empty/undefined nominal
slot) and define(handle, info) (decode a TypeInfo VALUE + complete the
slot) — executed by the interpreter against a new `mint` TypeTable handle
(setMintTable). reify becomes PLAIN sx in meta.sx:
  reify :: (info) -> Type { h := declare(); define(h, info); return h; }

`E :: f(...)` where f is a non-generic Type-returning fn (reify, and later
make_enum) is now comptime-evaluated via evalComptimeTypeNamed: wrap the
call in a throwaway comptime fn, run it through the interp with the mint
table enabled so declare/define mint the type, read back the type_tag, and
rename the anonymous slot to the binding name. The compiler has ZERO reify
knowledge at the decl site — the old `E :: reify` hook is deleted.

examples/0614 (inline reify) now runs on this floor. Full suite green (673).

INTERMEDIATE: reifyType + findReturnReifyCall still serve the type-fn path
(0615/0617) and will be deleted in the next slice (type-fn body
comptime-eval), after which the compiler has no reify code at all.
2026-06-16 20:39:02 +03:00
agra
ac8c689518 green(reify): field_type($T, i) -> Type over the type table
REIFY Phase 2.1. fieldTypeOf (lower/generic.zig, re-exported on Lowering)
returns the i-th member type of T: struct field / tagged-union + union
variant payload (.void for a tagless variant) / tuple element / array +
vector element. Out-of-range and memberless types poison to .unresolved
with a loud diagnostic (never a silent default). Wired into
resolveTypeCallWithBindings (replacing the Phase-2 bail); since it folds
to a TypeId at lower time it composes inside type_eq / type_name / any
type-arg slot.

examples/0616 green: struct fields (name via field_name + type via
field_type), type_eq fold, tagged-union payloads incl. quit -> void.
Suite green (672 examples, 447 unit).

type_info($T) -> TypeInfo (reflect into a value, inverse of reify) is
NOT done — still bails loudly; it's the larger Phase 2.2 step (widen the
TypeInfo data model + comptime value construction). Plan/checkpoint updated.
2026-06-16 19:06:57 +03:00
agra
18a4f9dd54 green(reify): type-fn over reify memoizes by mangled name (identity)
REIFY Phase 1.1 (Phase 1 complete). instantiateTypeFunction detects a
type-fn body that returns reify(...) (findReturnReifyCall) and routes it
to reifyType under the instantiation's name — mangled for inline use,
the alias name for `Foo :: Box(i64)` — with the type-arg bindings active
so reify payloads (`payload = T`) resolve against the instantiation args.
Placed before the general case, whose resolveTypeWithBindings would
route the reify call to the inline-position loud bail.

Registering under the mangled name lets the top-of-instantiation cache
return the SAME TypeId on a second instantiation, so Box(i64) resolved
at two independent sites is ONE type (Contract 1). examples/0615 green
(build()->consume() cross-site + `b : Box(i64) = .none`). Suite green
(671 examples, 447 unit).
2026-06-16 18:54:11 +03:00
agra
353109206b green(reify): implement reify(.enum) — mint a flat enum from TypeInfo
REIFY Phase 0.2 (Phase 0 complete). Lowering.reifyType (lower/nominal.zig)
reads the flat-enum TypeInfo literal off the AST, synthesizes an
ast.EnumDecl, and feeds it through the SAME type_bridge.buildEnumInfo
path source enums use — so the minted type is byte-identical to a
hand-written `enum { value: i64; closed; }` and flows through enum
codegen (layout / construct / match) UNMODIFIED (Contract 2).

Wired at the `E :: reify(...)` const-decl hook in lower/decl.zig
(replacing the Phase-0.0 loud bail). Unsupported argument shapes bail
loudly via reifyBail — never a silent default. The generic.zig inline
reify path now reports it's only supported in a `::` binding (Phase 0).

examples/0614 green: reify a {value: i64, closed} enum, construct
.value(3) and .closed, match both -> "value 3" / "closed". Full suite
green (670 examples, 447 unit).
2026-06-16 18:32:05 +03:00
agra
b25a2f60d6 feat(parser): reserved keyword as member name after .
After a leading `.` (enum literal `.enum`, field access `x.enum` /
`E.struct`, match arm `case .enum:`) a reserved keyword is unambiguously
the member/variant NAME — the dot rules out the keyword reading — so no
backtick escape is needed. A declaration of such a variant still needs
the backtick (enum { `enum: i64 }), since the decl site has no dot.

Adds Parser.dotMemberName() (identifier OR identifier-shaped keyword)
and routes the leading-dot enum-literal and postfix field-access sites
through it. readme updated. The reify example 0614 now uses the cleaner
reify(.enum(...)) spelling (still xfail — reify lands next commit).
2026-06-16 18:22:21 +03:00
agra
81669c72b7 lock(reify): meta.sx surface + bodyless #builtin decls + loud bails
REIFY Phase 0.0. Add the comptime type-metaprogramming surface as the
on-demand module modules/std/meta.sx (NOT the prelude — declaring its
data types in always-loaded core.sx interns them into every module's
type table and shifts every .ir snapshot):

  - EnumVariant / EnumInfo / TypeInfo data types. TypeInfo's variant uses
    the backtick raw escape `enum so it reads as the keyword.
  - reify / type_info / field_type as bodyless #builtin decls.

Each builtin bails LOUDLY when reached unimplemented (no silent default):
  - reify(...) in a :: type-alias position -> decl.zig .call branch
    (also the Phase 0.2 construction hook); poisons the alias .unresolved.
  - reify / field_type in any other type position ->
    generic.zig resolveTypeCallWithBindings.
  - type_info(...) in expression position -> call.zig tryLowerReflectionCall.

Unit test src/parser.test.zig (registered in root.zig) locks that the
decls parse. zig build test green (447 unit, 669 examples).
2026-06-16 17:44:19 +03:00
agra
b6a7378af4 feat(dist): bundled-zig link backend for hermetic macOS/Linux/Windows builds
Drive a bundled `zig` as `zig cc` for the AOT link step, supplying lld + CRT
+ libc (musl/glibc/mingw) so `sx build` produces native binaries with no host
toolchain. Default Linux output is static musl (portable-anywhere).

- src/zig_backend.zig: discover zig ($SX_ZIG / bundled-next-to-exe / PATH);
  bundled-vs-PATH provenance gates auto-activation.
- src/target.zig: selectZigLinker + emitZigLinkArgv + zigTargetTriple, dispatched
  before the per-OS branches; macOS/Linux/Windows in scope.
- src/ir/emit_llvm.zig: LLVMNormalizeTargetTriple so vendor-less zig triples
  (e.g. x86_64-windows-gnu) parse to the correct OS/object format (COFF not ELF).
- src/main.zig: --self-contained / --no-self-contained; linux-musl, linux-musl-arm,
  windows-gnu shorthands; de-vendor linux/linux-arm to match the corpus runner.
- examples/1660: Windows Win32 print-42 + exit(0) via kernel32 (ir-only off-Windows).

Auto-activates only for a bundled zig; a PATH-only zig engages under
--self-contained, so native dev/CI builds are never silently rerouted.

Docs: readme Cross-Compilation, design/bundled-zig-link-backend-design.md, current/PLAN-DIST.md.
2026-06-16 15:56:06 +03:00
agra
066ba54346 feat(asm): portable symbol refs — auto-inject :c operand modifier
A `%[name]` that references a symbol ("s") operand without an explicit
modifier now lowers to `${N:c}` (LLVM 'bare constant — no punctuation')
instead of `${N}`. This makes `bl %[fn]` / `call %[fn]` portable across
targets with no per-arch knowledge: x86 would otherwise render `$cb`
(an invalid call target, requiring a hand-written `:P`); aarch64 is
unaffected. Verified `:c` is equivalent to `:P` for x86-64 calls (both
emit R_X86_64_PLT32), and correct for branch targets, RIP-relative
addressing, and `$`-prefixed absolute immediates.

renderAsmTemplate injects `:c` only for symbol operands lacking an
explicit modifier (asmNamedIsSymbol helper); an explicit `%[name:X]`
still wins (escape hatch). x86 example 1659 drops its `:P` for the same
plain `%[fn]` as aarch64 1656. Snapshots regen to `${N:c}`. zig build
test green (668 corpus, 446 unit).
2026-06-16 09:04:23 +03:00
agra
10f4137cbd feat(asm): symbol operands ("s") — direct call/branch to a function
A `"s"` input operand feeds a function/global symbol; the template's
%[name] emits the platform-mangled name, so `bl %[fn]` / `call %[fn]`
branches DIRECTLY to it (PC-relative, no register load — one fewer
indirection than register-indirect `blr`).

Lowering: an `"s"` input lowers its RHS normally (a function name →
`ptr @fn`); the rejection added last commit is removed. Emit: a symbol
operand is passed with its OWN llvm type (LLVMTypeOf) and no coercion —
the function value is a `ptr`, and the old coerce-to-register-int path
mistyped it and failed the verifier. New asmIsSymbol helper.

Verified on aarch64: examples/1656 (sx → asm → bl _cb → sx → 42); the
emitted asm is a direct `bl <_cb>` (objdump-confirmed), IR constraint
`...,s,...`(ptr @cb). Flipped 1656 from the rejection lock to a runnable
aarch64 example. zig build test green (665 corpus, 446 unit).
2026-06-16 08:24:53 +03:00
agra
c187122531 test(asm): reject symbol "s" operands cleanly + lock (symbol-op prep)
A symbol operand (constraint "s") feeds a function/global symbol whose
mangled name the template emits — enabling a DIRECT `bl %[fn]` (one
fewer indirection than register-indirect `blr`). Until now `"s" = fn`
fell through to emit and produced an LLVM-verifier crash (param type
mismatch). Reject it at lowering with a clear diagnostic instead, and
lock that with examples/1656-platform-asm-symbol-operand.sx. The next
commit implements it and flips the example to run (→ 42).
2026-06-16 08:19:18 +03:00
agra
cb6c032c58 feat(asm): indirect-memory =*m place outputs
Implements indirect-memory (`=*m`) `-> @place` outputs — the last
substantive asm feature. Unlike a write-through `=` output (which
returns a value that is then stored), an indirect output passes the
place ADDRESS to the asm and the asm writes through it; there is no
return slot.

emitInlineAsm:
  - indirect outputs are excluded from the LLVM return type;
  - their pointer is passed as an opaque `ptr` call arg, placed FIRST
    (the arg-consuming constraint order is: output-section indirect
    pointers, then inputs, then read-write tied seeds);
  - each indirect arg gets an `elementtype(T)` call-site attribute
    (required in the opaque-pointer era), T = the pointee type;
  - the store-back loop skips indirect outputs (already written).
New asmIsIndirect helper. Lowering stops rejecting `*` (constraint kept
verbatim; `=*m` reaches the constraint string as-is). asmOperandIndex
is unchanged — indirect outputs still count as operands, so `%[name]`
${N} numbering holds.

Verified by running on aarch64: store-through-pointer (str x9, %[out]
→ 42, IR `=*m,~{x9}` with `ptr elementtype(i64)`) and a mixed case
(indirect + value output + input → `=*m,=r,r`, indirect ptr arg first,
${0}/${1}/${2} correct). 1652 flipped from the rejection lock to a
runnable aarch64 example (ir-only elsewhere). zig build test green
(661 corpus, 446 unit).
2026-06-16 07:09:17 +03:00
agra
2a954ceeb6 fix(0138): diagnose @scalar-const address-of (no storage)
A scalar `::` constant folds to its value and has no storage. The
unary `.address_of` lowering (src/ir/lower/expr.zig) skipped the
alloca path (is_alloca == false) and resolveGlobalRef (scalar consts
get no storage global), falling through to the generic addr_of arm,
which reinterpreted the folded value as a pointer:
`inttoptr (i64 <value> to ptr)`. That wild pointer segfaulted on
deref and emitted invalid stores for inline-asm `-> @const`.

Diagnose instead, in the address_of(identifier) path: a non-alloca,
non-ref-capture, non-pack-elem scope binding (local scalar const) and
a module_const_map name not backed by storage (module scalar const)
both report "cannot take the address of constant '<name>' — a scalar
'::' constant has no storage …" and return a placeholder Ref. Chose
diagnose over materializing read-only storage (consistent with the
fold-only scalar model). Array/struct consts keep real storage and
stay addressable (@K/@LIT unchanged).

Also gives the ASM stream's planned output-to-const rejection for
free — asm `-> @const` lowers through the same path. Regression:
examples/1177-diagnostics-addr-of-const-rejected.sx. Resolves 0138.
2026-06-16 06:29:36 +03:00
agra
4128416d48 feat(asm): read-write + place outputs
Implements read-write (`+r` / `+{reg}`) `-> @place` outputs. LLVM has
no `+` constraint, so a read-write place lowers to:

  - an output `=` constraint (return slot, stored back through the
    place after the call), with the leading `+` rewritten to `=`; plus
  - a TIED input constraint (the decimal index of that output) appended
    after the regular inputs, seeded with the place's loaded value
    passed as a call arg.

Tied inputs are appended last so existing operand indices (%[name] ->
${N}) are undisturbed; asmOperandIndex stays correct. Lowering no longer
rejects `+` (indirect `*` still rejected). emitInlineAsm grows the
arg/param arrays by the rw count, loads each seed, and emits the tied
constraint.

Verified by running: increment-in-place (41 -> 42) and a mixed case
(rw place + regular input + value output) producing the textbook
"=r,=r,r,0" constraint with correct ${N} indices. 1650 flipped from
the rejection lock to a runnable aarch64-pinned example (ir-only
elsewhere). zig build test green (658 corpus, 446 unit).
2026-06-15 23:07:38 +03:00
agra
967005621a feat(asm): Phase 2 — -> @place write-through outputs
An asm result can be STORED through a place (a local / struct field) instead of
returned; the place output does not join the result tuple.

- parser.zig: `-> @place` parses `@place` as an ordinary address-of expression
  → an out_place operand (the in-function form; reuses the existing `@` prefix).
- inst.zig: AsmOperand gains out_ty (the output slot's value type) so emit can
  build the combined return struct without re-deriving from Inst.ty.
- lower/expr.zig: out_place operand = the lowered @place address, out_ty = the
  pointee. Read-write (`+`) and indirect-memory (`*`) constraints rejected loudly
  (not yet implemented) rather than miscompiled.
- ops.zig emitInlineAsm: the LLVM return type is built from ALL outputs
  (out_value + out_place); after the call, out_place slots are stored through
  their address and out_value slots rebuild the sx result. Fast path when there
  are no place outputs (the struct return IS the result — pure-value asm IR
  unchanged).

Verified: write-to-local (42), struct field, mixed value+place (v=10 b=20), `+`
rejected. Locked with 1649-platform-asm-place-output (mixed, runs on aarch64).

zig build test green (657 corpus, 446 unit).
2026-06-15 22:47:34 +03:00
agra
4d75b9323c feat(asm): Phase F — global (module-scope) asm
A top-level `asm { "tmpl", };` block (template only) lowers to LLVM `module asm`;
a lib-less `extern` declaration calls into the symbols it defines (the import
direction reuses the existing C-FFI extern path — no new surface).

- ast.zig: asm_global node (AsmGlobal { template }).
- parser.zig: parseAsmGlobal, dispatched from parseTopLevel on kw_asm — rejects
  `volatile` and any operands/clobbers (template only). The in-function asm
  expression form stays in parsePrimary.
- module.zig: Module.global_asm list; lower/decl.zig captures each template in
  lowerMainAndComptime (the real top-level pass — lowerDecls is dead for
  top-level); emit_llvm.zig emit() appends each via LLVMAppendModuleInlineAsm in
  source order.
- the new node forced asm_global arms in sema.zig (analyzeNode +
  findNodeAtOffset) and semantic_diagnostics.zig (checkBindingNames).

Verified end-to-end: an aarch64 `_my_add` global routine, called via `extern`,
returns 42 — AOT only (the ORC JIT doesn't link module-asm symbols; global-asm
symbols live in the final linked binary). Locked with 1648-platform-asm-global
({ "aot": true, "target": "macos" } → AOT build+run on aarch64, ir-only else).

zig build test green (656 corpus, 446 unit).
2026-06-15 22:22:29 +03:00
agra
d3c6ffed5a feat(asm): Phase E — multi-output asm returns tuples
Replaces the N>1 "Phase E" bail with a shared asmResultType helper (lowering +
inferType) that derives the result type from the out_value operands: 0→void,
1→T, N→a named tuple (fields named via the §II.5 effective-name rule).

Key realization: toLLVMType(tuple) already produces a literal struct {T1,…,Tn} —
exactly what LLVM's multi-output inline asm returns — so emit needs NO change.
Building the op with a tuple result type makes the asm call return the struct,
which IS sx's tuple value (destructured by the normal tuple_get path).

inferType's .asm_expr arm now also delegates to asmResultType (single owner), so
`return asm`, `x := asm`, and `q, r := asm` all agree on the type.

Verified end-to-end on aarch64: split(0x1234)→(lo=52,hi=18), a udiv/msub
divmod→(3,2). IR: `call { i64, i64 } asm "divq ${4}",
"={rax},={rdx},{rax},{rdx},r,~{cc}"(…)` → extractvalue → tuple.

1640 → the x86_64 multi-output IR lock (ir-only); 1647 → a multi-output example
that runs on aarch64.

zig build test green (655 corpus, 446 unit).
2026-06-15 21:55:38 +03:00
agra
5a5e04c6d5 feat(asm): Phase C.1 + D — inline asm codegen (runs end-to-end)
lowerAsmExpr stops bailing and builds the inline_asm op: resolves each operand's
effective name (§II.5 — explicit [name] else the {reg} pin), interns
template/constraints/clobbers, lowers input Refs, derives the result TypeId
(0→void, 1→T). Adds the last deferred validation (every %[name] must name an
operand). Multi-output (N>1) bails with a named "Phase E" diagnostic.

emitInlineAsm (backend/llvm/ops.zig) ports Zig's airAssembly: assembles the LLVM
constraint string (outputs → inputs → ~{clobber}, ',' → '|'), rewrites the
template (%[name]→${N}, %%→%, $→$$, %=→${:uid}), then LLVMGetInlineAsm +
LLVMBuildCall2 (AT&T dialect). Dispatch wired in emit_llvm.zig (replacing the C.0
@panic tripwire).

inferType gains an .asm_expr arm (expr_typer.zig) so a bare `x := asm {…-> T}`
binding types correctly — without it the binding inferred .unresolved and
silently produced 0.

llvm_shim.c: LLVMInitializeNativeAsmParser() — the JIT must assemble inline asm
at run time.

Verified end-to-end on the aarch64 host: `mov`/`add` with register-class inputs
and a value output run (exit 42/99), `nop volatile` runs (exit 0). IR is
textbook: `call i64 asm "add ${0},${1},${2}", "=r,r,r"(…)`.

Locked with 1645 (aarch64 add, runs; ir-only on non-aarch64) + 1646 (:= binding).
Updated 1640 (now Phase-E bail) + 1642 (now runs).

zig build test green (654 corpus, 446 unit).
2026-06-15 21:39:54 +03:00
agra
6c08de8ec1 feat(asm): Phase C.0 — add inline_asm IR op (lock, no behavior change)
Adds the `inline_asm: InlineAsm` opcode to the IR Op union (inst.zig): interned
template + operand list (role/name/constraint/operand) + interned clobber names
+ has_side_effects; the result rides on Inst.ty (void / scalar / tuple).

The new variant forces coverage in the exhaustive Op switches:
- interp.zig: loud bailDetail — inline asm is never comptime-evaluable.
- print.zig: an IR-dump arm.
- emit_llvm.zig: a @panic TRIPWIRE — emit lands in Phase D, and until then
  lowerAsmExpr still bails, so no inline_asm op is ever created. Reaching emit
  would mean lowering switched over before emit was ready; crash loudly rather
  than miscompile.

No behavior change: lowering still bails, the op is constructed only in the new
`inline_asm op shape` unit test (inst.test.zig).

zig build test green (652 corpus, 446 unit).
2026-06-15 21:00:12 +03:00