Files
sx/issues/0042-const-decl-type-aliases-not-resolved-as-identifier.md
agra 29784c22a8 mem: implicit-context foundation + many compiler fixes
The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):

  Step 1 — `CAllocator :: struct {}` stateless allocator in
    library/modules/allocators.sx, delegating directly to
    libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
    `func_ref: FuncId` leaf so nested aggregates can carry function
    pointers (the inline Allocator value's fn-ptr fields). Switch
    sites updated in emit_llvm.zig, print.zig, interp.zig.

  Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
    a static `__sx_default_context` global with a nested-aggregate
    init_val pointing at the CAllocator → Allocator thunks. The
    second-pass `initVtableGlobals` in emit_llvm.zig is generalised
    to handle `.aggregate` init_vals (re-emits after func_map is
    populated so func_ref leaves resolve to real symbols).

Also folded in from earlier work this session:

  - Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
    through `context.allocator` via the new `allocViaContext` helper.
  - interp.zig: `marshalForeignArg` double-offset bug fixed —
    `heapSlice` already adds `hp.offset` to the slice ptr, so the
    extra `+ hp.offset` was scribbling memcpy/memset into adjacent
    heap state, corrupting `heap.items[0]`. Symptom: `build_format`
    at comptime produced zero bytes, all `print` calls failed.
  - Lazy lowering: `lazyLowerFunction` now declares foreign-body
    functions as extern stubs in the local (comptime) module so
    cross-module foreign calls resolve.
  - Allocator API: all stdlib allocators on one-line `init() -> *T`
    (CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
    backed; BufAlloc: embeds state at head of user buffer).
  - issues 0038 (transitive #import), 0039 (chess + stdlib migration
    fallout), 0040 (generic struct method dot-dispatch), 0041
    (pointer types as type-arg), 0042 (alias name resolution) — all
    fixed; regression tests in examples/.
  - Diagnostic: `emitError` now embeds the lowering's
    `current_source_file` and enclosing function in the literal
    message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
    emit site so misattributed spans can't hide where the failure
    is.
  - tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
    (interp/codegen parity tester) added.

Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
2026-05-24 22:59:20 +03:00

4.6 KiB

issue-0042 — Const-decl type aliases (MyInt :: s32;) silently return .s64 from size_of / align_of

Symptom

A type alias declared via Foo :: SomeType; is registered in the lowering's type_alias_map but is never consulted when the alias name is later used as a type argument to size_of / align_of. The fallback returns .s64 (8 bytes) — which coincidentally produces a correct result for any alias whose underlying type is 8 bytes (*T, f64, function pointers, s64, u64), silently masking the bug for years.

Observed:

size_of(s32)    = 4    ← direct, correct
size_of(MyInt)  = 8    ← via alias, WRONG (expected 4)

Where MyInt :: s32;.

Reproduction

#import "modules/std.sx";

MyInt :: s32;

main :: () -> s32 {
    print("direct: {}\n", size_of(s32));    // 4
    print("alias:  {}\n", size_of(MyInt));  // 8 — should be 4
    0;
}

./zig-out/bin/sx run against unmodified master prints:

direct: 4
alias:  8

Why this surfaces now

issue-0041 work extends the const-decl alias path to register pointer, optional, array, slice, many-pointer, and function-type aliases (Ptr :: *u8;, Maybe :: ?u8;, Arr :: [3]u8;, Cb :: (s32) -> s32;). Every one of those aliases ends up in type_alias_map, then size_of(<alias>) falls through the same .identifier branch that ignores the map — returning .s64 (8). For pointer and function-type aliases this is coincidentally right (8 bytes). For optional, array, etc. it produces silently-wrong sizes (size_of(Maybe) = 8 instead of 2; size_of(Arr) = 8 instead of 3).

The issue-0041 work cannot land without this being fixed — the test snapshots would pin in the wrong values and the new feature would ship subtly broken.

Investigation prompt

The bug lives in src/ir/lower.zig, in resolveTypeArg (line ~7132). The .identifier branch looks like:

.identifier => |id| {
    if (self.type_bindings) |tb| {
        if (tb.get(id.name)) |ty| return ty;
    }
    const name_id = self.module.types.internString(id.name);
    return self.module.types.findByName(name_id) orelse .s64;
},

It checks type_bindings (generic-monomorphization) and findByName (registered named types), but never consults self.type_alias_map — which is where the const-decl alias registration in lower.zig:425 puts entries. The neighbouring .type_expr branch (line ~7143) DOES check type_alias_map:

.type_expr => |te| {
    if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
    return type_bridge.resolveAstType(node, &self.module.types);
},

Why two branches: an .identifier AST node is what parsePrimary emits for non-keyword names; .type_expr is what it emits for built-in primitive names recognised by Type.fromName (s32, u8, etc.) and for the f32/f64/Type keywords. User-defined alias names like MyInt and Ptr flow through .identifier.

Likely fix: mirror the type_alias_map.get lookup in the .identifier branch — try alias map first (or before/after findByName, whichever is the established precedence elsewhere).

.identifier => |id| {
    if (self.type_bindings) |tb| {
        if (tb.get(id.name)) |ty| return ty;
    }
    if (self.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
    const name_id = self.module.types.internString(id.name);
    return self.module.types.findByName(name_id) orelse .s64;
},

Verification:

  1. Add the repro above as examples/issue-0042.sx.
  2. bash tests/run_examples.sh --update to capture expected output (alias: 4, not alias: 8).
  3. Make sure existing snapshots that test type aliases (search examples/ for :: patterns followed by size_of) don't change in unexpected ways.

Possible adjacency: the issue may extend to align_of (likely same call path) and to type-alias chains (A :: s32; B :: A; — does B resolve through A's alias entry?). Worth pinning down with a test once the primary fix lands.

Plan-level impact

Blocks issue-0041 (compound-type-as-expression). Once 0042 is fixed, 0041 work can resume from the testing phase (the parser and lowering edits for 0041 are already in place; only the alias lookup is broken).

Suggested fix order

  1. Land 0042's .identifier alias-map lookup.
  2. Resume 0041 from the test step — re-run examples/issue-0041.sx and verify size_of(Maybe) = 2, size_of(Arr) = 3, etc.
  3. Regenerate snapshots and proceed with the 0041 finishing steps (50-smoke, rename, etc.).