Files
sx/current/CHECKPOINT-MEM.md
agra d8076b9333 lang: rename signed integer types sN -> iN
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.
2026-06-12 09:31:53 +03:00

954 lines
49 KiB
Markdown

# Memory Module — Progress + Issues Log
Tracking checkpoint for the mem.sx Zig-aligned implementation
(plan: `~/.claude/plans/tidy-doodling-cray.md`).
## Last completed step
**Allocator primitive rename — `alloc``alloc_bytes`,
`dealloc``dealloc_bytes` (`88bae3c`, 2026-06-11).** Phase 4's naming
piece pulled forward per Agra's call (Option A in the 2.2 naming fork):
the bare names free up NOW so the Phase 2.2 helpers land under their
FINAL Appendix-A names (`alloc(T,n)` / `free(s)` / `create` / `destroy`
/ `clone` / `resize` / `mem_realloc`) with zero later churn. Phase 4
shrinks to signature expansion (size/align params + resize/remap +
deinit) only.
What changed (signatures unchanged — 2-method era continues):
- `std.sx` protocol decl; `std/mem.sx` 6 impls + internals; library
call sites (glyph_cache/json/state/renderer); 13 example .sx files
(incl. the two custom `Tracer` impls in 0306/0808 — caught broken
mid-step by stdout-diff review BEFORE pinning; the first `--update`
had captured their broken output, restored + fixed + re-pinned).
- Compiler: `interp.zig` thunk-name lookups
(`__thunk_CAllocator_Allocator_alloc_bytes`/`_dealloc_bytes`) — the
ONE hard name coupling; `allocViaContext` + default-context emission
are slot-positional (rename-safe); ffi.zig's `"alloc"` check is the
Obj-C `Cls.alloc()` intercept (unrelated, untouched).
- 37 `.ir` snapshots re-pinned (thunk symbols + reflection
field-name strings); all 37 verified stdout-clean before update;
path-noise churn reverted.
- External repos migrated + gated: game (`main.sx`, SxChess.app
builds + bundles) and m3te (`board_fx.sx`, `main.sx`,
`tools/key_particle.sx`; tools/run_tests.sh 23/23). Obj-C zero-arg
`.alloc()` calls are NOT protocol calls — excluded by pattern.
Gates: zig build 0, zig build test 0, suite 582/582, m3te 23/23,
game rebuilt.
<details><summary>Prior steps (2026-05-25 era)</summary>
- **`resolveType(null) → .i64` silent fallback removed.** `resolveType`
now takes a non-optional `*const Node`; the `null → .i64` branch is
gone. Callers that legitimately had no annotation handle it
themselves: top-level `var_decl` at `lower.zig:630` infers from the
initializer or diagnoses if neither is present (matches the
`lowerVarDecl` pattern that already existed for locals).
`FfiIntrinsicCall.return_type` is non-optional in the AST so its
callers (`#objc_call`, `#jni_call`) didn't actually need the
fallback. JNI super-call and JNI method paths were already guarded
with `if (rt) |t| ... else .void`.
Caller cleanup also dropped the explicit `if (x != null)` guards in
favour of optional-payload syntax (`if (cd.type_annotation) |ta|
...`), which makes the always-non-null path obvious from the type.
Real-world impact: `g_pi := 3.14;` at the top level used to be
silently typed as `i64`. Now it infers as `f64`. Regression at
`examples/137-toplevel-var-type-inference.sx` (count/pi/flag — int /
float / bool inferred correctly). 159/159 example tests + chess
clean.
- **Phase 1.4a — IR `TypeId` threaded through `valueToLLVMConst`;
string/slice fat-pointer aggregates serialize by reading host
memory.** The Phase 1.4 serializer bailed on `heap_ptr` / `byte_ptr`
and silently emitted `i0 0` for the trap-case where a `.int` host
address landed in a ptr-typed slot. Now the call site at
`emit_llvm.zig:676` passes `global.ty` (TypeId) and `&interp_inst`
instead of just the LLVM type. The serializer splits on the IR
type:
- `string` / `slice` (fat pointer `{data, len}`): extract `len`,
read that many bytes from the data field's address (heap_ptr →
`interp.heapSlice`; byte_ptr/int → raw process memory via a new
`readHostBytes` helper; string literal → direct slice). Emit
the bytes as a private global byte array via the existing
`emitConstStringGlobal` and use it as the aggregate's data ptr.
- `struct`: walk the IR field types in lockstep with the value
fields; recurse per field with its declared TypeId. Replaces
the old LLVM-type-walk via `LLVMStructGetTypeAtIndex` which
couldn't tell `string`-typed fields from generic ptr fields.
- `array`: walk elements with the element TypeId.
The `.int → ptr` slot mismatch (a host address landing in a
non-fat-pointer ptr slot) now bails loudly with a named
diagnostic — that's the genuine heap-walk frontier where future
work would need to capture struct content recursively, not the
silent malformed-const we had before.
`Interpreter.heapSlice` was promoted from package-private to
`pub` so the serializer can read interp heap. Regression at
`examples/136-comptime-string-global.sx`: `GREETING :: #run
build_greeting();` where `build_greeting` returns `concat("hello",
" world")` — runtime prints `greeting = 'hello world' / greeting.len
= 11`. Pre-1.4a this segfaulted. 158/158 example tests + chess
clean on all three platforms via `tools/verify-step.sh`.
- **Allocator `init` returns the state by value.** Building on the
Option 3 lvalue-borrow rule, `GPA.init`, `Arena.init`, and
`TrackingAllocator.init` now return `T` (not `*T`). The caller binds
the local; the local IS the storage. `xx local` borrows under Option
3 so the `Allocator` protocol value's `ctx` points at the local.
Saves one `libc_malloc` per allocator instance; closes the
state-struct leak surface (the local goes away with its scope, no
explicit `deinit` needed for the struct itself).
Migration:
- `library/modules/allocators.sx`: `init` signatures changed
(`-> T` instead of `-> *T`); `Arena.deinit` drops the trailing
`parent.dealloc(xx a)` — caller owns the storage.
- `library/modules/ui/pipeline.sx`: `arena_a: Arena;` /
`arena_b: Arena;` (was `*Arena;`); `@self.arena_a` /
`@self.arena_b` at the use site that needs `*Arena` for the
`build_arena` local.
- `examples/126-xx-recover-then-dispatch.sx` updated: comparisons
against `*GPA` use `@gpa` now.
- `examples/135-xx-lvalue-borrows.sx` simplified: no
`tracker_ptr.*` deref needed.
- `examples/50-smoke.sx` Arena counts dropped by 1 each (no
state-struct alloc); comments + snapshots updated to match.
- CLAUDE.md "Allocator construction" rule rewritten around the
by-value convention.
157/157 example tests + chess clean on macOS / iOS sim / Android
(`tools/verify-step.sh` ran green).
- **`xx <lvalue>` borrows the operand's storage** (Option 3 in the
protocol-erasure design discussion). Today's behavior — `xx
<struct-typed local>` heap-copies the value — was a silent footgun:
the protocol value pointed at the heap copy, the original local
stayed stale, mutations through the protocol weren't visible to the
original (and vice versa). Under the new rule, when the operand
names existing storage (identifier, field access, index expression,
dereferenced pointer), `xx` takes its address and the protocol
borrows. Heap-copy is reserved for `xx <rvalue>` — struct literals,
function-call results, arithmetic expressions, anything without its
own storage.
Single point of change at `buildProtocolErasure` in `lower.zig:10334`,
via a new `isLvalueExpr` helper at `lower.zig:10322`. specs.md §3
ownership table updated. The `examples/130-...` regression that
previously tested heap-copy on `xx <local>` now tests `xx
<struct-literal>` (still the heap-copy path); new regression
`examples/135-xx-lvalue-borrows.sx` witnesses the borrow path via
TrackingAllocator. 157/157 example tests + chess clean across all
three platforms (`tools/verify-step.sh` gate ran green right
before this work landed).
- **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 `i64` (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.
</details>
## Current state
**std.sx-as-pure-re-exports plan COMPLETE** (2026-06-11,
Agra-directed end-to-end). std.sx is now a facade of alias
declarations only (`49a36bb`): implementations live in std/core.sx
(builtins, libc escape hatch, Context/Allocator/Into/Source_Location/
`string` — the reserved name needs and permits no alias), std/fmt.sx
(print/format/any_to_string/string ops), std/list.sx (List); the
namespace tail is unchanged and `core`/`fmt`/`list` carry alongside
it. Consumer surface byte-identical; 37 .ir snapshots re-pinned
(pure renumbering, digit-normalized diff empty).
Issues filed AND resolved along the way (all same-day,
Agra-authorized): 0120 generic-struct head aliases (`f2db8ec`,
example 0211), 0121 fn aliases of every kind incl. comptime-pack
(`721369a`, example 0546), 0122 whole-program passes pinning the
source context per decl (`340be40` — latent on master, exposed by
the facade; coverage 0129/1047/1049/1052/1053/1056). Protocol
aliases (plain + Into's xx path) and #builtin/#foreign aliases
probe-verified working; param-protocol impl dot-calls are a designed
opt-in gap, not a bug.
Gates at completion: zig build test 426/426, suite 588/588,
m3te 23/23, game SxChess builds + bundles. Suite baseline 588.
**(2026-06-11) Phase-by-phase ground truth** (verified against the
tree; the sections below this one are the 2026-05-25 era record):
- Phase 1 DONE (xx heap-copy via context.allocator; serializer; the
whole implicit-Context refactor).
- Phase 2.1 DONE-equivalent: allocators.sx became `std/mem.sx` via the
STDLIB restructure (`59f0aa7`).
- **Primitive rename DONE (`88bae3c`)** — see "Last completed step".
Protocol is `alloc_bytes(size)` / `dealloc_bytes(ptr)` (2-method
era signatures).
- Phase 2.2 helpers: NOT YET — next step. Final names per Appendix A:
`create(a,$T)->*T`, `alloc(a,$T,n)->[]T`, `destroy(a,ptr)`,
`free(a,slice)`, `clone(src,a)`, `resize(slice,a,n)`,
`mem_realloc(a,ptr,old,new,align)`. The `free` helper requires
REMOVING std.sx's bare `malloc`/`free` foreign decls (lines ~13-16;
`libc_malloc`/`libc_free` aliases already exist) and migrating ~30
bare uses (mostly examples) — fold into the helpers step.
- Phase 2.3: TrackingAllocator exists; FailingAllocator +
LoggingAllocator missing.
- Phase 3 (caller migration to helpers): not started.
- Phase 4: now signature-expansion only (alloc_bytes gains alignment;
dealloc_bytes gains size+align; + resize/remap/deinit) — naming
piece already landed.
- Phase 5 (--leak-check + specs Memory chapter): not started.
- NOTE: plan's "init returns Allocator via xx heap-copy" section is
SUPERSEDED by the by-value convention (CLAUDE.md "Allocator
construction"); BufAlloc.init still returns *BufAlloc (state lives
in the caller's buffer — review at Phase 4 whether to align).
- Suite baseline 582; gates now: zig build && zig build test &&
bash tests/run_examples.sh (+ m3te 23/23 + game build for
std-touching steps). The old 148-159 counts below are historical.
<details><summary>2026-05-25 era state (historical)</summary>
Phase 0.0c shipped (allocator API on one-line `init` returning `*T`;
TrackingAllocator added). 148/148 tests pass.
Phase 0.2 verified across all known patterns. Phase 0.3 documented.
Phase 0 of the MEM plan is **COMPLETE**.
Fixed this session by paired sessions: issue-0038 (transitive
`#import`), issue-0039 (chess + stdlib migration to explicit imports),
issue-0040 (generic struct method dot-dispatch). All landed and
re-verified. Full gate green: 151/151 example tests + chess on macOS
/ iOS sim / Android with screenshots.
Phase 0 spike outcomes:
- **0.2** xx cast patterns — verified across 5 additional shapes
beyond the issue-0037 regression.
- **0.3** chess allocator audit — documented; no chess-side
migration needed.
- **0.6** `align_of($T)` builtin — landed. Mirrors `size_of` in
inst.zig BuiltinId, lower.zig (registry + return-type + reflection
handler), interp.zig (fallback), sema.zig (allowed-builtins list),
lsp/server.zig (both completion tables), library/modules/std.sx.
Smoke coverage added in `examples/50-smoke.sx` (u8/i32/i64/Point).
- **0.7** `#import` transitivity — surfaced and fixed via issue-0038.
- **0.8** `#foreign("c")` rename syntax — confirmed
`#foreign libc "name"`.
- **0.9** generic UFCS dispatch — surfaced and fixed via issue-0040.
Dot-call now works for struct methods with `$T: Type` parameters,
so the plan's `a.create(MyType)` shape is viable.
issue-0041 (pointer type as type-arg) and issue-0042 (alias names in
`resolveTypeArg`) **FIXED**. Regression tests at
`examples/issue-0041.sx` and `examples/issue-0042.sx`. Scratch
verification: `size_of(*u8)=8`, `size_of(Ptr where Ptr::*u8)=8`,
`size_of(?u8)=2`, `size_of(Maybe where Maybe::?u8)=2` — all clean on
interp + codegen.
Also landed during 0041/0042: the silent `.i64` fallback in
`resolveTypeArg` is gone — unresolved type names now emit a real
diagnostic. Surfaced and removed two bogus `size_of(Complex)` /
`size_of(Sx)` calls in `examples/10-generic-struct.sx` that were
relying on the silent default. Caller-side speculative paths in
`buildTypeBindings` + `inferGenericReturnType` now gate the call
with `type_bridge.isTypeShapedAstNode` before invoking
`resolveTypeArg`. CLAUDE.md REJECTED PATTERNS gained a section
forbidding silent `orelse default` returns in compiler code.
Parser regressions introduced by the 0041 work fixed in
`src/parser.zig:hasFnBodyAfterArrow`:
- `(s: string) -> [:0]u8 { ... }` — added `.colon` to the
return-type token walk.
- `(x) -> Closure(...) -> R { ... }` — added `.arrow` so nested
return-type arrows continue the walk.
- `name :: (self: T) -> Ret;` inside `struct #compiler` — recognise
trailing `;` as a method-decl terminator. Was silently dropping
every `BuildOptions.*` method from `fn_ast_map`.
Full gate green: 151/151 example tests + chess on macOS / iOS sim /
Android with screenshots.
**Implicit Context refactor SHIPPED.** Every default-conv sx function
now carries `__sx_ctx: *void` at LLVM slot 0. `context.X` lookups
resolve through the lowering's `current_ctx_ref` — a one-indirection
load, no per-access walk, thread-safe by construction (each call
stack carries its own Context chain). `push Context.{...}` allocates
a fresh slot and rebinds `current_ctx_ref` for the body's lexical
scope. The `context` LLVM global is gone; the only runtime Context
is the static `__sx_default_context` (a CAllocator backing libc
malloc/free), installed at FFI-inbound entries (`main`, `Java_*`,
`JNI_OnLoad`) and used by the interp for `#run` evaluation.
What landed:
- `Function.has_implicit_ctx` flag + `Module.has_implicit_ctx`
per-compilation switch (gated by `Context :: struct {...}` being
present in the dep graph).
- Param prepend + call-site forwarding across every sx-to-sx path:
direct calls, indirect through fn-pointer vars, protocol dispatch,
closure trampolines, lambda trampolines, bare-fn trampolines,
generic monomorphizations, comptime functions.
- `ConstantValue.func_ref: FuncId` variant so the static initializer
for `__sx_default_context` can reference the CAllocator thunks.
- emit_llvm two-pass global emission: aggregates that name funcs by
FuncId resolve them after `func_map` is populated.
- Interp: `defaultContextValue()` builds the Context aggregate on
demand; `interp.call` bootstraps slot_ptr(0) when an entry function
with implicit ctx is invoked sans args; `materializeCtxArg` derefs
the caller's slot_ptr at every sx-to-sx boundary so callees can
treat ref 0 as the Context aggregate; `.load` of an aggregate is
passthrough; `.global_addr` of `__sx_default_context` returns the
aggregate directly.
- `matchContextAllocCall` is GONE (commit `d415bcc`). Comptime now
runs the full Allocator-protocol dispatch chain — the same IR
codegen emits — by reusing the parent module instead of spinning
up a separate `ct_module`. The interp gained raw-pointer paths
(`index_gep`, `index_get`, `store`, `marshalForeignArg`,
`asString`) so `context.allocator.alloc` bottoms out at host
`libc_malloc` and the returned pointer survives downstream sx ops.
- `inst.Store` carries `val_ty: TypeId` so the interp's raw-pointer
store honours the destination width — no more "assume 8 bytes"
silent clobber. Regression test at
`examples/132-comptime-typed-store-widths.sx` exercises every
primitive width (u8/u16/u32/u64, i8..i64, bool, f32, f64) via
comptime checksums compared to runtime checksums.
- Call-convention mismatch at bare-fn → fn-pointer coercion is now
a compile error (commit `f886d5f`). The chess-debug sweep that
surfaced the bug also moved `#foreign` decls to default
`callconv(.c)` and fixed every consumer-side sx callback exposed
to a C API. Regression test:
`examples/131-callconv-mismatch-diagnostic.sx`.
- Interp silent-arm sweep (commit `e9df33a`). Every `else =>` arm
has a named bail reason via `bailDetail` / `typeErrorDetail`.
`.deref` and `.unbox_any` used to silently pass through arbitrary
Value kinds — now enumerated. `#run const` errors no longer
swallow into `void_val`; emit_llvm surfaces them via std.debug.
- C-side callback into sx requires `callconv(.c)` on the sx fn (and
on any fn-pointer TYPE the user casts a C fn-pointer through).
Tests adjusted: `examples/61-objc-roundtrip.sx`,
`examples/62-objc-class.sx`, `examples/95..97-objc-block*.sx`,
`examples/ffi-06-callback.sx`.
154/154 example tests pass (two new regression tests added: 131 and
132). Chess green on macOS / iOS sim / Android.
ISSUE-MEM-002 (the `context.allocator.alloc(size)` pattern-match
bypass) is FULLY CLOSED. User-typed `context.allocator.X` flows
through the real protocol vtable at codegen *and* runs the same
chain at comptime in the interp. No remaining shortcut.
</details>
## Current state
**Opt-in UFCS landed (`a47ea14`, 2026-06-11)** — the canonical dot
surface now works: `context.allocator.create(Session)`,
`slice.clone(a)`. Agra specified the model in-session (three
clarifying rulings): free-fn dot-calls are OPT-IN via
`name :: ufcs (params) { body }` (NEW declaration form) or
`name :: ufcs target;` (alias); plain fns are direct/`|>`-only with a
tailored rejection. Implementation inverted TWO pre-existing gaps:
unannotated fns used to dot-dispatch (removed; 6 example files
audited + migrated, ZERO reliance in m3te/game) and aliases did NOT
dot-dispatch at all (0036 only ever pinned direct+pipe). Generic ufcs
fns bind `$T` from the receiver; protocol receivers dispatch own
methods first, fall through to ufcs fns for non-members
(protocolHasMethod gate in lower/call.zig). Root-cause bonus:
`inferGenericReturnType` now delegates to `buildTypeBindings` (ONE
binding builder) — structured generic params (`[]$T`) no longer type
direct calls as `T{}` stubs. mem.sx helpers marked `ufcs`; specs.md
§UFCS rewritten around the opt-in matrix. Tests: 0053 (matrix), 1166
(rejection), 0838 re-pinned (dot+pipe+direct). Gates: 585/585, zbt 0,
m3te 23/23, game builds. 0119 RESOLVED (final banner).
**Phase 2.2 DONE (`84e0fb0`, 2026-06-11).** The 0119 block resolved as
a LANGUAGE RULING, not a compiler fix (Agra, in-session): dot-form UFCS
on generic free functions is not the contract — UFCS free-fn
dot-dispatch is the annotated `ufcs` alias mechanism (concrete
targets), and the FLUENT spelling for free functions is the pipe:
`context.allocator |> create(Session)` desugars at parse time to the
direct call, which dispatches generics through normal monomorphization
(verified for protocol + slice receivers). specs.md §UFCS corrected
(it overstated "generic functions" for the dot form). Issue 0119
carries the RESOLVED banner; residual unfiled corner: a `ufcs` alias
naming a generic target doesn't dot-dispatch either.
Landed:
- `std/mem.sx` typed helpers, era-complete bodies, final names:
`create(a,$T)->*T`, `destroy(a,*$T)`, `alloc(a,$T,n)->[]T`,
`free(a,[]$T)`, `clone(src,a)`, `resize(slice,a,n)` (fresh storage +
copy + free-old; old slice dangles), `mem_realloc(a,ptr,old,new,
align)` (alloc+copy+dealloc; align unhonored until the protocol
carries alignment — documented inline). NO zero-init (Zig-aligned).
- std.sx bare `malloc`/`free` decls REMOVED (libc_malloc/libc_free
stay as the raw escape hatch); users migrated: examples
0205/0604/0804/0806/0808/1610 + game/chess/pieces.sx.
- Regression: examples/0838-memory-helpers.sx (whole surface, direct +
`|>` spellings, TrackingAllocator balances 8/8). 37 .ir re-pins
(constant-pool renumbering from the removed decls — the
ISSUE-MEM-004 cascade; all verified stdout-clean pre-update).
- KNOWN GAP: `string` does NOT bind a `[]$T` param (probe: "unknown
type 'T'") — string-clone story deferred (sx string is [:0]u8-shaped;
decide at Phase 4/5).
Gates: zig build 0, zbt 0, suite 583/583, m3te 23/23, game SxChess.app
builds.
## Next step
**Phase 2.3 — diagnostic wrappers**: `FailingAllocator` (delegates to
parent while budget remains, then alloc returns null) and
`LoggingAllocator` (tag-prefixed prints, delegates) in `std/mem.sx`,
2-method-era bodies, by-value `init` per the CLAUDE.md convention.
Then Phase 3 (migrate std.sx/library/example callers to the helpers —
NOTE std.sx itself cannot import mem.sx (circular); its internals keep
alloc_bytes), Phase 4 (protocol signature expansion: alignment + size
on the primitives, resize/remap/deinit — naming already landed),
Phase 5 (--leak-check + specs Memory chapter).
<details><summary>2026-05-25 era next-step record (historical)</summary>
Phase 1.3 (closure env allocation through context) shipped in commit
`8e21cc5`. Phase 1.4 (codegen serializer for all interp Value
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 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).
Open follow-ups, in roughly the order they make sense:
- **`.int → ptr` heap-walk follow-up.** Phase 1.4a handles the
fat-pointer aggregate case. A `.int` host-address landing in a
bare ptr field (e.g. a struct with `*Inner` or `[*]u8` members)
still bails. Investigated this session — the practical blocker
is interp-level: `struct_gep` / load / store through raw integer
pointers aren't wired (only `index_gep` is), so users can't even
construct most cases. Two-step plan if a real trigger surfaces:
(1) lift the interp's struct_gep+load+store-through-raw-int limit
using `FieldAccess.base_type` for the field offset table; (2) add
recursive heap-walk in `valueToLLVMConst` with cycle detection on
`(addr, type_id)` visited pairs. No practical trigger in-tree —
the canonical buffer/string case is already handled by `[]T` /
`string`.
</details>
## Phase 0.3 audit findings — chess allocator usage (closed)
After Step 5 / matchContextAllocCall removal, every consumer call
to `context.allocator.X` flows through the real protocol vtable.
This section is left for history — the audit drove which sites
needed migration, but no chess code actually needed any allocator-
API change. The sites that used to bypass the protocol via the
`.heap_alloc` pattern-match now dispatch through the inline
Allocator value naturally.
- `~/projects/game/main.sx` — 7 sites of
`context.allocator.alloc(size_of(T))` for platform/GPU/pipeline
state. Now real protocol dispatch.
- `~/projects/game/chess/game.sx``ChessGameState.init` captures
`context.allocator` into a `parent_allocator: Allocator` field
and restores it via `push Context.{ allocator = ... }`. Worked
before, still works.
- `~/projects/game/chess/pieces.sx` — declares its own `free`
bound to libc and calls it on a libc-malloc'd buffer (from a
foreign reader). Intentional C-interop bypass — no change needed.
- `~/projects/game/quick.sx` — quicksort demo. Same flow as main.sx.
## Log
- **2026-06-11 (latest)** — Redundant flat `#import "modules/std/mem.sx"`
dropped from the facade (`c75cd9c`, Agra spotted it): the tail's
`mem ::` import already covers the graph needs (ufcs helpers +
CAllocator); the double import was duplicating lowered IR (~2.5k
lines across 37 re-pinned .ir snapshots, output byte-identical).
Gates: suite 588/588, zbt 0, m3te 23/23, game builds.
- **2026-06-11 (prior)** — std.sx restructured to a pure re-export
facade (`49a36bb`): all implementations moved to std/core.sx /
std/fmt.sx / std/list.sx; std.sx = alias decls + namespace tail.
En route, two more issues filed AND resolved: 0121 fn aliases
(`721369a` — renamed aliases were broken for EVERY fn kind, not
just packs; scan-time fn_ast_map registration via the shared
alias-chain walk; example 0546) and 0122 ambient source-context
bugs in convergeClosureShapeSets / checkErrorFlow / unknown-type
loop (`340be40` — latent on master, exposed by the facade).
Probe-verified before executing: protocols (plain + Into xx path),
#builtin / #foreign aliases, reserved `string` (no alias needed or
possible). 37 .ir re-pins (pure renumbering). Gates: zbt 426/426,
suite 588/588, m3te 23/23, game builds + bundles.
- **2026-06-11 (prior)** — Issue 0120 filed AND resolved (Agra-directed
same-session fix). Found probing the std.sx-as-pure-re-exports
restructure: generic-struct head alias (`BoxAlias :: Box;`) lowered
silently to `.unresolved` → LLVM backend panic; cross-module
`Box :: r.Box;` re-export invisible. Fix: `selectGenericStructHead`
follows const-alias decls hop-by-hop from each ALIAS AUTHOR's source
(`aliasedStructTemplate`, nominal.zig; `namespaceAliasVerdictFrom`
for `ns.X` RHS), checked before the template map so a facade's
same-name re-export beats an invisible global template; plus the
missing "unknown type" diagnostic on the `.call` type-head tail
(resolveTypeCallWithBindings). Also fixed PRE-EXISTING stale unit
test (calls.test.zig UFCS plan — predated a47ea14's opt-in model;
master was 425/426). specs.md Type Aliases + readme re-export
section + Decisions Log updated. Regression: examples/0211 (+rich/
+facade companions). Gates: zbt 426/426, suite 587/587.
- **2026-06-11 (prior)** — BufAlloc.init by-value (`51194a2`, Agra
request): init no longer carves its state struct off the buffer's
head (`-> BufAlloc`, plain literal; the old `-> *BufAlloc` cost 24
bytes of every buffer and returned null under min-size). The
CLAUDE.md by-value convention now holds for ALL allocators.
Regression: examples/0839 (full-capacity 64+64 on a 128 buffer —
failed pre-fix; exact-fit, overflow, reset). 0129's pinned output
unchanged. .ir churn: init's signature (sret) + renumbering.
Gates: 586/586, zbt 0, m3te 23/23, game builds.
- **2026-06-11 (earlier)** — Opt-in UFCS (`a47ea14`). Agra's model:
dot-calls opt-in via `:: ufcs (...)` marker or `:: ufcs target;`
alias; plain fns direct/`|>`-only. Parser (ufcs-fn form,
FnDecl.is_ufcs), call-plan + lowering gates (calls.zig,
lower/call.zig), protocol-receiver fall-through, generic dispatch
with receiver-bound `$T`, inferGenericReturnType → buildTypeBindings
(fixes pre-existing `T{}` mis-typing of structured-param generics).
6 reliant examples migrated; mem helpers marked ufcs; specs §UFCS
rewritten; tests 0053+1166+0838. 585/585, zbt 0, m3te 23/23, game
builds. 0119 closed with the full arc in its banner.
- **2026-06-11 (later)** — Phase 2.2 shipped (`84e0fb0`). Typed helpers
in std/mem.sx (create/destroy/alloc/free/clone/resize/mem_realloc,
era-complete bodies, no zero-init); bare malloc/free dropped from
std.sx (6 example files + game pieces.sx migrated to libc_*). The
0119 "blocker" resolved as Agra's language ruling: generic free fns
are NOT dot-rewritten — fluent spelling is `|>` (parse-time desugar
→ direct call → normal monomorphization; verified on protocol +
slice receivers). specs.md §UFCS corrected; 0119 RESOLVED banner.
Regression examples/0838 (direct + |> spellings; tracker 8/8).
37 .ir re-pins (const-pool renumbering). Gates: 583/583, zbt 0,
m3te 23/23, game builds. String-clone deferred (string doesn't bind
[]$T — known gap noted).
- **2026-06-11** — Allocator primitive rename (`88bae3c`): protocol
`alloc``alloc_bytes`, `dealloc``dealloc_bytes` (2-method-era
signatures unchanged). Phase 4's naming piece pulled forward (Agra
Option A) so Phase 2.2 helpers land final-named once. Touched:
std.sx decl, mem.sx 6 impls, 4 library files, 13 examples (incl.
two custom Tracer impls — initially missed, caught by pre-pin
stdout review after the first --update captured their broken
output; restored, fixed, re-pinned), interp.zig thunk-name strings,
37 .ir snapshots. Externals migrated + gated: game (SxChess.app
builds) + m3te (23/23). Suite 582/582, zbt 0. Discriminator note:
Obj-C `.alloc()` is zero-arg; Allocator `.alloc(size)` has an arg —
the sed keyed on `\.alloc\((?!\))`.
- **2026-05-25 (latest)** — `resolveType(null) → .i64` removed.
Signature changed to non-optional `*const Node`; 12 callers
surveyed and classified. The three unguarded ones — top-level
`var_decl` at `lower.zig:630` (now mirrors lowerVarDecl's
infer-from-initializer pattern), `#objc_call` / `#jni_call` return
types (already non-optional in AST so no actual silent fallback
was reached) — handle null explicitly. The rest were guarded by
`if (x != null)` blocks; cleaned up to optional-payload syntax.
`examples/137-toplevel-var-type-inference.sx` proves the visible
win: `g_pi := 3.14;` at module scope now infers `f64` (used to be
silent `i64`). 159/159 + chess clean.
- **2026-05-25 (penultimate)** — Phase 1.4a shipped. `valueToLLVMConst`
takes IR `TypeId` (not LLVM type) + an interpreter handle.
String/slice fat pointers are serialized by capturing the
pointed-to bytes (via `interp.heapSlice` for heap_ptr, raw
process memory via new `readHostBytes` for byte_ptr / .int /
string literal) and emitting a private global byte array. Struct
/ array aggregates recurse with declared field/element TypeIds.
The trap case (`.int` landing in a ptr slot outside a fat
pointer) bails loudly. `Interpreter.heapSlice` promoted to
`pub`. Regression: `examples/136-comptime-string-global.sx`.
158/158 + chess green on all three platforms.
- **2026-05-25 (penultimate)** — Allocator `init` returns the state by
value. GPA / Arena / TrackingAllocator all changed; `Arena.deinit`
no longer self-deallocs. `UIPipeline.arena_a/_b` embedded as values;
`@self.arena_a` at the *Arena use site. `examples/50-smoke.sx`
Arena alloc counts dropped by 1 (no state struct alloc); FFI IR
snapshots regenerated to reflect new signatures (`-> ptr`
`-> i64`/`-> void sret(...)`). CLAUDE.md "Allocator construction"
rewritten around the by-value convention. 157/157 + chess green
on all three platforms via `tools/verify-step.sh`.
- **2026-05-25 (penultimate)** — `xx <lvalue>` semantics changed to borrow.
Single change at `lower.zig:10334` (`buildProtocolErasure`) gated by
new `isLvalueExpr` helper at `lower.zig:10322`. specs.md §3
ownership table extended (three modes: rvalue / lvalue / pointer).
`examples/130-xx-value-routes-through-context-allocator.sx` updated
to use a struct literal (rvalue) as the operand — the heap-copy
routing through `context.allocator` is what Phase 1.1 actually
proves, and that path is still active for rvalues. New regression
at `examples/135-xx-lvalue-borrows.sx` witnesses the borrow path
via TrackingAllocator counts on the local. 157/157 + chess green
on all three platforms (`tools/verify-step.sh` ran green
immediately before this).
- **2026-05-25 (penultimate)** — 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-i64 default for `NAME ::
#run expr;` bindings. The broader `resolveType(null) -> .i64`
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
dispatch path via new `appendDefaultArgs` helper at
`lower.zig:7974-7991`, wired in at `lower.zig:5332`. Long-lived
owners that grew internal Lists during render — `GlyphCache`,
`DockInteraction`, `StateStore`, `Gles3Gpu`, `MetalGPU` — now
capture `parent_allocator: Allocator` at init and forward it to
every internal `.append` / `.alloc` / `.dealloc`. Chess panel-text
regression (text vanished after the first move because GlyphCache
hash + entries grew into the per-frame arena and died on reset)
fixed end-to-end on macOS. specs.md §11 gains a "Default Parameter
Values" subsection documenting the existing capability. Operator-
precedence fix kept in `pipeline.sx` (`(self.frame_index & 1) == 0`
instead of `self.frame_index & 1 == 0`, which was parsing as
`self.frame_index & (1 == 0)` = always 0). All diagnostic logging
added during the bug hunt has been stripped. 155/155 example tests
green.
- **2026-05-25 (late)** — Interp silent-arm sweep (`e9df33a`).
Every `else =>` arm has a `bailDetail` reason; `.deref` /
`.unbox_any` previously silently passed through arbitrary Value
kinds, now enumerated. `#run const` errors surface a real
diagnostic via emit_llvm instead of becoming `void_val`.
CLAUDE.md REJECTED PATTERNS gained the "silent unimplemented
arms" section (`4de565b`). 154/154 + chess green.
- **2026-05-25 (mid)** — Typed raw-pointer stores (`f2b3868`).
`inst.Store` carries `val_ty: TypeId`, threaded by
`builder.store` and consumed by `storeAtRawPtr` to write exactly
the declared destination width. Regression at
`examples/132-comptime-typed-store-widths.sx` exercises every
primitive width via comptime/runtime checksum comparison.
`index_get` raw-pointer arm added (was bailing). Comptime init
errors no longer swallow into zero.
- **2026-05-25 (mid)** — Drop `matchContextAllocCall` (`d415bcc`).
Comptime now runs the full Allocator-protocol dispatch chain by
reusing the parent module instead of spinning up a fresh
ct_module. Interp gained `.int` / `.byte_ptr` arms in
`index_gep`, `store`, `marshalForeignArg`, `asString`. Closes
ISSUE-MEM-002 fully. JNI stub binding extended to call
current_ctx_ref → &__sx_default_context (used to be gated on
isExportedEntryName).
- **2026-05-25 (mid)** — Reject call-conv mismatch at bare-fn →
fn-pointer coercion (`f886d5f`). `#foreign` decls now default to
`callconv(.c)`. Library audit (`619aff8`) — all C-side callbacks
already followed the rule; documented the one remaining gap
(`xx <sx_fn> : *void` cast to opaque, ambiguous from cast alone).
Regression at `examples/131-callconv-mismatch-diagnostic.sx`.
- **2026-05-25 (early)** — Implicit-Context refactor SHIPPED
end-to-end. All 9 plan steps
(`lets-see-options-for-merry-dijkstra.md`) landed. `context` is
no longer an LLVM global; every sx function carries `__sx_ctx`
at slot 0; `context.X` reads load through `current_ctx_ref`;
`push Context.{...}` is alloca + rebind; FFI-inbound entries
install `&__sx_default_context`; interp bootstraps the default
Context on top-level call. 152/152 + unit tests green.
Commits: `29784c2` (Steps 1-2), `92c6b47` (Step 3),
`4bf5908` (Steps 5-7), `b69a2ea` (Step 8).
- **2026-05-24** — Phase 1.1 shipped: `buildProtocolValue` heap-copy
now routes through `context.allocator.alloc` via the new
`allocViaContext` helper. Regression at
`examples/130-xx-value-routes-through-context-allocator.sx`
proves a `Tracer` installed via `push Context` sees the alloc
(`Tracer.count = 1`) — interp + codegen parity. 152/152 +
chess green.
- **2026-05-24** — issue-0041 and issue-0042 both fixed end-to-end.
Also removed the silent `.i64` fallback in `resolveTypeArg`,
guarded the two upstream callers (`buildTypeBindings`,
`inferGenericReturnType`) with `type_bridge.isTypeShapedAstNode`,
and fixed three parser regressions introduced by the 0041 work
(`[:0]u8` return type, nested return-arrows, `struct #compiler`
trailing-`;` method decls). Full verify-step.sh gate green.
CLAUDE.md REJECTED PATTERNS gained the silent-fallback-defaults
prohibition. Stream now READY for Phase 1.
- **2026-05-24** — Phase 0.6 shipped (`align_of($T)` builtin).
Touchpoints: `inst.zig` BuiltinId, `lower.zig` registry +
return-type table + reflection handler, `interp.zig` fallback,
`sema.zig` builtin allowlist, `lsp/server.zig` both completion
tables, `library/modules/std.sx`. Smoke coverage added in
`examples/50-smoke.sx`. 151/151 + chess green on all platforms.
Then `size_of(*u8)` parse error was investigated — filed as
issue-0041 (pre-existing, affects both `size_of` and `align_of`).
Stream paused on 0041. Also tightened CLAUDE.md IMPASSIBLE RULES
to close the "pre-existing / non-blocking" loophole that almost
let this session roll past the issue filing.
- **2026-05-24** — issue-0040 filed. Phase 0.9 verified that
`obj.method(Type)` with a `$T: Type` parameter fails to dispatch
via dot, while explicit static (`T.method(obj, Type)`) and pipe
(`obj |> T.method(Type)`) both work. Root cause pinpoint:
`src/ir/lower.zig:5066-5123` has branches for generic-template
struct methods (5082) and non-generic qualified (5106), but no
branch for a non-template struct with a generic method. Stream
paused on 0040.
- **2026-05-24** — Phase 0.8 `#foreign("c")` syntax verified.
Form is `name :: <sig> #foreign libc ["c_name"]` with the optional
string literal supplying a rename. Confirmed via
`libc_strlen :: (s: *u8) -> usize #foreign libc "strlen";`
scratch test (interp + codegen parity).
- **2026-05-24** — issue-0039 fix verified, full
`tools/verify-step.sh` gate green again: 150/150 example tests
pass and chess builds + screenshots OK on macOS / iOS sim /
Android.
- **2026-05-24** — issue-0038 fix verified; spike now errors as
expected on transitive references. 149/149 example tests pass
(+1 vs pre-fix). Chess build broken as predicted — three-bucket
triage written up in `issues/0039-chess-needs-explicit-imports-
post-0038.md`. Stream paused on 0039.
- **2026-05-24** — Phase 0.7 spike at `/tmp/sx-import-spike/`
(`a.sx → b.sx → c.sx`) showed `a.sx` calls `c_only_fn()` and reads
`c_only_const` directly. Filed as issue-0038. Stream is paused per
the IMPASSIBLE RULE — no workaround, the libc hide-by-internal-
module strategy in the plan depends on the language semantics
matching the assumption.
- **2026-05-24** — Phase 0.2 sanity sweep landed. Five additional xx
cast patterns exercised through `tools/scratch.sh`; all show
interp/codegen parity. Combined with the issue-0037 regression at
`examples/126-xx-recover-then-dispatch.sx`, the cast story is now
considered tight for the cases the MEM plan relies on.
- **2026-05-24** — Phase 0.3 chess allocator audit recorded above.
No chess-side migration needed; ISSUE-MEM-002 (`context.allocator`
bypass) is the only thing the chess codebase is exposed to and
Phase 1 already owns it.
- **2026-05-24** — Phase 0.0a (`tools/verify-step.sh`) shipped.
Confirmed working: 145/145 example tests + chess builds + screenshots
on all 3 platforms. Initial 3-second screencap delay was too short
for Android — increased to 6 seconds; iOS sim + macOS to 5 seconds.
- **2026-05-24** — Phase 0.0b (`tools/scratch.sh`) shipped. Verified
with a hello-world snippet: interp + codegen agree.
- **2026-05-24** — Phase 0.0c initial implementation of
`TrackingAllocator` in `library/modules/allocators.sx`. Build + 145
tests pass after snapshot regen. Chess green on all 3 platforms.
Manual scratch.sh test confirms counters increment correctly when
called directly on the tracker variable.
- **2026-05-24** — ISSUE-MEM-007 fixed in `src/ir/lower.zig`.
Root cause: `emitProtocolDispatch` keyed the auto-unbox path on
`mi.ret_type == void_ptr`, but `*void` is overloaded — both
Self-disguised-as-*void AND a literal `-> *void` return appear as
the same `TypeId`. With `target_type` leaking from the enclosing
function's return type (e.g. `main -> i32`), every `*void` return
was loaded as `i32`, yielding 0 → null. Fix: stash `ret_is_self`
on `ProtocolMethodInfo` during `registerProtocolDecl` (set when
the AST return-type node is the `Self` type-expr), and gate the
unbox on that flag. Regression at
`examples/99-protocol-void-pointer-return.sx`. Sister symptom
(SIGTRAP inside struct-static method storing an Allocator field)
also fixed by the same change.
- **2026-05-24** — issue-0037 fixed in `src/ir/lower.zig`. Root
cause: `lowerXX` had a Concrete→Protocol erasure branch but no
inverse Protocol→pointer recovery — the cast fell through to
`coerceToType`, which couldn't match the (struct, pointer) shape
and returned the operand unchanged. Result: a 24-byte protocol
struct stored into an 8-byte ptr alloca, corrupting adjacent
stack (the protocol value's own slot was the next victim, so the
next dispatch loaded garbage and crashed). Fix: when src is a
protocol value and dst is a pointer, emit `struct_get` of field 0
(ctx), then bitcast `*void` → dst. Regression at
`examples/126-xx-recover-then-dispatch.sx`.
## Known issues (discovered during execution)
### ISSUE-MEM-001: Type inference defaults `p := malloc(64)` to `i64`
**Severity:** medium (workaround exists; bites unexpectedly).
**Symptom:** Writing `p := malloc(64)` (no explicit type) infers
`p: i64` instead of `p: *void`. Subsequent `free(p)` then fails LLVM
verification with "Call parameter type does not match function
signature!" because `free` expects `ptr` but receives `i64`.
**Workaround:** Explicit type annotation `p : *void = malloc(64);`
or `xx malloc(64);` at the call site.
**Reproduction:**
```sx
main :: () -> i32 {
p := malloc(64); // p inferred as i64
free(p); // LLVM verify fails: ptr expected, i64 given
0;
}
```
**Root cause:** Likely in the inference path for `:=` declarations
when the RHS is a `*void`-returning #builtin. The compiler defaults
the binding to i64 instead of matching the return type. To
investigate in a future session.
**Status:** Open. Not blocking mem.sx work but worth fixing as a
quality-of-life issue. File as `examples/issue-NNNN.sx` when
addressed.
### ISSUE-MEM-002: `context.allocator.alloc/dealloc` bypasses protocol dispatch
**Severity:** high (breaks the entire Phase 1 premise; documented
fix in Phase 1).
**Symptom:** Any code that goes through `context.allocator.alloc(size)`
or `context.allocator.dealloc(ptr)` is pattern-matched in
[src/ir/lower.zig:5137-5159](src/ir/lower.zig#L5137-L5159) and lowered
directly to `.heap_alloc`/`.heap_free` IR — which calls libc
malloc/free. The protocol-value vtable is bypassed entirely.
This means a `push Context { allocator = my_tracker }` block
followed by `context.allocator.alloc(size)` does NOT call the
tracker's `alloc` method. The tracker sees zero allocations even
though many occurred.
**Workaround:** Call the allocator directly via a variable rather
than via `context.allocator`:
```sx
push Context.{ allocator = tracker, data = null } {
p := tracker.alloc(64); // works — dispatches through tracker
tracker.dealloc(p);
}
```
But this defeats the purpose of context-allocator overriding for
user code that doesn't know about the tracker.
**Fix:** Phase 1 of the mem.sx plan removes this pattern-match and
replaces it with proper context dispatch through the Allocator
protocol. After Phase 1, `context.allocator.alloc(size)` correctly
dispatches to whatever allocator is currently set in context.
**Status:** Documented in plan; fixed in Phase 1. Blocks
auto-tracker-wrap (Phase 5 `--leak-check`).
### ISSUE-MEM-003: 08-types test depends on undefined memory contents
**Severity:** low (flaky test exposes itself when allocator code
changes).
**Symptom:** `examples/08-types.sx` declares a struct field
`c : u8 = ---;` (uninitialized) and prints the struct. The expected
snapshot captures a specific value (formerly `c: 176`, now `c: 8`)
which depends on whatever's in undefined memory at that moment. Any
allocator change shifts the value.
**Workaround:** Regenerate snapshot via `bash tests/run_examples.sh
--update` when this test fails for unrelated reasons.
**Fix:** Test should be rewritten to NOT depend on undefined memory
content — perhaps verify the field is one of N specific values, or
just don't print uninitialized data. Out of scope for mem.sx.
**Status:** Open. Document and live with it for now.
### ISSUE-MEM-004: Adding code to allocators.sx shifts JNI/Obj-C IR snapshots
**Severity:** medium (eats time per step).
**Symptom:** Every additive change to allocators.sx (new struct,
new method) cascades into ~11 IR snapshot diffs across the
`tests/expected/ffi-{jni,objc}-*.ir` files. Each step needs
`--update` + `git diff` review. The diffs are usually benign
(additive declarations) but one (`ffi-objc-call-06-sret-return.ir`)
reorganises ~1500 lines because it uses reflection on the full type
registry.
**Workaround:** Per-step `--update` + diff review.
**Fix:** Phase 0.1 spike — extend `normalize_ir()` in
`tests/run_examples.sh` to strip allocator-related declarations and
constant-pool renumberings from snapshots. Should make additive
changes invisible at the snapshot level while preserving JNI/Obj-C
ceremony signal.
**Status:** Mitigated; structural fix in Phase 0.1.
### ISSUE-MEM-005: Two-line `create(@storage)` pattern for allocators
**Severity:** low (cosmetic but real DX friction).
**Symptom:** Current pattern requires a separate storage decl + a
create call:
```sx
g_gpa : GPA = ---;
libc := GPA.create(@g_gpa);
```
Two lines per allocator. Plan committed to one-line via heap-copy:
```sx
libc := GPA.create(); // heap-copies via xx value
```
**Fix in progress:** Phase 0.0c — switch `create` methods on at
least TrackingAllocator (and possibly GPA/Arena/BufAlloc) to use
`xx value` heap-copy. Add `instance(a) -> *T` accessor for types
where users need the underlying state (TrackingAllocator).
**Status:** In progress (active work).
### ISSUE-MEM-007: Protocol dispatch on `*void`-returning methods returns null (FIXED)
**Severity:** was CRITICAL; resolved.
**Root cause:** `emitProtocolDispatch` in `src/ir/lower.zig` gated
its auto-unbox path on `mi.ret_type == void_ptr`, but the same
`TypeId` covers both Self-disguised-as-*void and a literal
`-> *void`. With `target_type` leaking from the surrounding
function (e.g. `main -> i32`), every protocol call returning
`*void` got its result loaded as `sizeof(target_type)` bytes — for
`i32` that's the first 4 bytes of the malloc'd block, which were
zero, comparing equal to null.
**Fix:** Tag `ProtocolMethodInfo` with `ret_is_self: bool`, set in
`registerProtocolDecl` when the AST return-type is the `Self`
type-expr. `emitProtocolDispatch` only takes the auto-unbox path
when `ret_is_self` is true. Literal `*void` returns are now passed
through unchanged.
**Regression:** `examples/99-protocol-void-pointer-return.sx`. The
sister symptom (SIGTRAP from protocol dispatch inside a struct
static method that stores an Allocator field) was the same root
cause and is also fixed.
**Status:** Resolved 2026-05-24.
### ISSUE-MEM-006: Android screencap needs 6+ seconds; iOS sim + macOS 5+
**Severity:** low (fixed in tooling).
**Symptom:** Initial `verify-step.sh` used `sleep 3` before
screencap. Android needed longer for the side panel to render;
without it, the screenshot showed only the chess board and a black
strip where the side panel should be.
**Fix:** Updated `verify-step.sh` delays: macOS 5s, iOS sim 5s,
Android 6s. Documented inline in the script.
**Status:** Resolved.