Files
sx/issues/0191-coerce-to-type-no-coercibility-check-welds-garbage.md

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 as i64), 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 (i32i64, &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.)