mem: Phase 1.4 — serialize every interp Value variant for #run globals
`valueToLLVMConst` in emit_llvm previously handled int / float / boolean
and collapsed everything else into `LLVMConstNull(ty)`. A `#run` returning
a struct, string, function pointer, or anything aggregate produced a
zero-initialized global silently — the comptime result was computed by
the interp, then thrown away when emit_llvm couldn't represent it.
Replaced with a real walk:
- int / float / boolean — as before.
- null_val — `LLVMConstNull`.
- void_val / undef — `LLVMGetUndef`.
- func_ref — `func_map` lookup (already populated for the implicit-Context
static initializer of `__sx_default_context`).
- string — `emitConstStringGlobal`, returns a pointer to the byte array.
- aggregate — recurse field-by-field. Struct: walk
`LLVMStructGetTypeAtIndex` and emit `LLVMConstNamedStruct`. Array:
walk `LLVMGetElementType` and emit `LLVMConstArray2`.
The remaining variants (heap_ptr, byte_ptr, slot_ptr, closure, type_tag)
bail loudly with a `std.debug.print` carrying the global name — per
CLAUDE.md REJECTED PATTERNS, no more silent unimplemented arms. heap_ptr
serialization requires threading the IR `TypeId` so the heap content can
be walked recursively; deferred to Phase 1.4a alongside cycle detection.
The call site at emit_llvm.zig:676 now passes `global.name` so the
diagnostic locates the offending `#run` binding.
Type-inference fix at the binding site: `NAME :: #run expr;` with no
annotation used to default to `s64` via `resolveType(null) -> .s64`,
so even a successful Phase 1.4 serialization would emit `{0, 0}` —
the global's destination type was wrong. `lowerComptimeGlobal` now
calls `inferExprType(expr)` when no annotation is given, so the
inferred type matches the comptime function's return type. The
broader `resolveType(null)` fallback is left in place for other
callers — flagged in the MEM checkpoint as a follow-up audit.
Regression: `examples/134-comptime-aggregate-global.sx` exercises
`POINT :: #run make_point()` returning a `Point { x: s32, y: s32 }`.
Both interp (`sx run`) and codegen (`sx build`) now print
`POINT.x = 7 / POINT.y = 13` instead of `0 / 0`. 156/156 example
tests pass; chess unchanged.
This commit is contained in:
@@ -5,18 +5,33 @@ Tracking checkpoint for the mem.sx Zig-aligned implementation
|
||||
|
||||
## Last completed step
|
||||
|
||||
- **`List(T)` mutations gain an optional `alloc: Allocator =
|
||||
context.allocator` argument** (plan:
|
||||
`~/.claude/plans/lets-see-options-for-merry-dijkstra.md`). One
|
||||
language-level change (function-param defaults already supported)
|
||||
closes a whole bug class where long-lived containers' growth
|
||||
silently landed in a per-frame arena. The chess panel-text
|
||||
regression that triggered this work is fixed end-to-end on macOS.
|
||||
GlyphCache, DockInteraction, StateStore, Gles3Gpu, and MetalGPU
|
||||
all carry `parent_allocator` and pass it explicitly to internal
|
||||
list growth. 155/155 example tests pass — zero-diff against
|
||||
snapshots since every existing callsite still resolves to
|
||||
`context.allocator`.
|
||||
- **Phase 1.4 — `valueToLLVMConst` upgraded to handle every interp
|
||||
`Value` variant.** The serializer at `emit_llvm.zig:734` used to
|
||||
collapse anything past int/float/boolean into `LLVMConstNull(ty)` —
|
||||
a silent fallback that emitted `{0, 0}` for any `#run` returning a
|
||||
struct, string, function pointer, or pointer. Replaced with a real
|
||||
walk: int/float/boolean as before; null_val → LLVMConstNull;
|
||||
void_val/undef → LLVMGetUndef; func_ref → func_map lookup;
|
||||
string → emitConstStringGlobal; aggregate → recurse via
|
||||
`LLVMStructGetTypeAtIndex` / `LLVMGetElementType`. The unsupported
|
||||
cases (heap_ptr/byte_ptr/slot_ptr/closure/type_tag) now bail loudly
|
||||
with a named diagnostic that includes the global name (per CLAUDE.md
|
||||
REJECTED PATTERNS — no silent unimplemented arms). Heap_ptr is
|
||||
deferred to Phase 1.4a (needs IR TypeId threaded down for cyclic /
|
||||
recursive heap content).
|
||||
|
||||
Also closes the type-inference half of the same bug: `NAME :: #run
|
||||
expr;` with no annotation used to default to `s64` (silent fallback
|
||||
in `resolveType(null)`). `lowerComptimeGlobal` now infers from the
|
||||
expression's return type when no annotation is provided. The
|
||||
silent fallback in `resolveType` itself is left in place for other
|
||||
callers — separate audit, separate session.
|
||||
|
||||
Regression test at
|
||||
`examples/134-comptime-aggregate-global.sx`: a `POINT :: #run
|
||||
make_point();` struct binding now prints the real fields instead
|
||||
of zeros, on both interp and codegen paths. 156/156 example tests
|
||||
+ chess clean.
|
||||
|
||||
## Current state
|
||||
|
||||
@@ -152,21 +167,34 @@ chain at comptime in the interp. No remaining shortcut.
|
||||
|
||||
Phase 1.3 (closure env allocation through context) shipped in commit
|
||||
`8e21cc5`. Phase 1.4 (codegen serializer for all interp Value
|
||||
variants) remains open. Phase 1.2 (free / malloc through context) was
|
||||
considered and **skipped** — `context.allocator.alloc/dealloc`
|
||||
already works directly; wrapper-only `malloc`/`free` would be lossy
|
||||
renames.
|
||||
variants) shipped this session. Phase 1.2 (free / malloc through
|
||||
context) was considered and **skipped** — `context.allocator.alloc
|
||||
/dealloc` already works directly; wrapper-only `malloc`/`free` would
|
||||
be lossy renames.
|
||||
|
||||
The `List(T)` allocator-arg work documented above is **outside the
|
||||
original MEM plan** but lives in the same problem space (long-lived
|
||||
container growth silently capturing the wrong allocator). It
|
||||
generalises the `parent_allocator` capture pattern that
|
||||
`ChessGameState` / `UIPipeline` already used.
|
||||
The `List(T)` allocator-arg work shipped earlier this session is
|
||||
**outside the original MEM plan** but lives in the same problem
|
||||
space (long-lived container growth silently capturing the wrong
|
||||
allocator).
|
||||
|
||||
Suggested next move: verify on iOS sim + Android via
|
||||
`tools/verify-step.sh` to confirm the GlyphCache fix + Metal/Gles3
|
||||
sweep behave on those platforms, then either commit the verify-step
|
||||
goldens or move to Phase 1.4.
|
||||
Open follow-ups, in roughly the order they make sense:
|
||||
|
||||
- **Phase 1.4a** — Thread IR `TypeId` (not just LLVM `LLVMTypeRef`)
|
||||
through `valueToLLVMConst` so `heap_ptr` values from `#run` can be
|
||||
serialized. Requires walking the struct/slice/primitive children
|
||||
recursively; cycle detection via `(heap_id, type_id)` visited set.
|
||||
Practical trigger: a `#run` that builds a `Widget.{}` and
|
||||
protocol-erases via `xx`, producing a `heap_ptr` to the boxed
|
||||
payload. None exists in-tree yet — surface it via a focused
|
||||
regression alongside the implementation.
|
||||
- **`resolveType(null) -> .s64` audit.** The silent fallback at
|
||||
`lower.zig:8387` is still in place for every caller other than
|
||||
`lowerComptimeGlobal`. CLAUDE.md REJECTED PATTERNS forbids this
|
||||
shape. Survey callers; either make the default an error
|
||||
diagnostic or thread an inferred type per call site.
|
||||
- **`tools/verify-step.sh` gate.** Run iOS sim + Android to confirm
|
||||
this session's GlyphCache + Metal/Gles3 sweeps + Phase 1.4 didn't
|
||||
regress non-macOS platforms.
|
||||
|
||||
## Phase 0.3 audit findings — chess allocator usage (closed)
|
||||
|
||||
@@ -192,7 +220,22 @@ Allocator value naturally.
|
||||
|
||||
## Log
|
||||
|
||||
- **2026-05-25 (latest)** — `List(T)` mutation API gained an optional
|
||||
- **2026-05-25 (latest)** — Phase 1.4 shipped. `valueToLLVMConst`
|
||||
(`emit_llvm.zig:734`) replaced the primitive-only switch with a
|
||||
full serializer covering null_val, void_val, undef, func_ref,
|
||||
string, and aggregate (struct + array via
|
||||
`LLVMStructGetTypeAtIndex` / `LLVMGetElementType`). Unsupported
|
||||
variants (heap_ptr, byte_ptr, slot_ptr, closure, type_tag) bail
|
||||
loudly via `std.debug.print` with the global name. The call site
|
||||
at line 676 now passes `global.name` so the diagnostic locates the
|
||||
offending `#run` site. `lowerComptimeGlobal` (`lower.zig:6384`)
|
||||
infers the return type from the expression when the user omits
|
||||
the type annotation — closes the silent-s64 default for `NAME ::
|
||||
#run expr;` bindings. The broader `resolveType(null) -> .s64`
|
||||
fallback is left in place for other callers — flagged for a
|
||||
follow-up audit. Regression at
|
||||
`examples/134-comptime-aggregate-global.sx`. 156/156 + chess green.
|
||||
- **2026-05-25 (penultimate)** — `List(T)` mutation API gained an optional
|
||||
trailing `alloc: Allocator = context.allocator` argument
|
||||
(`library/modules/std.sx`). Default-arg substitution previously
|
||||
only fired for identifier callees; extended to the generic-method
|
||||
|
||||
Reference in New Issue
Block a user