diff --git a/issues/0118-cast-compound-type-arg-unresolved.md b/issues/0118-cast-compound-type-arg-unresolved.md new file mode 100644 index 0000000..85778ee --- /dev/null +++ b/issues/0118-cast-compound-type-arg-unresolved.md @@ -0,0 +1,71 @@ +# 0118 — `cast() 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 + +```sx +#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.