Skip to content

Commit f09a86d

Browse files
committed
fix(analyzer): fold constant switch arm conditions like an if chain
1 parent 3580c28 commit f09a86d

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

crates/analyzer/src/conv/statement.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,18 @@ impl Conv<&SwitchStatement> for ir::StatementBlock {
10581058
SwitchItemGroup::Defaul(_) => None,
10591059
};
10601060

1061+
// A dead arm's out-of-range select must not reach the range check or the
1062+
// simulator, and an always-taken arm must not leave the chain with an
1063+
// empty false side that reads as an uncovered branch.
1064+
let (true_side_only, false_side_only) = match &cond {
1065+
Some(cond) => eval_cond_true_false(context, cond),
1066+
None => (false, false),
1067+
};
1068+
1069+
if false_side_only {
1070+
continue;
1071+
}
1072+
10611073
let convert = |c: &mut Context| -> IrResult<ir::StatementBlock> {
10621074
match item.switch_item.switch_item_group0.as_ref() {
10631075
// A bare statement arm has no StatementBlockItem wrapper, so
@@ -1078,6 +1090,12 @@ impl Conv<&SwitchStatement> for ir::StatementBlock {
10781090

10791091
match cond {
10801092
Some(cond) => {
1093+
// Nothing after this arm can run, including `default` - which is
1094+
// the fallback no matter where it was listed.
1095+
if true_side_only {
1096+
default = true_side.0;
1097+
break;
1098+
}
10811099
arms.push((cond, true_side.0, item.switch_item.as_ref().into()));
10821100
}
10831101
None => {

crates/analyzer/src/ir/tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,3 +3280,59 @@ fn const_fold_else_if() {
32803280

32813281
check_ir(code, exp);
32823282
}
3283+
3284+
#[test]
3285+
fn const_fold_switch_arm() {
3286+
let code = r#"
3287+
module ModuleA (
3288+
a: input logic ,
3289+
o: output logic<4>,
3290+
) {
3291+
always_comb {
3292+
for i in 0..4 {
3293+
switch {
3294+
i >: 3 : o[i] = 0;
3295+
a : o[i] = 1;
3296+
i <: 4 : o[i] = 0;
3297+
default : o[i] = 1;
3298+
}
3299+
}
3300+
}
3301+
}
3302+
"#;
3303+
3304+
let exp = r#"module ModuleA {
3305+
input var0(a): logic = 1'hx;
3306+
output var1(o): logic<4> = 4'hx;
3307+
const var2([0].i): signed bit<32> = 32'sh00000000;
3308+
const var3([1].i): signed bit<32> = 32'sh00000001;
3309+
const var4([2].i): signed bit<32> = 32'sh00000002;
3310+
const var5([3].i): signed bit<32> = 32'sh00000003;
3311+
3312+
comb {
3313+
if var0 {
3314+
var1[32'sh00000000] = 32'sh00000001;
3315+
} else {
3316+
var1[32'sh00000000] = 32'sh00000000;
3317+
}
3318+
if var0 {
3319+
var1[32'sh00000001] = 32'sh00000001;
3320+
} else {
3321+
var1[32'sh00000001] = 32'sh00000000;
3322+
}
3323+
if var0 {
3324+
var1[32'sh00000002] = 32'sh00000001;
3325+
} else {
3326+
var1[32'sh00000002] = 32'sh00000000;
3327+
}
3328+
if var0 {
3329+
var1[32'sh00000003] = 32'sh00000001;
3330+
} else {
3331+
var1[32'sh00000003] = 32'sh00000000;
3332+
}
3333+
}
3334+
}
3335+
"#;
3336+
3337+
check_ir(code, exp);
3338+
}

crates/analyzer/src/tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8951,6 +8951,25 @@ fn unassign_variable() {
89518951

89528952
let errors = analyze(code);
89538953
assert!(errors.is_empty());
8954+
8955+
// With every arm folded away and no `default`, nothing drives `o` - reported
8956+
// as unassigned, not as an uncovered branch on always-false `if`s.
8957+
let code = r#"
8958+
module ModuleA (
8959+
o: output logic<4>,
8960+
) {
8961+
always_comb {
8962+
for i in 0..4 {
8963+
switch {
8964+
i >: 10: o[i] = 1;
8965+
}
8966+
}
8967+
}
8968+
}
8969+
"#;
8970+
8971+
let errors = analyze(code);
8972+
assert!(matches!(errors[0], AnalyzerError::UnassignVariable { .. }));
89548973
}
89558974

89568975
#[test]
@@ -9981,6 +10000,72 @@ fn uncovered_branch() {
998110000

998210001
let errors = analyze(code);
998310002
assert!(errors.is_empty());
10003+
10004+
// A compile-time-true `switch` arm makes the missing `default` unreachable.
10005+
let code = r#"
10006+
module ModuleA (
10007+
i_a: input logic ,
10008+
o : output logic<4>,
10009+
) {
10010+
always_comb {
10011+
for i in 0..4 {
10012+
switch {
10013+
i_a : o[i] = 1;
10014+
i <: 4 : o[i] = 0;
10015+
}
10016+
}
10017+
}
10018+
}
10019+
"#;
10020+
10021+
let errors = analyze(code);
10022+
assert!(errors.is_empty());
10023+
}
10024+
10025+
#[test]
10026+
fn switch_const_false_arm_body_unchecked() {
10027+
// A dead arm's body is never converted, so conversion-time checks stop seeing
10028+
// it. That is the point here: unrolling the `for` puts this select out of range.
10029+
let code = r#"
10030+
module ModuleA (
10031+
o: output logic<4>,
10032+
) {
10033+
always_comb {
10034+
for i in 0..4 {
10035+
switch {
10036+
i >: 3 : o[i + 1] = 1;
10037+
default: o[i] = 0;
10038+
}
10039+
}
10040+
}
10041+
}
10042+
"#;
10043+
10044+
let errors = analyze(code);
10045+
assert!(errors.is_empty());
10046+
10047+
// The same skip also hides condition-independent errors like a wrong argument
10048+
// count. `if` behaves this way too; pinned so `switch` is revisited with it.
10049+
let code = r#"
10050+
module ModuleA (
10051+
o: output logic<4>,
10052+
) {
10053+
function f (a: input logic) -> logic {
10054+
return a;
10055+
}
10056+
always_comb {
10057+
for i in 0..4 {
10058+
switch {
10059+
i >: 10: o[i] = f(1, 2, 3);
10060+
default: o[i] = 0;
10061+
}
10062+
}
10063+
}
10064+
}
10065+
"#;
10066+
10067+
let errors = analyze(code);
10068+
assert!(errors.is_empty());
998410069
}
998510070

998610071
#[test]

0 commit comments

Comments
 (0)