Files
sx/issues/0118-cast-compound-type-arg-unresolved.md

2.9 KiB

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

Symptom

cast(T) expr with a COMPOUND static type argument (*s64, []u8, ?s32, [*]s64, [4]s64, …) 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 : *s64 = cast(*s64) p;
   |                     ^^^^

Expected: the cast resolves the type argument statically and routes through coerceExplicit (for cast(*s64) p where p : *s64, a no-op), exactly as it does for bare names (cast(s32) 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 *s64 reproduces it.

Reproduction

#import "modules/std.sx";

main :: () {
    x := 42;
    p : *s64 = @x;
    q : *s64 = cast(*s64) 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(*s64) folds fine while cast(*s64) 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(?s32), cast([*]s64) 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.