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.
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).
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).
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.
Records the current state (read side, write side P3.3, lowering-time hardening +
wiring + zeroed context P3.4) and pins the next focused step: a dedicated Type
builtin TypeId (8B) distinct from .any (16B box) — ~123 .any refs across ~25 files,
a cross-cutting change to run as its own session. Paused here at a clean, green
boundary (697/697 both gates) per the decision to not rush it.
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.
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.
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).
Dual-path + emit-time legacy fallback are transitional scaffolding only; the VM
must reach parity at BOTH comptime sites (emit time AND lowering time), after
which the -Dcomptime-flat flag, the fallback, and interp.zig are all removed.
We do not ship both evaluators permanently.
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).
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).
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).
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).
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).
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.
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.
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.
`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.
Add the COMPILER-API stream to CLAUDE.md's session-start router and a
`## ⏯ Resume` block to CHECKPOINT-COMPILER-API.md (next action = sub-step 2.2,
read order, build/verify, and the cross-arch snapshot-regen gotcha).
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).
#library already lexes/parses (library_decl node); extern/export are
keywords. Phase 1 new work pinned to concrete sites: parser (extern(.zig)
postfix at the #builtin/#compiler positions), AST binding field,
compiler_hooks.zig as the registry, types/llvm layout emission, host_ffi
comptime bridge. First testable sub-step: extern(.zig) <lib> parses on a
fn decl.
Unified sx<->compiler binding that subsumes the metatype declare/define
primitives AND the #compiler struct attribute. A named 'compiler' library
exposes the compiler's real types (layout-welded via extern(.zig), offsets
queried from the Zig type at compiler-build time + a build-time equality
assertion) and functions (comptime-only, host-call bridged). declare/
define/type_info become sx library code over register_*/find_type; the
projected meta.sx TypeInfo + hand marshaling are deleted; BuildOptions
migrates onto it and #compiler is removed. Includes the safety boundary
(curated export list, guarded mutators, comptime-only), the honest limit
(the ordering law stays, but stops leaking as 'weird stages' — dissolving
the 0141 class), a phased suite-green build order, and the open risks
(union(enum) welding, optional fields, LLVM offset emission).
Wired a minimal deferral (eval at a new Pass 1c' after the CAllocator
thunks exist) — the List repro STILL bailed with struct_get, and it
destabilized examples/0620. So deferring past the thunks isn't the cause
of the wrong IR; the field-access lowering only emits struct_gep at
body-lowering/emit time. No single pass slot satisfies both 'body lowers
correctly' and 'layout ready before use'. Pivot to Direction 1 (robust
*Struct field-access lowering). Experiment reverted; tree clean.
Instrumentation shows List.append lowers list.len/list.cap to struct_gep
(correct) at #run/emit time but struct_get (wrong, value access on a *T
receiver) at scanDecls/metatype time — same source, different IR. The
function IS lowered both ways, just to wrong IR at scanDecls due to
incomplete generic-instantiation context. So an interp-side lazy-lower
hook can't fix it (IR is wrong before the interp runs); the fix is either
robust field-access lowering or deferring the comptime type-construction
eval to a complete-world pass (like #run). Supersedes the two-layer framing.
File the last METATYPE deferred enhancement: List(T).append at comptime
bails ('struct_get: base has no fields') in a type-construction ::.
Standalone repro + two-layer root cause (null comptime allocator at
scanDecls; *T slot_ptr struct_get) + investigation prompt. Non-blocking:
array-literal locals already build variant lists (examples/0620/0624).
Checkpoint + Known issues reference 0141.
Investigated the last deferred enhancement. List(T).append at comptime
fails in two independent layers (both reproduce with plain List(i64);
List works via #run because that evaluates at emit time, after lowering):
1. null comptime allocator — defaultContextValue looks up the
CAllocator->Allocator thunks by name, but they aren't lowered at
scanDecls time. Fixable by forcing getOrCreateThunks before the interp
runs in runComptimeTypeFunc (tried, works for this layer).
2. struct_get through a *T slot_ptr chain (the *List receiver) — the
deep part; comptime pointer/struct/slot resolution, its own session.
Speculative fixes reverted (no end-to-end win without layer 2).
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).
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).
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.
Constructed-type companion to examples/1178 (source form): a declare/
define enum whose variant references itself BY VALUE is rejected by the
same checkInfiniteSize guard ('infinitely sized'). Pins the use-before-
define corner of the validation story — by-value self-reference is the
one self-ref shape that isn't legal; *L (pointer) is fine (see 0618).
No compiler change (locks existing behavior).
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.
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.
make_enum from dirs[0..2] — mints Axis from a comptime SUBSLICE of a
local EnumVariant array. Locks the interp subslice-over-non-string-
aggregate fix (d22037c); previously bailed.
`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.