use super::*;
fn assert_comb_loop(case: &str, code: &str, expected: bool) {
let errors = analyze(code);
let actual = errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. }));
assert_eq!(actual, expected, "{case}: {errors:?}");
}
fn assert_complete_comb_loop(code: &str, expected: bool) {
let errors = analyze(code);
assert!(
errors
.iter()
.all(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"{code}\n{errors:#?}"
);
assert_eq!(!errors.is_empty(), expected, "{code}\n{errors:#?}");
assert!(comb_loop_analysis_is_complete(code), "{code}");
}
#[test]
fn comb_loop_rhs_sampling_preserves_initializer_side_effects() {
for (ty, literal, used) in [
("bit[2]", "'{0, 0, default: effect()}", false),
("bit[2]", "'{default: effect(), 0, 0}", false),
("bit[2]", "'{effect() repeat 0, 0, 0}", false),
("bit[2]", "'{effect() repeat (1 - 1), 0, 0}", false),
("bit<2>", "'{0, 0, default: effect()}", false),
(
"bit<2, 2>",
"'{'{0, 0}, '{0, 0}, default: '{effect(), 0}}",
false,
),
("bit<2>[2]", "'{'{0, 0, default: effect()}, '{0, 0}}", false),
(
"bit<2>[3]",
"'{(if enable ? '{0, 0, default: effect()} : '{0, 0, default: effect()}), 0, 0}",
false,
),
(
"bit<2>[3]",
"'{(if enable ? '{0, default: effect()} : '{0, default: effect()}), 0, 0}",
true,
),
(
"bit[2]",
"if enable ? '{0, 0, default: effect()} : '{0, 0}",
false,
),
("bit[2, 2]", "'{'{0, 0, default: effect()}, '{0, 0}}", false),
(
"bit[2, 2]",
"'{'{effect(), 0} repeat 0, '{0, 0} repeat 2}",
false,
),
(
"bit[2, 2]",
"'{'{0, 0} repeat 2, default: '{effect(), 0}}",
false,
),
("bit[2]", "'{0, default: effect()}", true),
("bit[2]", "'{effect() repeat 1, 0}", true),
("bit[2]", "'{effect() repeat 2}", true),
("bit[2, 2]", "'{'{0, default: effect()}, '{0, 0}}", true),
("bit[2, 2]", "'{'{0, 0}, default: '{effect(), 0}}", true),
("bit<2>", "{effect() repeat 0, 2'b0}", true),
("bit<2>", "{effect() repeat (1 - 1), 2'b0}", true),
("bit<2>", "{effect() repeat 1, 1'b0}", true),
] {
for boundary in ["return", "argument", "assignment"] {
for (initial, effect, expected) in [("o", "0", !used), ("0", "o", used)] {
let (declaration, result_type, expression) = match boundary {
"return" => (
format!("function make() -> Value {{ return {literal}; }}"),
"Value",
"make()".to_string(),
),
"argument" => (
"function consume(value: input Value) -> bit { return 0; }".to_string(),
"bit",
format!("consume({literal})"),
),
"assignment" => (String::new(), "Value", literal.to_string()),
_ => unreachable!(),
};
let code = format!(
r#"
module Top(enable: input bit, o: output bit) {{
type Value = {ty};
var saved: bit;
var result: {result_type};
function effect() -> bit {{ saved = {effect}; return 0; }}
{declaration}
always_comb {{
saved = {initial};
result = {expression};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_evaluates_packed_repeats_once() {
for count in [0, 1, 3] {
let literal = format!("{{advance() repeat {count}, 1'b0}}");
for boundary in ["assignment", "argument", "return"] {
let (declaration, expression) = match boundary {
"assignment" => (String::new(), literal.clone()),
"argument" => (
"function consume(x: input bit<4>) -> bit<4> { return 0; }".to_string(),
format!("consume({literal})"),
),
"return" => (
format!("function make() -> bit<4> {{ return {literal}; }}"),
"make()".to_string(),
),
_ => unreachable!(),
};
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit<2>;
var result: bit<4>;
function advance() -> bit {{ saved = saved << 1; return 0; }}
{declaration}
always_comb {{
saved = o;
result = {expression};
}}
assign o = saved[1];
}}
"#
);
assert_complete_comb_loop(&code, true);
}
}
}
#[test]
fn comb_loop_rhs_sampling_discards_zero_repeat_value_dependencies() {
for count in [0, 1] {
for operand in ["value", "read()"] {
for suffix in ["", " + 2'b0"] {
let literal = format!("{{1'b0, {operand} repeat {count}}}{suffix}");
for boundary in ["assignment", "argument", "return"] {
let (declaration, expression) = match boundary {
"assignment" => (String::new(), literal.clone()),
"argument" => (
"function identity(x: input bit<2>) -> bit<2> { return x; }"
.to_string(),
format!("identity({literal})"),
),
"return" => (
format!("function make() -> bit<2> {{ return {literal}; }}"),
"make()".to_string(),
),
_ => unreachable!(),
};
let code = format!(
r#"
module Top(o: output bit<2>) {{
var value: bit;
function read() -> bit {{ return value; }}
{declaration}
assign value = o[0];
always_comb {{ o = {expression}; }}
}}
"#
);
assert_complete_comb_loop(&code, count != 0);
}
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_uses_lowered_array_element_shapes() {
for condition in ["enable", "1'b1", "1'b0"] {
for count in [1, 2] {
let packed = format!("'{{0 repeat {count}, default: effect()}}");
let element = format!("(if {condition} ? {packed} : {packed})");
let row = format!("'{{{element}, 0 repeat 2}}");
let inner = format!("'{{{packed}, 0 repeat 3}}");
let partial =
format!("'{{(if {condition} ? {inner} : {inner}), '{{0 repeat 4}} repeat 2}}");
for (ty, select, expression) in [
("bit<2>[3]", "", row.clone()),
("bit<2>[2, 3]", "[1]", row.clone()),
("bit<2>[2, 3]", "", format!("'{{{row} repeat 2}}")),
("bit<2>[4, 3]", "[1+:2]", format!("'{{{row} repeat 2}}")),
("bit<2>[3, 4]", "", partial.clone()),
("bit<2>[2, 3, 4]", "[1]", partial),
("bit<2>[3]", "", format!("if {condition} ? {row} : {row}")),
] {
let used = count < 2;
for (initial, effect, expected) in [("o", "0", !used), ("0", "o", used)] {
let code = format!(
r#"
module Top(enable: input bit, o: output bit) {{
var saved: bit;
var result: {ty};
function effect() -> bit {{ saved = {effect}; return 0; }}
always_comb {{
saved = {initial};
result = '{{default: 0}};
result{select} = {expression};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_preserves_lowered_element_operand_width() {
for condition in ["1'b1", "1'b0"] {
for count in [2, 4] {
let used = condition == "1'b1" && count < 4;
for (initial, effect, expected) in [("o", "0", !used), ("0", "o", used)] {
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit;
var result: bit<4>[5];
function effect() -> bit {{ saved = {effect}; return 0; }}
always_comb {{
saved = {initial};
result = '{{(if {condition} ? '{{0 repeat {count}, default: effect()}} : 4'b0), 0 repeat 4}};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_preserves_operand_width_before_narrowing() {
for width in [1, 2, 4, 8] {
for count in [2, 4] {
for operator in ["|", "^", "&", "+"] {
let literal = format!("'{{0 repeat {count}, default: effect()}}");
for expression in [
format!("4'b0 {operator} {literal}"),
format!("{literal} {operator} 4'b0"),
] {
let used = count < width.max(4);
for (initial, effect, expected) in [("0", "o", used), ("o", "0", !used)] {
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit;
var result: bit;
function effect() -> bit {{ saved = {effect}; return 0; }}
function consume(x: input logic<{width}>) -> bit {{ return 0; }}
always_comb {{
saved = {initial};
result = consume({expression});
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_uses_selected_destination_width() {
for (select, width) in [
("[0]", 1),
("[1:0]", 2),
("[0+:2]", 2),
("[3-:2]", 2),
("[1 step 2]", 2),
("[7:0]", 8),
("", 8),
] {
for operator in ["|", "^", "&", "+"] {
for count in [2, 4] {
let used = count < width.max(4);
for (initial, effect, expected) in [("o", "0", !used), ("0", "o", used)] {
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit;
var result: logic<8>;
function effect() -> bit {{ saved = {effect}; return 0; }}
always_comb {{
saved = {initial};
result = 0;
result{select} = 4'b0 {operator} '{{0 repeat {count}, default: effect()}};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_preserves_selected_packed_dimensions() {
for (ty, select) in [
("logic<8, 2>", "[1:0]"),
("logic<8, 2>", "[0+:2]"),
("logic<8, 2>", "[3-:2]"),
("logic<8, 2, 2>", "[0]"),
] {
for count in [1, 2] {
let literal = format!("'{{'{{0, 0}} repeat {count}, default: '{{effect(), 0}}}}");
let used = count < 2;
for (initial, effect, expected) in [("o", "0", !used), ("0", "o", used)] {
let code = format!(
r#"
module Top(enable: input bit, o: output bit) {{
var saved: bit;
var result: {ty};
function effect() -> bit {{ saved = {effect}; return 0; }}
always_comb {{
saved = {initial};
result = 0;
result{select} = if enable ? {literal} : {literal};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_precedes_selector_side_effects() {
for select in [
"[address()]",
"[address()+:2]",
"[address()+1-:2]",
"[address() step 2]",
] {
for count in [2, 4] {
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit;
var observed: bit;
var result: logic<8>;
function effect() -> bit {{ saved = o; return 0; }}
function address() -> u32 {{ observed = saved; saved = 0; return 0; }}
always_comb {{
saved = 0;
observed = 0;
result = 0;
result{select} = 4'b0 | '{{0 repeat {count}, default: effect()}};
}}
assign o = observed;
}}
"#
);
assert_complete_comb_loop(&code, count < 4);
}
}
}
#[test]
fn comb_loop_rhs_sampling_preserves_width_boundaries_and_array_shapes() {
for (ty, expression, used) in [
("logic<1>", "~(4'b0 | '{0, 0, default: effect()})", true),
("logic<1>", "-(4'b0 | '{0, 0, default: effect()})", true),
(
"logic<1>",
"if 1'b1 ? '{0, 0, default: effect()} : 4'b0",
true,
),
(
"logic<1>",
"if 1'b0 ? '{0, 0, default: effect()} : 4'b0",
false,
),
("logic<1>", "(4'b0 | '{0, 0, default: effect()}) as 1", true),
("logic<8>", "'{0, 0, default: effect()} as 2", false),
(
"logic<8>",
"4'b0 == '{0 repeat 4, default: effect()}",
false,
),
(
"logic<8>",
"|(4'b0 | '{0 repeat 4, default: effect()})",
false,
),
(
"logic<8>",
"4'b0 << (4'b0 | '{0 repeat 4, default: effect()})",
false,
),
(
"logic<8>",
"{(4'b0 | '{0 repeat 4, default: effect()}), 1'b0}",
false,
),
(
"logic<1>[8]",
"'{4'b0 | '{0, 0, default: effect()}, 0 repeat 7}",
true,
),
(
"logic<1>[8]",
"'{4'b0 | '{0 repeat 4, default: effect()}, 0 repeat 7}",
false,
),
] {
for boundary in ["return", "argument", "assignment"] {
for (initial, effect, expected) in [("0", "o", used), ("o", "0", !used)] {
let (declaration, result_type, expression) = match boundary {
"return" => (
format!("function make() -> Value {{ return {expression}; }}"),
"Value",
"make()".to_string(),
),
"argument" => (
"function consume(x: input Value) -> bit { return 0; }".to_string(),
"bit",
format!("consume({expression})"),
),
"assignment" => (String::new(), "Value", expression.to_string()),
_ => unreachable!(),
};
let code = format!(
r#"
module Top(o: output bit) {{
type Value = {ty};
var saved: bit;
var result: {result_type};
function effect() -> bit {{ saved = {effect}; return 0; }}
{declaration}
always_comb {{
saved = {initial};
result = {expression};
}}
assign o = saved;
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
}
}
#[test]
fn comb_loop_rhs_sampling_evaluates_defaults_after_explicit_elements() {
for (ty, literal, selection, expected) in [
("bit[2]", "'{saved, default: clear()}", "[0]", true),
("bit[2]", "'{default: clear(), saved}", "[0]", true),
("bit[2]", "'{clear(), default: saved}", "[1]", false),
("bit[2]", "'{default: saved, clear()}", "[1]", false),
(
"bit[2, 2]",
"'{'{saved, default: clear()}, '{0, 0}}",
"[0][0]",
true,
),
(
"bit[2, 2]",
"'{'{default: saved, clear()}, '{0, 0}}",
"[0][1]",
false,
),
] {
let code = format!(
r#"
module Top(o: output bit) {{
type Value = {ty};
var saved: bit;
var result: Value;
function clear() -> bit {{ saved = 0; return 0; }}
function make() -> Value {{ return {literal}; }}
always_comb {{
saved = o;
result = make();
}}
assign o = result{selection};
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
#[test]
fn comb_loop_rhs_sampling_keeps_comparison_operands_before_destination_selectors() {
let code = r#"
module Top(o: output bit) {
var saved: bit;
var result: logic<2>;
function effect() -> bit { saved = o; return 0; }
function address() -> u32 { saved = 0; return 0; }
always_comb {
saved = 0;
result = 0;
result[address()] = 4'b0 == '{0, 0, default: effect()};
}
assign o = saved;
}
"#;
assert_complete_comb_loop(code, false);
}
#[test]
fn comb_loop_assignment_reads_precede_all_region_writes() {
for width in [3usize, 8] {
for source_bit in 0..width {
for result_bit in 0..width {
for (operator, expected_bit) in [
("<<", Some(source_bit + 1)),
(">>", source_bit.checked_sub(1)),
] {
for compound in [false, true] {
let assignment = if compound {
format!("value {operator}= 1;")
} else {
format!("value = value {operator} 1;")
};
let code = format!(
r#"
module Top(o: output bit) {{
var value: bit<{width}>;
always_comb {{
value = (o as {width}) << {source_bit};
{assignment}
}}
assign o = value[{result_bit}];
}}
"#
);
assert_complete_comb_loop(&code, expected_bit == Some(result_bit));
}
}
}
}
}
}
#[test]
fn comb_loop_concatenated_assignment_preserves_rhs_before_any_write() {
for source_bit in 0..4 {
for result_bit in 0..4 {
let code = format!(
r#"
module Top(o: output bit) {{
var value: bit<4>;
always_comb {{
value = (o as 4) << {source_bit};
{{value[1:0], value[3:2]}} = value;
}}
assign o = value[{result_bit}];
}}
"#
);
assert_complete_comb_loop(&code, result_bit == (source_bit + 2) % 4);
}
}
}
#[test]
fn comb_loop_assignment_samples_rhs_in_expression_order() {
for (expression, result_bit, expected) in [
("{saved, clear()}", 1, true),
("{clear(), saved}", 0, false),
("(saved as 2) | clear()", 0, true),
("clear() | (saved as 2)", 0, false),
] {
let code = format!(
r#"
module Top(o: output bit) {{
var saved: bit;
var result: bit<2>;
function clear() -> bit {{ saved = 0; return 0; }}
always_comb {{
saved = o;
result = {expression};
}}
assign o = result[{result_bit}];
}}
"#
);
assert_complete_comb_loop(&code, expected);
}
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_2_block_ring_assign_b_c_a_assign_c_b_1() {
let code = r#"
module ModuleA (
a: input logic<8>,
b: output logic<8>,
c: output logic<8>,
) {
assign b = c + a;
assign c = b + 1;
}
"#;
let errors = analyze(code);
assert!(matches!(errors[0], AnalyzerError::CombinationalLoop { .. }));
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_ff_broken_feedback_assign_x_y_always_ff_y_x_no_loop()
{
let code = r#"
module ModuleA (
clk: input clock,
a: input logic<8>,
b: output logic<8>,
) {
var y: logic<8>;
assign b = y;
always_ff (clk) {
y = a;
}
}
"#;
let errors = analyze(code);
assert!(errors.is_empty());
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_disjoint_partial_write_self_reference_a_1_a_0_is_not_a_loop()
{
let code = r#"
module ModuleA {
var a: logic<2>;
always_comb {
a[0] = 0;
}
always_comb {
a[1] = a[0];
}
}
"#;
let errors = analyze(code);
assert!(errors.is_empty());
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_block_local_variables_declared_inside_always_comb_do_not_create()
{
let code = r#"
module ModuleA (
a: input logic<32>,
b: output logic<32>,
) {
always_comb {
var c: logic<32>;
var d: logic<32>;
c = a;
d = 2 * c;
c = d;
b = c;
}
}
"#;
let errors = analyze(code);
assert!(errors.is_empty());
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_continuous_assign_self_reference_real_combinational_loop()
{
let code = r#"
module ModuleA (
a: output logic<8>,
) {
assign a = a + 1;
}
"#;
let errors = analyze(code);
assert!(
errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_conditional_self_reference_one_branch_reads_x_before_assigning()
{
let code = r#"
module ModuleA (
cond: input logic,
a: input logic<8>,
b: input logic<8>,
x: output logic<8>,
) {
always_comb {
if cond {
x = a;
} else {
x = b + x;
}
}
}
"#;
let errors = analyze(code);
assert!(
errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_procedural_overwrite_within_always_comb_is_not_a_loop_the_first()
{
let code = r#"
module ModuleA (
a: output logic<8>,
) {
always_comb {
a = 0;
a = a + 1;
}
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_both_branches_assign_x_with_no_self_read_not_a_loop()
{
let code = r#"
module ModuleA (
cond: input logic,
a: input logic<8>,
b: input logic<8>,
x: output logic<8>,
) {
always_comb {
if cond {
x = a;
} else {
x = b;
}
}
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_pre_assign_before_conditional_self_reference_is_dominating_and()
{
let code = r#"
module ModuleA (
cond: input logic,
b: input logic<8>,
x: output logic<8>,
) {
always_comb {
x = 0;
if cond {
x = b;
} else {
x = b + x;
}
}
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_bit_precise_nodekey_distinguishes_ca_0_ca_1_ca_2_so_the()
{
let code = r#"
module FullAdder (
a : input logic,
b : input logic,
c : input logic,
sum : output logic,
carry: output logic,
) {
assign sum = a ^ b ^ c;
assign carry = (a & b) | (c & (a ^ b));
}
module Arith (
a : input logic<2>,
b : input logic<2>,
sub: input logic ,
r : output logic<2>,
c : output logic ,
) {
var ca: logic<3>;
assign ca[0] = sub;
assign c = ca[2];
var cl_0: logic;
var cl_1: logic;
assign cl_0 = ca[0];
assign cl_1 = ca[1];
inst u0: FullAdder (
a : a[0] ,
b : b[0] ^ sub,
c : cl_0 ,
sum : r[0] ,
carry: ca[1] ,
);
inst u1: FullAdder (
a : a[1] ,
b : b[1] ^ sub,
c : cl_1 ,
sum : r[1] ,
carry: ca[2] ,
);
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_forward_array_chain_c_i_c_i_1_is_not_a_loop_requires()
{
let code = r#"
module Buf (
i: input logic<8>,
o: output logic<8>,
) {
assign o = i;
}
module Top (
d_in: input logic<8>,
d_out: output logic<8>,
) {
var c: logic<8> [3];
assign c[0] = d_in;
for i in 0..2 :cell {
inst u: Buf (
i: c[i],
o: c[i + 1],
);
}
assign d_out = c[2];
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_const_for_over_many_disjoint_array_elements_is_feed_forward() {
assert_comb_loop(
"a constant for loop over many disjoint array elements remains feed-forward",
r#"
module Top #(
param N: u32 = 1024,
) (
mem : input logic<32> [N],
i_d : input logic<32>,
sum : output logic<32>,
) {
var acc: logic<32> [N];
always_comb {
sum = 0;
for k in 0..N {
acc[k] = mem[k] ^ i_d;
sum = sum + acc[k];
}
}
}
"#,
false,
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_bit_disjoint_feedback_through_x_x_a_1_a_0_x_bit()
{
let code = r#"
module Top (
b_in: input logic,
a: output logic<2>,
) {
var x: logic;
var y: logic;
assign a[0] = x;
assign a[1] = y;
assign x = a[1];
assign y = b_in;
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_dst_side_bit_disjoint_writes_t1_0_0_t1_1_t3_reads_of()
{
let code = r#"
module ModuleAOk2 (
a: input logic,
b: input logic,
x: output logic,
) {
always_comb {
x = a & ~b;
}
}
module ModuleBOk2 (
a: input logic,
y: output logic,
) {
always_comb {
y = a;
}
}
module ModuleCOk2 (
a: input logic,
y: output logic,
) {
var t1: logic<2>;
var t2: logic;
var t3: logic;
inst mb: ModuleBOk2 (
a: t1[0],
y: t2,
);
inst ma: ModuleAOk2 (
a: a,
b: t2,
x: t3,
);
always_comb {
t1[0] = 0;
t1[1] = t3;
y = t3;
}
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
"false positive: {errors:?}"
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_src_side_bit_disjoint_reads_b_a_0_c_a_1_in_one_decl()
{
let code = r#"
module ModuleA (
d: input logic<2>,
out: output logic,
) {
var a: logic<2>;
var b: logic;
var c: logic;
always_comb {
b = a[0];
c = a[1];
}
always_comb {
a[0] = d[0];
a[1] = b;
}
assign out = c;
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
"source-side false positive: {errors:?}"
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_false_positive_cycle_through_unrelated_assigns_in_the_same_comb_block()
{
let code = r#"
module FPComp (
op1: input logic<32>,
op2: input logic<32>,
less_than: output logic,
) {
struct FP32 {
sign: logic ,
exp : logic<8> ,
frac: logic<23>,
}
var op1_fp32: FP32;
var op2_fp32: FP32;
always_comb {
op1_fp32 = op1;
op2_fp32 = op2;
if (op1_fp32.exp == op2_fp32.exp) {
less_than = op1_fp32.frac <: op2_fp32.frac;
} else {
less_than = op1_fp32.exp <: op2_fp32.exp;
}
}
}
"#;
let errors = analyze(code);
assert!(errors.is_empty());
}
#[test]
fn comb_loop_statement_order_and_observer_semantics_an_observer_inside_a_writer_process_does_not_create_a_signal_value_edge()
{
let code = r#"
module Top (
a: input logic,
o: output logic,
) {
var x: logic<2>;
var y: logic;
always_comb {
x[0] = a;
$display("x1=%d", x[1]);
o = x[1];
}
assign y = x[0];
assign x[1] = y;
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"observer-only read formed a false signal cycle: {errors:?}"
);
}
#[test]
fn comb_loop_statement_order_and_observer_semantics_a_dominating_procedural_write_supplies_the_later_read_memoryssa_must()
{
let code = r#"
module Top (
a: input logic,
o: output logic,
) {
var x: logic<2>;
always_comb {
x[0] = a;
x[1] = x[0];
o = x[1];
}
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"dominating procedural definition was treated as an entry read: {errors:?}"
);
}
#[test]
fn comb_loop_ordered_module_scope_reassignments_are_feed_forward() {
assert_comb_loop(
"module-scope definitions propagate in statement order",
r#"
module Top (
a: output logic,
) {
var b: logic;
always_comb {
a = 0;
b = a;
a = b;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_observer_semantics_the_converse_is_real_after_x_0_consumes_the_entry_value_of_x_1_the()
{
let code = r#"
module Top (
o: output logic<2>,
) {
always_comb {
o[0] = o[1];
o[1] = o[0];
}
}
"#;
let errors = analyze(code);
assert!(
errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"read-before-write feedback was not diagnosed: {errors:?}"
);
}
#[test]
fn comb_loop_dynamic_select_is_confined_to_static_prefix() {
assert_comb_loop(
"a dynamic suffix aliases only its longest static prefix",
r#"
module Top (
idx: input logic<2>,
src: input logic,
o : output logic,
) {
var buff: logic<4, 4>;
always_comb {
buff[1] = 0;
buff[1][0] = src;
}
always_comb {
buff[0][idx] = buff[1][0];
o = buff[0][0];
}
}
"#,
false,
);
}
#[test]
fn comb_loop_dynamic_write_is_a_weak_update_a_dynamic_store_cannot_kill_every_candidate_definition()
{
assert_comb_loop(
"a dynamic store cannot kill every candidate definition",
r#"
module Top (
idx: input logic<2>,
o : output logic<4>,
) {
always_comb {
o[idx] = 0;
o[0] = o[1];
o[1] = o[0];
}
}
"#,
true,
);
}
#[test]
fn comb_loop_dynamic_write_is_a_weak_update_a_full_store_still_kills_every_candidate_definition() {
assert_comb_loop(
"a full store still kills every candidate definition",
r#"
module Top (
o: output logic<4>,
) {
always_comb {
o = 0;
o[0] = o[1];
o[1] = o[0];
}
}
"#,
false,
);
}
fn logical_rhs_side_effect_code(expression: &str) -> String {
format!(
r#"
module Sink (
i: input logic,
) {{}}
module Top (
o: output logic,
) {{
var x: logic;
function touch (
a: input logic,
) -> logic {{
x = a;
return 0;
}}
inst u: Sink (
i: {expression},
);
assign o = x;
}}
"#
)
}
#[test]
fn comb_loop_false_logical_and_suppresses_rhs_side_effect() {
assert_comb_loop(
"a false logical-and LHS suppresses RHS function side effects",
&logical_rhs_side_effect_code("1'b0 && touch(o)"),
false,
);
}
#[test]
fn comb_loop_true_logical_and_retains_rhs_side_effect() {
assert_comb_loop(
"a true logical-and LHS retains RHS function side effects",
&logical_rhs_side_effect_code("1'b1 && touch(o)"),
true,
);
}
#[test]
fn comb_loop_true_logical_or_suppresses_rhs_side_effect() {
assert_comb_loop(
"a true logical-or LHS suppresses RHS function side effects",
&logical_rhs_side_effect_code("1'b1 || touch(o)"),
false,
);
}
#[test]
fn comb_loop_false_logical_or_retains_rhs_side_effect() {
assert_comb_loop(
"a false logical-or LHS retains RHS function side effects",
&logical_rhs_side_effect_code("1'b0 || touch(o)"),
true,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_read_before_write_observes_liveonentry() {
assert_comb_loop(
"read before write observes LiveOnEntry",
r#"
module Top (
a: input logic,
c: output logic,
) {
var b: logic;
always_comb {
c = b;
b = a;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_opposite_directions_on_disjoint_bits() {
assert_comb_loop(
"opposite directions on disjoint bits",
r#"
module Top (
i: input logic<2>,
o: output logic<2>,
) {
var a: logic<2>;
var b: logic<2>;
always_comb {
a[0] = i[0];
b[0] = a[0];
b[1] = i[1];
a[1] = b[1];
}
assign o = {a[1], b[0]};
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_observer_in_a_writer_with_a_multi_stage_assign_chain()
{
assert_comb_loop(
"observer in a writer with a multi-stage assign chain",
r#"
module Top (
a: input logic,
o: output logic,
) {
var x: logic<4>;
always_comb {
x[0] = a;
$display("x3=%d", x[3]);
o = x[3];
}
assign x[1] = x[0];
assign x[2] = x[1];
assign x[3] = x[2];
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_case_phi_with_complete_definitions() {
assert_comb_loop(
"case phi with complete definitions",
r#"
module Top (
sel: input logic<2>,
a : input logic,
b : input logic,
o : output logic,
) {
var selected: logic;
always_comb {
case sel {
2'd0: selected = a;
2'd1: selected = b;
default: selected = 0;
}
o = selected;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_one_branch_keeps_an_entry_definition_live()
{
assert_comb_loop(
"one branch keeps an entry definition live",
r#"
module Top (
sel: input logic,
o : output logic,
) {
always_comb {
if sel {
o = 0;
}
o = o;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_case_without_a_covering_default_keeps_entry_live()
{
assert_comb_loop(
"case without a covering default keeps entry live",
r#"
module Top (
sel: input logic<2>,
o : output logic,
) {
always_comb {
case sel {
2'd0: o = 0;
2'd1: o = 1;
}
o = o;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_dynamic_same_index_read_write_is_structurally_self_dependent()
{
assert_comb_loop(
"dynamic same-index read/write is structurally self-dependent",
r#"
module Top (
index: input logic<2>,
o : output logic,
) {
var values: logic [4];
always_comb {
values[index] = values[index];
o = values[0];
}
}
"#,
true,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_dynamic_data_write_without_an_old_value_read_is_feed_forward()
{
assert_comb_loop(
"dynamic data write without an old-value read is feed-forward",
r#"
module Top (
index: input logic<2>,
data : input logic,
o : output logic,
) {
var values: logic [4];
always_comb {
values[index] = data;
o = values[0];
}
}
"#,
false,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_a_value_controlling_its_own_dynamic_write_address_closes_a_loop()
{
assert_comb_loop(
"a value controlling its own dynamic write address closes a loop",
r#"
module Top (
data: input logic,
o : output logic,
) {
var index : logic<2>;
var values: logic [4];
always_comb {
index = {1'b0, values[0]};
values[index] = data;
o = values[0];
}
}
"#,
true,
);
}
#[test]
fn comb_loop_mutually_exclusive_reverse_dependencies_are_not_simultaneous_feedback() {
assert_comb_loop(
"opposing dependencies on mutually exclusive branches do not form a realizable loop",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
if sel {
a = b;
} else {
b = a;
}
o = a | b;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_break_path_does_not_reach_the_opposing_dependency_after_the_loop_exit() {
assert_comb_loop(
"a dependency before break is exclusive with the reverse dependency after it",
r#"
module Top (
stop: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
for _index in 0..1 {
if stop {
a = b;
break;
}
b = a;
}
o = a | b;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_break_exit_preserves_feedback_killed_only_on_the_continuing_path() {
assert_comb_loop(
"a write after a conditional break must not kill the break exit's dependency",
r#"
module Top (
stop: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
for _index in 0..1 {
if stop {
a = b;
break;
}
a = 0;
}
b = a;
o = b;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_false_negative_break_condition_controls_a_following_write() {
assert_comb_loop(
"a break condition controls whether the following assignment executes",
r#"
module Top (
o: output logic,
) {
var stop: logic;
var value: logic;
assign stop = value;
always_comb {
value = 0;
for _index in 0..1 {
if stop {
break;
}
value = 1;
}
o = value;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_break_condition_does_not_control_write_after_loop_exit() {
assert_comb_loop(
"all break and fallthrough paths execute a write after the loop",
r#"
module Top (
o: output logic,
) {
var stop : logic;
var value: logic;
assign stop = value;
always_comb {
value = 0;
for _index in 0..1 {
if stop {
break;
}
}
value = 1;
o = value;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_branches_in_separate_processes_are_not_mutually_exclusive() {
assert_comb_loop(
"branch identities are local to their procedural process",
r#"
module Top (
left : input logic,
right: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
if left {
a = b;
} else {
a = 0;
}
}
always_comb {
if right {
b = 0;
} else {
b = a;
}
}
always_comb {
o = a | b;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_feedback_within_one_branch_is_detected() {
assert_comb_loop(
"a realizable loop within one branch remains a loop",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
if sel {
a = b;
b = a;
} else {
a = 0;
b = 0;
}
o = a | b;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_mutually_exclusive_case_arms_do_not_form_feedback() {
assert_comb_loop(
"opposing dependencies in distinct case arms cannot execute together",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var a: logic;
var b: logic;
always_comb {
case sel {
1'b0: a = b;
default: b = a;
}
o = a | b;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_opposing_shifts_in_mutually_exclusive_arms_are_acyclic() {
assert_comb_loop(
"opposing positional dependencies cannot combine across exclusive arms",
r#"
module Top (
sel: input logic,
o : output logic<8>,
) {
var value: logic<8>;
always_comb {
if sel {
value = value << 1;
} else {
value = value >> 1;
}
o = value;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_read_before_write_across_disjoint_nibbles_is_acyclic_without_a_return_pa()
{
assert_comb_loop(
"read-before-write across disjoint nibbles is acyclic without a return path",
r#"
module Top (
o: output logic<8>,
) {
always_comb {
o[3:0] = o[7:4];
o[7:4] = 0;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_complete_overwrite_after_a_rotate_shaped_dead_store_kills_the_loop()
{
assert_comb_loop(
"complete overwrite after a rotate-shaped dead store kills the loop",
r#"
module Top (
o: output logic<8>,
) {
always_comb {
o = {o[3:0], o[7:4]};
o = 0;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_structural_dependency_semantics_observer_only_duplicate_reads_do_not_manufacture_value_feedback()
{
assert_comb_loop(
"observer-only duplicate reads do not manufacture value feedback",
r#"
module Top (
a: input logic,
o: output logic,
) {
always_comb {
$display("o=%d %d", o, o);
o = a;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_nested_break_exits_only_inner_loop_before_dominating_overwrite() {
assert_comb_loop(
"an inner break does not skip the outer loop's dominating overwrite",
r#"
module Top (
o: output logic,
) {
var a: logic;
var b: logic;
always_comb {
for _outer in 0..1 {
a = b;
for _inner in 0..1 {
break;
}
a = 0;
}
}
assign b = a;
assign o = b;
}
"#,
false,
);
}
#[test]
fn comb_loop_nested_break_preserves_dependency_before_inner_exit() {
assert_comb_loop(
"an inner break retains the reachable dependency before it",
r#"
module Top (
o: output logic,
) {
var a: logic;
var b: logic;
always_comb {
for _outer in 0..1 {
for _inner in 0..1 {
a = b;
break;
}
}
}
assign b = a;
assign o = b;
}
"#,
true,
);
}
#[test]
fn comb_loop_explicit_self_read_remains_feedback() {
let errors = analyze(
r#"
module Top (
o: output logic,
) {
var value: logic;
always_comb {
value = value;
o = value;
}
}
"#,
);
assert!(
errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"an explicit same-evaluation self-read remains combinational feedback: {errors:#?}"
);
}
#[test]
fn comb_loop_if_assignment_to_array_is_feed_forward_if_statement_chain_is_acyclic() {
let if_stmt = r#"
module Chain (
x0: input signed logic<16>,
y0: input signed logic<16>,
xo: output signed logic<16>,
yo: output signed logic<16>,
) {
var xw: signed logic<16> [5];
var yw: signed logic<16> [5];
always_comb {
xw[0] = x0;
yw[0] = y0;
for i in 0..4 {
if yw[i] <: 0 {
xw[i + 1] = xw[i] - (yw[i] >>> i);
yw[i + 1] = yw[i] + (xw[i] >>> i);
} else {
xw[i + 1] = xw[i] + (yw[i] >>> i);
yw[i + 1] = yw[i] - (xw[i] >>> i);
}
}
xo = xw[4];
yo = yw[4];
}
}
"#;
let errors = analyze(if_stmt);
assert!(
errors.is_empty(),
"feed-forward array chain (if-statement form) must not be a comb loop: {errors:?}"
);
}
#[test]
fn comb_loop_if_assignment_to_array_is_feed_forward_condition_driven_loop_is_reported() {
let real_cond_loop = r#"
module RealLoop (
o: output logic,
) {
var a: logic;
var b: logic;
always_comb {
if a {
b = 1;
} else {
b = 0;
}
if b {
a = 1;
} else {
a = 0;
}
o = a;
}
}
"#;
let errors = analyze(real_cond_loop);
assert!(
errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
"a real condition-driven loop must still be detected: {errors:?}"
);
}
#[test]
fn comb_loop_if_assignment_to_array_is_feed_forward_array_loop_is_reported() {
let real_array_loop = r#"
module RealArray (
o: output logic<8>,
) {
var m: logic<8> [2];
always_comb {
m[0] = m[1];
m[1] = m[0];
o = m[0];
}
}
"#;
let errors = analyze(real_array_loop);
assert!(
errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. })),
"a real array comb loop must still be detected: {errors:?}"
);
}
#[test]
fn comb_loop_if_expression_arms_are_mutually_exclusive() {
assert_comb_loop(
"opposing dependencies in one if expression cannot execute together",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var state: logic<2>;
always_comb {
state = if sel ? {state[0], 1'b0} : {1'b0, state[1]};
o = |state;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_case_expression_arms_are_mutually_exclusive() {
assert_comb_loop(
"opposing dependencies in one case expression cannot execute together",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var state: logic<2>;
always_comb {
state = case sel {
1'b0 : {state[0], 1'b0},
default: {1'b0, state[1]},
};
o = |state;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_switch_expression_arms_are_mutually_exclusive() {
assert_comb_loop(
"opposing dependencies in one switch expression cannot execute together",
r#"
module Top (
left : input logic,
right: input logic,
o : output logic,
) {
var state: logic<2>;
always_comb {
state = switch {
left : {state[0], 1'b0},
right : {1'b0, state[1]},
default: 2'b00,
};
o = |state;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_if_expression_function_side_effects_remain_arm_exclusive() {
assert_comb_loop(
"captured writes in opposite expression arms cannot execute together",
r#"
module Top (
sel: input logic,
o : output logic,
) {
var a : logic;
var b : logic;
var dummy: logic;
function write_a (value: input logic) -> logic {
a = value;
return 1'b0;
}
function write_b (value: input logic) -> logic {
b = value;
return 1'b0;
}
always_comb {
dummy = if sel ? write_a(b) : write_b(a);
o = a | b | dummy;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_false_negative_runtime_short_circuit_write_kills_disabled_feedback() {
assert_comb_loop(
"a conditionally skipped function write cannot kill a realizable loop",
r#"
module Top (
enable: input logic,
o : output logic,
) {
var a : logic;
var b : logic;
var dummy: logic;
function clear_a () -> logic {
a = 1'b0;
return 1'b0;
}
always_comb {
a = b;
dummy = enable && clear_a();
b = a;
o = a | b | dummy;
}
}
"#,
true,
);
}
#[test]
fn zz_q1_struct_literal_whole_write_two_directions() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module Producer (
src_valid_i: input logic,
back_i : input logic,
h_o : output Pk::H,
) {
assign h_o = Pk::H'{
a_valid: src_valid_i,
d_ready: back_i ,
};
}
module Dev (
h_i : input Pk::H,
ready_o: output logic,
) {
assign ready_o = h_i.a_valid;
}
module ModuleA (
v: input logic,
z: output logic,
) {
var h : Pk::H;
var rdy: logic;
inst p: Producer (src_valid_i: v , back_i: rdy, h_o : h );
inst d: Dev (h_i : h , ready_o : rdy );
assign z = rdy;
}
"#;
assert_comb_loop("Q1 whole-struct literal, two directions", code, false);
}
#[test]
fn zz_q2_same_but_field_wise_writes() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module Producer (
src_valid_i: input logic,
back_i : input logic,
h_o : output Pk::H,
) {
assign h_o.a_valid = src_valid_i;
assign h_o.d_ready = back_i;
}
module Dev (
h_i : input Pk::H,
ready_o: output logic,
) {
assign ready_o = h_i.a_valid;
}
module ModuleA (
v: input logic,
z: output logic,
) {
var h : Pk::H;
var rdy: logic;
inst p: Producer (src_valid_i: v , back_i: rdy, h_o : h );
inst d: Dev (h_i : h , ready_o : rdy );
assign z = rdy;
}
"#;
assert_comb_loop("Q2 field-wise writes, two directions", code, false);
}
#[test]
fn zz_q3_flatten_cast_of_the_bidirectional_struct() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module Dev (
v_i : input logic<2>,
ready_o: output logic ,
) {
assign ready_o = v_i[1]; // reads only the a_valid bit
}
module ModuleA (
v: input logic,
z: output logic,
) {
const W: u32 = 2;
var h : Pk::H ;
var hv : logic<2>;
var rdy: logic ;
assign h.a_valid = v;
assign h.d_ready = rdy;
assign hv = h as W;
inst d: Dev (v_i: hv, ready_o: rdy);
assign z = rdy;
}
"#;
assert_comb_loop("Q3 flatten cast of a bidirectional struct", code, false);
}
#[test]
fn zz_q4_arbiter_mux_shape() {
let code = r#"
module Arb #(
param N: u32 = 2,
) (
req_i : input logic<N> ,
data_i : input logic<2>[N],
gnt_o : output logic<N> ,
data_o : output logic<2> ,
ready_i: input logic ,
) {
var winner: logic<N>;
assign winner = req_i;
assign gnt_o = if ready_i ? winner : '0;
always_comb {
data_o = '0;
for i in 0..N {
if winner[i] {
data_o = data_i[i];
}
}
}
}
module ModuleA (
v0: input logic,
v1: input logic,
rd: input logic,
z : output logic,
) {
var req : logic<2> ;
var dat : logic<2>[2];
var gnt : logic<2> ;
var dout : logic<2> ;
inst a: Arb (req_i: req, data_i: dat, gnt_o: gnt, data_o: dout, ready_i: rd);
assign req = {v1, v0};
assign dat[0] = {v0, gnt[0]}; // the h2d bundle: a_valid + d_ready(=grant)
assign dat[1] = {v1, gnt[1]};
assign z = dout[0];
}
"#;
assert_comb_loop(
"Q4 arbiter mux with grant folded into the data bundle",
code,
false,
);
}
#[test]
fn zz_q5_flatten_through_instance_then_part_select_back() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module Fifo (
wdata_i: input logic<2>,
rdata_o: output logic<2>,
) {
assign rdata_o = wdata_i;
}
module ModuleA (
v: input logic,
z: output logic,
) {
const W: u32 = 2;
var h : Pk::H ;
var hv : logic<2>;
var dv : logic<2>;
var rdy: logic ;
assign h.a_valid = v;
assign h.d_ready = rdy;
assign hv = h as W;
inst f: Fifo (wdata_i: hv, rdata_o: dv);
assign rdy = dv[1+:1]; // recover a_valid by part-select
assign z = dv[0+:1];
}
"#;
assert_comb_loop("Q5 flatten -> instance -> part-select", code, false);
}
#[test]
fn zz_q6_no_cast_whole_variable_copy() {
let code = r#"
module Fifo (
wdata_i: input logic<2>,
rdata_o: output logic<2>,
) {
assign rdata_o = wdata_i;
}
module ModuleA (
v: input logic,
z: output logic,
) {
var h : logic<2>;
var hv : logic<2>;
var dv : logic<2>;
var rdy: logic ;
assign h[0] = rdy;
assign h[1] = v;
assign hv = h; // whole-variable destination, no select
inst f: Fifo (wdata_i: hv, rdata_o: dv);
assign rdy = dv[1+:1];
assign z = dv[0+:1];
}
"#;
assert_comb_loop("Q6 plain whole-variable copy", code, false);
}
#[test]
fn zz_q7_explicit_const_range_on_the_destination() {
let code = r#"
module Fifo (
wdata_i: input logic<2>,
rdata_o: output logic<2>,
) {
assign rdata_o = wdata_i;
}
module ModuleA (
v: input logic,
z: output logic,
) {
var h : logic<2>;
var hv : logic<2>;
var dv : logic<2>;
var rdy: logic ;
assign h[0] = rdy;
assign h[1] = v;
assign hv[1:0] = h[1:0]; // explicit const range on both sides
inst f: Fifo (wdata_i: hv, rdata_o: dv);
assign rdy = dv[1+:1];
assign z = dv[0+:1];
}
"#;
assert_comb_loop("Q7 explicit const range (mechanism control)", code, false);
}
#[test]
fn zz_q8_struct_cast_part_select_no_instance() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module ModuleA (
v: input logic,
z: output logic,
) {
const W: u32 = 2;
var h : Pk::H ;
var hv : logic<2>;
var rdy: logic ;
assign h.a_valid = v;
assign h.d_ready = rdy;
assign hv = h as W;
assign rdy = hv[1+:1];
assign z = hv[0+:1];
}
"#;
assert_comb_loop("Q8 struct cast + part-select, no instance", code, false);
}
#[test]
fn zz_q9_struct_to_struct_through_instance() {
let code = r#"
package Pk {
struct H {
a_valid: logic,
d_ready: logic,
}
}
module Fifo (
w_i: input Pk::H,
r_o: output Pk::H,
) {
assign r_o = w_i;
}
module ModuleA (
v: input logic,
z: output logic,
) {
var h : Pk::H;
var d : Pk::H;
var rdy: logic;
assign h.a_valid = v;
assign h.d_ready = rdy;
inst f: Fifo (w_i: h, r_o: d);
assign rdy = d.a_valid;
assign z = d.d_ready;
}
"#;
assert_comb_loop("Q9 struct through instance, no cast", code, false);
}