2.7 KiB
0191 — coerceToType silently welds a type-incompatible value into the return slot (no diagnostic)
Status: OPEN
Symptom
Returning a value whose type is not coercible to the declared return
type (e.g. a string where i64 is expected) is silently accepted:
the compiler reinterprets the bytes instead of emitting a type-mismatch
diagnostic, producing garbage at runtime with exit 0.
- Observed:
bad :: () -> i64 { "not an int" }compiles and runs, printing a garbage integer (the string's pointer reinterpreted asi64), exit 0. - Expected: a clear "cannot return 'string' where 'i64' is expected" (type-mismatch) diagnostic + non-zero exit.
This is the silent-clobber failure mode the project forbids: a bad coercion fabricated rather than rejected.
Reproduction
#import "modules/std.sx";
bad :: () -> i64 { "not an int" } // trailing expression
main :: () { print("{}\n", bad()); }
Run: ./zig-out/bin/sx run repro.sx → exit 0, prints e.g. 4352919116
(should error). The explicit-return form is identical:
bad :: () -> i64 { return "not an int"; }
It is not failable-specific — a plain non-failable return reproduces
it. A value-failable -> i64 !E { "not an int" } welds the 16-byte
string struct into the declared i64 slot (ret { i64, i32 } { {ptr,i64} ..., i32 0 }), so the caller's catch can even phantom-fire on the
garbage tag.
Investigation prompt
Root cause: coerceToType (in src/ir/lower/ — grep for pub fn coerceToType) performs the requested coercion (or a bit-reinterpret)
WITHOUT first checking that the source type is actually coercible to the
destination. The value/explicit-return/failable-success return paths all
call it and trust the result. The companion lowerFailableSuccessReturn
inherits the same gap.
Likely fix: coerceToType should validate coercibility (the same rules
used for assignment / call-argument coercion) and, when the source type
cannot coerce to the destination, emit a
diagnostics.addFmt(.err, span, "cannot coerce '<src>' to '<dst>'...")
and return a sentinel (or have callers handle a null/.unresolved),
rather than reinterpreting bytes. Thread the offending value's span
through (the return paths already plumb a span — see issue 0190's
lowerValueBody/lowerReturn work).
Verification: the repros above must error with a type-mismatch diagnostic
- exit 1; legitimate coercions (
i32→i64,&T→*T, numeric widening, struct-literal → struct, etc.) must keep working; the full corpus must stay green. Add a diagnostics regression example for the incompatible-return case.
(Found by adversarial review during the issue-0190 fix, commit
df1327e3. Pre-existing — independent of the 0190 change; reproduces on
plain non-failable returns.)