From 6b8bce1aba43963d4a85c01c2539880986098300 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 25 Jun 2026 22:40:43 +0300 Subject: [PATCH] docs: file issue 0191 (coerceToType welds type-incompatible value into return slot, no diagnostic) --- ...ype-no-coercibility-check-welds-garbage.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 issues/0191-coerce-to-type-no-coercibility-check-welds-garbage.md diff --git a/issues/0191-coerce-to-type-no-coercibility-check-welds-garbage.md b/issues/0191-coerce-to-type-no-coercibility-check-welds-garbage.md new file mode 100644 index 00000000..fa12fa43 --- /dev/null +++ b/issues/0191-coerce-to-type-no-coercibility-check-welds-garbage.md @@ -0,0 +1,69 @@ +# 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 + +```sx +#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: + +```sx +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 '' to ''...")` +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.)