// Regression: `match` in statement position where every arm body // either (a) ends in a void-returning fn call, or (b) terminates // the basic block (via `return`). Both cases previously crashed // the compiler: // (a) the phi over arm-produced values was built with a void // type — LLVM segfaults on Phi with void type. // (b) the merge block was created and the builder positioned // there, but no arm reached it; combined with a non-void // return type on the surrounding fn, the function had a // trailing unterminated block and emission segfaulted. // // Fix: // (a) detect void phi type before building the phi; return a // discarded sentinel instead. // (b) terminate the dead merge block with `unreachable` so the // IR verifies. const { assert } = import("test"); // (a) Void user-fn calls in every arm — statement-form match. // Each arm calls a different void helper, so the per-arm body's // "value" is a void call instruction (which used to leak into the // phi-type lookup). fn voidHelperA() {} fn voidHelperB() {} fn voidHelperC() {} fn voidHelperD() {} fn dispatchVoid(sel: i32) { match (sel) { 0 { voidHelperA(); } 1 { voidHelperB(); } 2 { voidHelperC(); } _ { voidHelperD(); } } } tfn matchWithVoidArmsCompiles() { // Pre-fix this segfaulted the compiler before any code ran. // No side effect to verify — the existence of a passing test // proves the codegen path succeeded. dispatchVoid(0); dispatchVoid(1); dispatchVoid(2); dispatchVoid(99); assert(1, 1); } // (b) Match-as-tail with `return` in every arm. Surrounding fn has // a non-void return type. Pre-fix the trailing merge block had no // terminator and LLVM emission segfaulted. fn dispatchReturn(sel: i32) i32 { match (sel) { 0 { return 10; } 1 { return 20; } 2 { return 30; } _ { return 99; } } } tfn matchAsTailWithReturnsCompiles() { assert(dispatchReturn(0), 10); assert(dispatchReturn(1), 20); assert(dispatchReturn(2), 30); assert(dispatchReturn(100), 99); } // Combined: hex-literal patterns, multi-arg call sites, scrutinee // is a function call. Mirrors the user-reported pattern. Verified // via per-arm helpers that return distinct values. fn maskOp(opc: u32) u32 { return (opc >> 21) & 0x1f; } fn handle00(c: i32) i32 { return c + 1; } fn handle04(c: i32) i32 { return c + 2; } fn handle10(c: i32) i32 { return c + 3; } fn handleDefault(c: i32) i32 { return c + 99; } fn dispatchUserPattern(c: i32, opc: u32) i32 { match (maskOp(opc)) { 0x00 { return handle00(c); } 0x04 { return handle04(c); } 0x10 { return handle10(c); } _ { return handleDefault(c); } } } tfn userReportedPatternCompiles() { assert(dispatchUserPattern(0, 0x00000000), 1); // mask=0x00 assert(dispatchUserPattern(0, 0x00800000), 2); // mask=0x04 assert(dispatchUserPattern(0, 0x02000000), 3); // mask=0x10 assert(dispatchUserPattern(0, 0xffffffff), 99); // mask=0x1f } // Mixed-arm shape: one arm ends in a void fn call, another in a real // value. The phi-type detection must skip the void arm and pick the // value arm's type; the void arm gets a typed-zero substitute. A // regression for that picking logic. fn mixedMatch(sel: i32) i32 { return match (sel) { 0 { voidHelperA(); 0 } 1 { 42 } _ { 99 } }; } tfn mixedVoidAndValueArmsCompile() { // Each arm produces a concrete i32; the void helper call sits as // a discarded statement before the arm's trailing expression. assert(mixedMatch(0), 0); assert(mixedMatch(1), 42); assert(mixedMatch(100), 99); } // Peer-type resolution across arms of different natural widths. // `100` lowers as i8; `200` lowers as i8; `300` doesn't fit in i8 // and lowers as i16. Without peer-type widening at phi-build time, // the wildcard arm's value would be substituted with typed zero // because phi-type would be picked from arm 1 (i8) and arm 3's i16 // value wouldn't match — peer-type widening fixes that. fn pickByWidth(sel: i32, gate: bool) i32 { var r: i32 = 0; if (gate) { r = match (sel) { 1 { 100 } 2 { 200 } _ { 300 } }; } return r; } tfn matchPromotesNarrowerArmsToWidestType() { assert(pickByWidth(1, true), 100); assert(pickByWidth(2, true), 200); assert(pickByWidth(99, true), 300); // pre-fix this returned 0 assert(pickByWidth(99, false), 0); } // Catch-all (`_`) must not generate a redundant `br i1 true, body, // next` conditional branch — the dead-edge predecessor would leave // the merge block's phi with a missing incoming pair and produce // malformed IR. Side-effect arms (no value, no phi at merge) are // still affected by the routing bug — a misroute would be visible // at runtime via wrong-output / bus errors. fn dispatchSideEffects(x: i32, gate: bool) i32 { var hits: i32 = 0; if (gate) { match (x) { 1 { hits = 11; } 2 { hits = 22; } _ { hits = 99; } } } return hits; } tfn catchAllArmRoutesCorrectly() { assert(dispatchSideEffects(1, true), 11); assert(dispatchSideEffects(2, true), 22); assert(dispatchSideEffects(7, true), 99); // wildcard assert(dispatchSideEffects(0, false), 0); } // Signed-mixed-width arms. Before expected-type propagation, arm 1's // `-1` lowered as i8 signed and arm 2's `300` lowered as i16 unsigned. // Phi was i16 with the wildcard's value substituted via ZExt → -1 became // 255 (i16). Now the var-decl's `i32` is threaded into each arm's tail // expression, so both literals lower as i32 from the start; the phi is // i32 directly and signed semantics are preserved. Mirrors Rust's // rustc_hir_typeck/src/_match.rs:67-97 where each arm is checked with // the expected type. fn signedPick(sel: i32) i32 { return match (sel) { 1 { -1 } _ { 300 } }; } tfn signedMixedWidthArmsPreserveSign() { assert(signedPick(1), -1); assert(signedPick(2), 300); }