Files
sx/issues/0118-cast-compound-type-arg-unresolved.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

3.9 KiB

0118 — cast(<compound type>) expr dies with "unresolved 'unknown_expr'"

RESOLVED (2026-06-11, same session, Agra-authorized in-session fix). Two-part root cause: (1) lowerCall lowers args BEFORE the cast handler, and compound type literals had NO expression-position lowering — they hit lowerExpr's catch-all unknown_expr error. Fixed by giving the six compound type-expr shapes (*T, [*]T, []T, ?T, [N]T, fn types) a first-class const_type lowering arm in src/ir/lower/expr.zig, mirroring named types (t : Type = *i64; now works like t : Type = f64;). (2) The cast handler's private static-type gate only accepted bare names — replaced with the canonical isStaticTypeArg (src/ir/lower/call.zig), so static compound casts route through coerceExplicit while the runtime-Type-variable form (cast(type) val in category arms) still falls through to runtime dispatch. Regression test: examples/0182-types-cast-compound-types.sx (all compound cast forms + the first-class Type value). Suite 579/579.

Symptom

cast(T) expr with a COMPOUND static type argument (*i64, []u8, ?i32, [*]i64, [4]i64, …) fails to compile with a junk diagnostic pointing at the type argument. Observed:

error: unresolved 'unknown_expr' (in probe.sx fn main)
  --> probe.sx:5:21
   |
 5 |     q : *i64 = cast(*i64) p;
   |                     ^^^^

Expected: the cast resolves the type argument statically and routes through coerceExplicit (for cast(*i64) p where p : *i64, a no-op), exactly as it does for bare names (cast(i32) 3.14 works). The spec places no scalar-only restriction on cast(Type) (specs.md "cast(Type) expr — prefix operator that converts expr to Type").

Pre-existing on master (verified on a clean build of 679653f) — independent of the in-flight const-pointer work; plain *i64 reproduces it.

Reproduction

#import "modules/std.sx";

main :: () {
    x := 42;
    p : *i64 = @x;
    q : *i64 = cast(*i64) p;   // error: unresolved 'unknown_expr'
    print("{}\n", q.*);        // expected: 42
}

Investigation prompt

The cast handler in src/ir/lower/call.zig (~line 391, the Handle cast(TargetType, val) block) gates static resolution with a PRIVATE is_static_type check that only accepts type_expr and identifier AST shapes. Every compound type-expression shape (pointer_type_expr, slice_type_expr, many_pointer_type_expr, optional_type_expr, array_type_expr, function_type_expr) falls through to the "runtime cast" builtin path, which cannot resolve it and surfaces the catch-all "unresolved 'unknown_expr'".

The codebase already has the canonical gate: Lowering.isStaticTypeArg (src/ir/lower/generic.zig:206), which type_name / type_eq use — it accepts the full compound-shape set (this is why type_name(*i64) folds fine while cast(*i64) dies). The fix likely: replace the private is_static_type block with self.isStaticTypeArg(type_arg) (keeping the scope-shadow semantics: an identifier bound to a runtime Type variable must still route to the runtime-dispatch path used by case-arm cast(type) — that path is load-bearing for any_to_string, see specs.md "runtime generic dispatch"). Then resolveTypeArg already handles the compound shapes.

Verification:

  1. Run the repro above — expect 42, exit 0.
  2. Sanity: cast([]u8), cast(?i32), cast([*]i64) forms resolve.
  3. bash tests/run_examples.sh — the case-arm runtime-dispatch examples (any_to_string formatting suite) must stay green, proving the runtime-Type-variable path still falls through to the builtin.
  4. Pin the repro as a regression example per CLAUDE.md ("Resolving an open issue").

Context note: surfaced while probing PLAN-CONST-AGG step 5 (*const T), whose const-discard diagnostic suggests "use an explicit cast (xx/cast)" — xx works for pointer-shaped targets; cast currently does not.