A comptime-constructed type with NO members is now VALID for every kind
(empty struct, empty tuple, empty enum, empty tagged_union) — only a bare
`declare("X")` placeholder that is never completed by a matching `define`
stays rejected (it would panic codegen).
- comptime_vm.zig registerTypeVm: drop the blanket "a type with no members
is never valid" rejection. The per-kind loops are vacuous for an empty
member list and the dup-name checks stay correct.
- types.zig TaggedUnionInfo: add `defined: bool = true`. Every real
construction (normal unions, error sets, register_type completion) is
"defined" by default; only the two declare-PLACEHOLDER sites set it false:
comptime_vm.declareNominal and lower/comptime.preregisterForwardTypes.
- lower/comptime.checkComptimeTypeResult: reject on `!defined` (never-defined
placeholder) instead of `fields.len == 0`, so an explicitly-defined empty
union passes through while a never-completed declare is still gated.
- types.zig typeSizeBytes(tagged_union): floor the payload area at 8 bytes
when no field carries a payload, mirroring the LLVM lowering — fixes a
verifySizes panic on an empty/all-void tagged_union (IR sized to tag-only,
LLVM laid out tag + [8 x i8]).
Tests:
- examples/1179: repurposed from "empty enum rejected" (now valid) to the
never-defined `declare` case (the remaining rejection); preserves its
issue-0140 regression role.
- examples/1180 (duplicate variant): still rejected, unchanged output.
- examples/0641 (new): construct empty struct/tuple/enum/tagged_union via
define/declare; instantiate the constructible ones; exit 0.
The 0141 repro relied on a silent-wrong coercion: passing List.items (a
[*]T many-pointer, no length) to a []T parameter passed the bare 8-byte
pointer into a 16-byte {ptr,len} slot — garbage .len, at comptime a segfault
in the VM slice decoder (decodeMemberSlice), at runtime an LLVM verify failure.
Fix (root cause): classify [*]T -> []T as many_to_slice_reject in
conversions.zig and emit a build-gating diagnostic in coerce.zig telling the
user to slice with a length (ptr[0..len]). Guard runComptimeTypeFunc to skip
VM eval once diagnostics.hasErrors() — a type-fn body that failed coercion
holds malformed comptime data (a real host Addr) that would fault the VM's
Ref-level guards.
Land the corrected feature as examples/0640 (List-grown comptime enum via
vs.items[0..vs.len] -> green=7) and the rejection as
examples/1183-diagnostics-many-pointer-to-slice-rejected. Mark issue 0141
RESOLVED.
708/0 corpus + 476/476 unit.
evalComptimeString (the #insert lowering-time site) was the last user of
the legacy Interpreter.call. Route it through comptime_vm.tryEval instead:
the VM is hardened to bail (never panic) on malformed lowering-time IR
(0737's ret Ref.none), and regToValue dupes the result string into the
lowering allocator so it outlives the VM arena. Drop the now-unused
interp_mod / build_opts imports from comptime.zig.
500/500 unit + 706/0 corpus.
Remove the comptime_flat/need_vm gate and the vm_result-orelse-legacy
fallback from emit_llvm.zig (runComptimeSideEffects + emitGlobals const-init)
and comptime.zig (runComptimeTypeFunc). The comptime VM now always runs;
a bail is always a build-gating diagnostic, never a fallback. Delete the
now-moot entryNeedsVm. runComptimeSideEffects drops the Interpreter entirely
(VM writes #run output direct to fd 1); emitGlobals keeps a fresh interp_inst
only as the valueToLLVMConst materialization context (the regToValue bridge,
removed with interp.zig in a later step).
#insert (evalComptimeString) still routes through the legacy interp — deferred
until interp.zig deletion.
Reconcile 1654: the comptime asm-global #run now reports the VM's clean dlsym
bail instead of the legacy CannotEvalComptime wrapper (exit still 1).
501/501 unit + 706/0 corpus.
`BuildOptions :: struct #compiler { ...35 methods... }` becomes
`BuildOptions :: struct { }` (an opaque null-sentinel handle) plus 35 free
`ufcs (self: BuildOptions, …) abi(.compiler)` decls in build.sx, each serviced
by a new `comptime_vm.callBuildOptionFn` arm (off `callCompilerFn`). No legacy
`compiler_lib` handler: the names are registered in `bound_fns` with a single
bailing stub only so `weldedCompilerFn` accepts them.
- String lifetime: setters dupe the arg into the persistent `Vm.gpa` (the
Compilation allocator, threaded into both `tryEval` and `runBuildCallback` —
not the per-eval VM arena) and write/append to the threaded `BuildConfig`.
Getters read the field/slice or compute the target predicate from the triple.
- Dispatch routing (Option B): a `#run`/const-init entry that directly calls a
compiler-domain/welded fn (`emit_llvm.entryNeedsVm`) runs on the VM with no
legacy fallback regardless of the `-Dcomptime-flat` gate, so gate-OFF stays
green without a legacy BuildOptions handler (P5.7 retires the legacy interp).
- Mark the 5 `platform/bundle.sx` getter-calling helpers `abi(.compiler)` (they
are comptime-only bundler code; otherwise their now-welded getter calls trip
the runtime-call gate).
- 37 `.ir` snapshots regenerated (std transitively imports build.sx → string-
pool/type-table indices shift); verified `.ir`-only, zero behavior-stream diffs.
BuildOptions `compiler_call` strict bails gone (1609/1614/1615 strict-clean);
1616 now bails on a separate, pre-existing unported bitwise/shift VM gap (`shr`),
to port first in P5.6. 703/0 both gates.
Also sweep the outdated "flat memory" terminology to "comptime/byte-addressable"
across comptime_vm + the plan/checkpoint/CLAUDE docs: the comptime VM is
arena-backed, byte-addressable memory where `Addr` is a real host pointer, not a
flat contiguous address space (flag names `-Dcomptime-flat`/`SX_COMPTIME_FLAT` kept).
Lands the full VM/compiler-API arc on branch reify (701/0 both gates):
- abi(.compiler) ABI replaces abi(.zig) extern compiler + the fake
#library "compiler"; bodiless decl = compiler-API surface, bodied =
user compiler-domain fn (lowered for VM eval, emit-skipped).
- out is a plain sx fn (libc write) — the out builtin deleted; the VM
handles it via host-FFI. trace_resolve + interp_print_frames ported.
- 4B VM-native diagnostics: 1179/1180 render proper comptime type
construction failed: under strict.
- S5a: build_options/set_post_link_callback on abi(.compiler) with
BuildConfig threaded into the VM (green intermediate).
- 0522 fixed (describe(args: []Type)); regression 0638.
Strict deletion-gate down to 4 compiler_call bails (1609/1614/1615/1616)
+ 1654 (legitimate unresolvable-symbol diagnostic).
Add -Dcomptime-flat-strict / env SX_COMPTIME_FLAT_STRICT (implies comptime_flat):
at all three comptime sites (type-fn in lower/comptime.zig, const-init + #run in
emit_llvm.zig) a VM bail becomes a build-gating error naming the reason INSTEAD of
falling back to legacy. Forces every comptime eval onto the VM so the complete gap
set is enumerable in one sweep; when the corpus is green under strict mode AND every
example matches legacy, interp.zig can be deleted.
Default behaviour unchanged (699/0 both default gates). Fixed a wiring bug: the
type-fn site's local comptime_flat didn't include the strict flag (every type-fn
falsely reported <unknown>); strict now implies flat there too.
Swept the gap list (19 strict bails): switch_br (5, + unmasks a []Type-across-call
silent-wrong in 0114), compiler_call (6, = the BuildOptions->abi(.zig) extern
compiler migration), out (2), type_name (1), global_addr (1), interp_print_frames
(1), 2 negative-test diagnostics (1179/1180), 1 dlsym (1654). Recorded as the
deletion checklist in CHECKPOINT-COMPILER-API.md.
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.
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.
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.
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).
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).
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.
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.
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).
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).
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.
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
A struct constant whose every field serializes — literals, enum tags,
nested aggregates, and (new) const EXPRESSIONS over named consts /
const-aggregate leaves ('r = K + 1', 'g = LIT.r', 'b = A[1]') — becomes
an immutable global: one storage, reads load/GEP it, '@LIT' is
addressable, dead-global elimination drops unused ones. constExprValue
gained a fold-through tail (evalConstIntExpr/evalConstFloatExpr,
source-aware), which also enables const-expression ELEMENTS in array
consts.
A const with a NON-serializable field (a call, a runtime read) keeps
inline re-lowering, and that per-use evaluation is now the documented
contract for the class (pinned: 'CALL.r' reads 1 then 2, side effects
run per use; '#run' is the evaluate-once tool).
Examples: 0180 (migrated shapes + @ptr + copy independence),
0181 (the inline-fallback contract). m3te (23/23) + game rebuilt green.
An array const's '.len' and 'K[<const idx>]' element reads, and a
struct const's field ('LIT.r'), fold as compile-time integer leaves —
usable in array dimensions and other constants' initializers. All
source-aware (the SELECTED author's elements, folded in the author's
context with the cyclic-definition frame); a const out-of-range index
diagnoses at fold time, never wraps.
- evalConstIntExpr gains the three ctx hooks (lookupConstAggLen /
lookupConstArrayElem / lookupConstStructField) + an index_expr arm;
all five ctx implementations extended (stateless tiers fold null).
- Array consts dual-register in module_const_map (value = the literal
node) so the folders see elements; bare reads still hit the GLOBAL
arm first, so no double emission.
- Untyped consts whose RHS is a const-aggregate leaf ('L :: K.len',
'E :: K[1]', 'R :: LIT.r') register in a pass 2b AFTER aggregates,
gated on the receiver naming a const aggregate — a namespaced member
('F :: m.PI_ISH') is never mis-typed by the count placeholder.
Examples: 0179 (folds in dims + const exprs), 1163 (OOB diagnostic).
K : [4]s64 : .[...] and the untyped A :: .[1, 2, 3] register as
is_const globals: one storage, reads GEP it, dead-global elimination
drops unused ones, source-aware reads come free via selectGlobalAuthor.
- registerConstArrayGlobal (scanDecls pass 2): typed via the annotation
(array-ness + dimension/count checked), untyped via element-type
unification — all ints s64; ANY float promotes the element type to
f64 with ints converting exactly; bool/string homogeneous; a
non-numeric mix or non-inferable element asks for an annotation.
- constExprValue converts int elements into float destinations exactly
(the int+float promotion rule, element-wise).
- emitGlobals marks is_const globals LLVMSetGlobalConstant — also flips
the comptime-backed #run globals and __sx_default_context to
'constant' (37 pinned IR snapshots regenerated; runtime unchanged).
- Element shapes: nested arrays, struct elements, strings, bools.
Non-constant elements / dim mismatch / mixed types diagnose loudly.
Examples: 0177 (feature matrix incl. @K reads through *[4]s64 — needs
the 0117 fix), 1159/1160/1161 (diagnostics), 0837 repointed to values.
The globals registry (global_names) was last-wins across modules with no
per-importer gate: any module's bare K could read/write/type against an
unrelated module's same-named global (hash.sx's K table hijacked every
user K once std's namespace tail pulled hash into the program), and an
own const of an unsupported shape borrowed another module's const and
panicked at the unresolved-type tripwire.
- var_decl joins RawDeclRef: module globals are selectable raw authors.
- selectGlobalAuthor (the globals analogue of F2's selectModuleConst):
own author wins, one flat-visible author resolves, >=2 distinct flat
authors diagnose loudly, authored-but-not-visible diagnoses, and a
compiler-synthesized global (no raw author) emits untracked. A var_decl
author whose per-source registration was deduped at flat-merge (two
modules declaring the same extern symbol) serves the symbol's
registration.
- All bare-identifier global sites route through it: value read, addr-of,
assignment (store + compound), lvalue address, fn-ptr call, call param
typing, and expression type inference.
- selectModuleConst gains .own_opaque: an own const author with no
materialized per-source value (e.g. an array '::' const) blocks
borrowing another module's same-named const — the read diagnoses
cleanly instead of panicking.
- The fn-as-VALUE arm admits raw-facts-only authors: an own fn whose name
a flat-merge collision dropped from the global decl list (first-wins)
now resolves via author selection for func_ref/closure/Any shapes too.
Regressions: examples 0835 (own const vs flat array global), 0836 (main
const vs namespaced array global, incl. inference), 0837 (own array
const never borrows cross-module — clean unresolved).
Sweep all src/**.zig comments that cite resolved issues (issue NNNN /
fix-NNNN / KB-N): the invariant or mechanism each comment states is
kept; the historical citation is dropped, per the no-conclusion-comments
rule. Pure-history parentheticals are removed outright. References to
the 16 still-open issues (0030, 0041-0056) are untouched, as are test
NAMES carrying regression provenance (matching the sanctioned
"Regression (issue NNNN)" example-header convention).
Also removes the issues/0019-import-non-transitive-c-scope/ fixture dir
— the issue is superseded and its behavior is covered by
examples/0706-modules-import-non-transitive.sx (the .md writeup stays).
issues/0030's repro .sx stays: that issue is an open feature request.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Review follow-up to the ARCH-B split (comment/import hygiene only, no
code changes):
- Section banners that travelled to the wrong file with the B1-B8 cuts
are reworded to describe the section that actually follows (e.g.
stmt.zig's trailing "Expression lowering", expr.zig's "Control flow"
before lowerChainedComparison) or deleted where nothing follows
(4 trailing-at-EOF banners). ffi.zig's facade note no longer claims
the IMP builders "stay here" (they live in lower/objc_class.zig);
protocol.zig's namespace-lookup banner now points at
pack.zig:resolvePackProjection for the orchestrator.
- lower.zig's two lower/expr.zig alias blocks (B8.1 + B8.2 appends)
merged into one.
- 448 unused header decls pruned from the 15 lower/*.zig files (each
had inherited lower.zig's full import block; pruned to fixpoint so
cascading type-extraction consts went too).
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.