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.
954 lines
49 KiB
Markdown
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.
|