Skip to content

Commit 9b4ea90

Browse files
committed
Fix whole-enum match scrutinee move-out and add regression coverage.
Infer match enum_type from the scrutinee for binding-only arms and add a cgen unit test for emit_match_scrutinee_whole_enum_moved_out.
1 parent 2554dcc commit 9b4ea90

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

.cursor/skills/finding-ion-bugs/references/bug-hotspots.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ CLI errors use `TypeCheckError` Debug form (`UseAfterMove { ... }`). LSP reforma
1313
## Codegen (`src/cgen/`)
1414

1515
- Drop order and `ion_drop_*` for moved fields
16-
- **Match scrutinee move-out**: pattern payload bindings null `match_val_N.data.variant_*` fields when ownership transfers (`statement_match_payload_move_neutralizes_scrutinee`); whole-enum binding arms clear active variant payloads via `emit_match_scrutinee_whole_enum_moved_out`
16+
- **Match scrutinee move-out**: pattern payload bindings null `match_val_N.data.variant_*` fields when ownership transfers (`statement_match_payload_move_neutralizes_scrutinee`); whole-enum binding arms clear active variant payloads via `emit_match_scrutinee_whole_enum_moved_out` (`whole_enum_binding_neutralizes_scrutinee_payloads`). IR infers `enum_type` from the scrutinee when arms use binding/wildcard only (`infer_match_enum_name`).
1717
- **Return unwind**: all function exits use `emit_function_return` (`ret_val`, `scope_emit_return_unwind`, `goto epilogue`), including diverging `return` inside rvalue `match` arms (`rvalue_match_divergent_return_unwinds_owned`). Value-producing rvalue arms still assign and `break` from the `switch`.
1818
- `Box`, `Vec`, `String` layout vs `runtime/ion_runtime.h`
1919
- Single-file merge (`merge_modules`) vs `--mode multi` divergences

src/cgen/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5455,6 +5455,53 @@ fn main() -> int {
54555455
);
54565456
}
54575457

5458+
#[test]
5459+
fn whole_enum_binding_neutralizes_scrutinee_payloads() {
5460+
let src = r#"enum ReadResult {
5461+
Ok(String);
5462+
Err(int);
5463+
}
5464+
5465+
fn read() -> ReadResult {
5466+
return ReadResult::Ok("hello ion\n");
5467+
}
5468+
5469+
fn main() -> int {
5470+
match read() {
5471+
r => {
5472+
return 80;
5473+
},
5474+
};
5475+
}"#;
5476+
let tokens = crate::lexer::Lexer::new(src).tokenize().unwrap();
5477+
let program = crate::parser::Parser::new(tokens).parse().unwrap();
5478+
let ir = crate::ir::IRBuilder::build(&program);
5479+
let mut cg = Codegen::new();
5480+
let c = cg.generate(&ir, "test.ion");
5481+
let binding_arm = c
5482+
.split("default: // binding r")
5483+
.nth(1)
5484+
.and_then(|tail| tail.split("goto epilogue;").next())
5485+
.unwrap_or("");
5486+
assert!(
5487+
binding_arm.contains("ReadResult r = match_val_0;"),
5488+
"expected whole-enum binding copy in:\n{c}"
5489+
);
5490+
assert!(
5491+
binding_arm.contains("switch (match_val_0.tag)"),
5492+
"expected scrutinee tag switch for whole-enum move-out in:\n{c}"
5493+
);
5494+
assert!(
5495+
binding_arm.contains("case 0:")
5496+
&& binding_arm.contains("match_val_0.data.variant_0.arg0 = NULL"),
5497+
"expected Ok string payload nulled in scrutinee switch in:\n{c}"
5498+
);
5499+
assert!(
5500+
!binding_arm.contains("ion_string_free(match_val_0"),
5501+
"expected no direct scrutinee string drop in:\n{c}"
5502+
);
5503+
}
5504+
54585505
#[test]
54595506
fn rvalue_match_divergent_return_unwinds_owned() {
54605507
let src = r#"enum E {

src/ir/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,36 @@ fn pattern_to_ir(pattern: &Pattern) -> IRPattern {
254254
}
255255
}
256256

257+
fn enum_name_from_type(ty: &Type) -> Option<String> {
258+
match ty {
259+
// Parser stores user type names as Struct until tc; enums share the same name.
260+
Type::Enum(name) | Type::Struct(name) => Some(name.clone()),
261+
Type::Generic { name, .. } => Some(name.clone()),
262+
_ => None,
263+
}
264+
}
265+
266+
fn infer_match_enum_name(expr: &Expr, ctx: &LoweringContext) -> Option<String> {
267+
if let Expr::EnumLit(lit) = expr {
268+
return Some(lit.enum_name.clone());
269+
}
270+
if let Some(ty) = ctx.resolve_expr_type(expr)
271+
&& let Some(name) = enum_name_from_type(&ty)
272+
{
273+
return Some(name);
274+
}
275+
if let Expr::Call(call) = expr
276+
&& let Some(ret) = ctx
277+
.function_returns
278+
.get(&call.callee)
279+
.and_then(|r| r.clone())
280+
&& let Some(name) = enum_name_from_type(&ret)
281+
{
282+
return Some(name);
283+
}
284+
None
285+
}
286+
257287
#[derive(Debug, Clone)]
258288
pub enum IRPattern {
259289
Variant {
@@ -893,6 +923,7 @@ fn build_expr_with_ctx(expr: &Expr, ctx: &LoweringContext) -> IREexpr {
893923
Pattern::Variant { enum_name, .. } => Some(enum_name.clone()),
894924
_ => None,
895925
})
926+
.or_else(|| infer_match_enum_name(&match_expr.expr, ctx))
896927
.unwrap_or_else(|| "Unknown".to_string());
897928

898929
IREexpr::Match {

0 commit comments

Comments
 (0)