Files
sx/issues/0042-const-decl-type-aliases-not-resolved-as-identifier.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

4.9 KiB

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

FIXED. MyInt :: i32; size_of(MyInt) now returns 4 correctly. The resolveTypeArg .identifier branch consults type_alias_map before falling through. The fix landed alongside the broader alias-resolution work tracked in CHECKPOINT.md (Session 63's type_bridge alias-resolution extension); no specific commit isolates this issue.

Below preserved as a record of the original problem.

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 .i64 (8 bytes) — which coincidentally produces a correct result for any alias whose underlying type is 8 bytes (*T, f64, function pointers, i64, u64), silently masking the bug for years.

Observed:

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

Where MyInt :: i32;.

Reproduction

#import "modules/std.sx";

MyInt :: i32;

main :: () -> i32 {
    print("direct: {}\n", size_of(i32));    // 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 :: (i32) -> i32;). 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 .i64 (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 .i64;
},

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 (i32, 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 .i64;
},

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 :: i32; 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.).