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:?}");
}
macro_rules! comb_loop_case {
($name:ident, $case:literal, $code:expr, $expected:expr) => {
#[test]
fn $name() {
let code = $code;
assert_comb_loop($case, code.as_ref(), $expected);
}
};
}
fn whole_unpacked_instance_code(output_index: usize) -> String {
format!(
r#"
module Identity (
i: input logic [200000], o: output logic [200000],
) {{ assign o = i; }}
module Top (o: output logic) {{
var feedback: logic [200000];
var passed: logic [200000];
inst u: Identity (i: feedback, o: passed);
assign feedback[123456] = passed[{output_index}];
assign o = passed[0];
}}
"#
)
}
fn assert_unaligned_unpacked_instance_input(target: usize, expected: bool) {
assert_comb_loop(
"an unaligned unpacked instance input preserves element identity",
&format!(
r#"
module Child (i: input logic [2], o: output logic) {{ assign o = !i[1]; }}
module Top (o: output logic) {{
var value: logic [2];
var passed: logic;
inst u: Child (i: value, o: passed);
assign value[{target}] = passed;
assign value[{}] = 0;
assign o = passed;
}}
"#,
1 - target
),
expected,
);
}
comb_loop_case!(
comb_loop_whole_unpacked_matching_element_retains_feedback,
"a distant matching element retains feedback",
whole_unpacked_instance_code(123_456),
true
);
comb_loop_case!(
comb_loop_whole_unpacked_disjoint_elements_remain_independent,
"distant disjoint elements remain independent",
whole_unpacked_instance_code(65_432),
false
);
fn periodic_repeat_code(through_instance: bool, count: usize, index: usize, bit: usize) -> String {
let body = if through_instance {
"inst u: Broadcast (i: feedback, o: passed);".to_string()
} else {
format!("assign passed = '{{feedback repeat {count}}};")
};
format!(
r#"
module Broadcast (i: input logic<2>, o: output logic<2> [{count}]) {{
assign o = '{{i repeat {count}}};
}}
module Top (o: output logic) {{
var feedback: logic<2>;
var passed: logic<2> [{count}];
{body}
assign feedback[0] = passed[{index}][{bit}];
assign feedback[1] = 0;
assign o = passed[0][0];
}}
"#
)
}
fn periodic_repeat_two_level_code(bit: usize) -> String {
format!(
r#"
module Broadcast (i: input logic<2>, o: output logic<2> [64]) {{
assign o = '{{i repeat 64}};
}}
module Wrapper (
i: input logic<2>, o: output logic<2> [64], tap: output logic,
) {{
inst u: Broadcast (i: i, o: o);
assign tap = i[0];
}}
module Top (o: output logic) {{
var feedback: logic<2>;
var passed: logic<2> [64];
var tap: logic;
inst u: Wrapper (i: feedback, o: passed, tap: tap);
assign feedback[0] = passed[42][{bit}];
assign feedback[1] = 0;
assign o = tap;
}}
"#
)
}
comb_loop_case!(
comb_loop_instance_repeat_retains_matching_phase_at_scale,
"instance repeat retains matching phase feedback",
periodic_repeat_code(true, 200_000, 123_456, 0),
true
);
comb_loop_case!(
comb_loop_instance_repeat_keeps_different_phase_disjoint,
"instance repeat keeps a different phase disjoint",
periodic_repeat_code(true, 64, 42, 1),
false
);
comb_loop_case!(
comb_loop_periodic_phase_survives_two_module_summaries,
"a periodic phase survives two module summaries",
periodic_repeat_two_level_code(0),
true
);
comb_loop_case!(
comb_loop_periodic_disjoint_phase_survives_two_module_summaries,
"a disjoint periodic phase survives two module summaries",
periodic_repeat_two_level_code(1),
false
);
#[test]
fn comb_loop_core_semantics_and_region_regressions_module_instance_feedthrough_child_has_assign_out_in_parent()
{
let code = r#"
module Buf (
i: input logic<8>,
o: output logic<8>,
) {
assign o = i;
}
module Top (
a: input logic<8>,
b: output logic<8>,
) {
var x: logic<8>;
var y: logic<8>;
inst u: Buf (
i: x,
o: y,
);
assign x = y;
assign b = y;
}
"#;
let errors = analyze(code);
assert!(
errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_core_semantics_and_region_regressions_module_instance_with_ff_driven_output_no_loop_should_be_detected()
{
let code = r#"
module Reg (
clk: input clock,
i: input logic<8>,
o: output logic<8>,
) {
always_ff (clk) {
o = i;
}
}
module Top (
clk: input clock,
a: input logic<8>,
b: output logic<8>,
) {
var x: logic<8>;
var y: logic<8>;
inst u: Reg (
clk: clk,
i: x,
o: y,
);
assign x = y;
assign b = y;
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|e| matches!(e, AnalyzerError::CombinationalLoop { .. }))
);
}
#[test]
fn comb_loop_dynamic_region_survives_module_summary() {
assert_comb_loop(
"a dynamic child output preserves feedback across two summaries",
r#"
module Leaf (
i: input logic,
o: output logic,
) {
assign o = i;
}
module Middle (
i : input logic,
idx: input logic,
o : output logic<2>,
) {
inst u: Leaf (
i: i,
o: o[idx],
);
}
module Top (
idx: input logic,
o : output logic,
) {
var feedback: logic;
var bus : logic<2>;
inst u: Middle (
i : feedback,
idx: idx,
o : bus,
);
assign feedback = bus[0];
assign o = feedback;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_module_summary_positions_a_vector_identity_module_preserves_disjoint_bit_positions()
{
assert_comb_loop(
"a vector identity module preserves disjoint bit positions",
r#"
module Child (
i: input logic<2>,
o: output logic<2>,
) {
assign o = i;
}
module Top (
o: output logic<2>,
) {
var child_i: logic<2>;
var child_o: logic<2>;
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[1];
assign child_i[1] = 0;
assign o = child_o;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_module_summary_positions_a_vector_identity_module_retains_same_bit_feedback()
{
assert_comb_loop(
"a vector identity module retains same-bit feedback",
r#"
module Child (
i: input logic<2>,
o: output logic<2>,
) {
assign o = i;
}
module Top (
o: output logic<2>,
) {
var child_i: logic<2>;
var child_o: logic<2>;
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[0];
assign child_i[1] = 0;
assign o = child_o;
}
"#,
true,
);
}
#[test]
fn comb_loop_module_summary_preserves_multidimensional_array_elements() {
assert_comb_loop(
"a multidimensional identity keeps distinct elements independent",
r#"
module Child (
i: input logic<2> [2, 3],
o: output logic<2> [2, 3],
) {
assign o = i;
}
module Top (o: output logic) {
var child_i: logic<2> [2, 3];
var child_o: logic<2> [2, 3];
inst u: Child (i: child_i, o: child_o);
assign child_i[0][1][0] = child_o[1][1][0];
assign child_i[0][1][1] = 0;
assign o = child_o[0][0][0];
}
"#,
false,
);
}
#[test]
fn comb_loop_module_summary_detects_multidimensional_same_element_feedback() {
assert_comb_loop(
"a multidimensional identity retains the same element and bit",
r#"
module Child (
i: input logic<2> [2, 3],
o: output logic<2> [2, 3],
) {
assign o = i;
}
module Top (o: output logic) {
var child_i: logic<2> [2, 3];
var child_o: logic<2> [2, 3];
inst u: Child (i: child_i, o: child_o);
assign child_i[0][1][0] = child_o[0][1][0];
assign child_i[0][1][1] = 0;
assign o = child_o[0][0][0];
}
"#,
true,
);
}
#[test]
fn comb_loop_module_summary_preserves_packed_struct_members() {
assert_comb_loop(
"a packed struct identity keeps distinct members independent",
r#"
package Types {
struct Pair { low: logic, high: logic, }
}
module Child (
i: input Types::Pair,
o: output Types::Pair,
) {
assign o = i;
}
module Top (o: output logic) {
var child_i: Types::Pair;
var child_o: Types::Pair;
inst u: Child (i: child_i, o: child_o);
assign child_i.low = child_o.high;
assign child_i.high = 0;
assign o = child_o.low;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_summary_detects_packed_struct_member_feedback() {
assert_comb_loop(
"a packed struct identity retains the same member",
r#"
package Types {
struct Pair { low: logic, high: logic, }
}
module Child (
i: input Types::Pair,
o: output Types::Pair,
) {
assign o = i;
}
module Top (o: output logic) {
var child_i: Types::Pair;
var child_o: Types::Pair;
inst u: Child (i: child_i, o: child_o);
assign child_i.low = child_o.low;
assign child_i.high = 0;
assign o = child_o.low;
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_output_address_contributes_dependency() {
assert_comb_loop(
"a dynamic instance output retains address feedback",
r#"
module Child (
i: input logic,
o: output logic,
) {
assign o = i;
}
module Top (
seed: input logic,
o : output logic,
) {
var idx: logic;
var bus: logic [2];
inst u: Child (
i: seed,
o: bus[idx],
);
assign idx = bus[0];
assign o = idx;
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_input_selector_side_effect_is_recorded() {
assert_comb_loop(
"an instance input selector retains a called function global write",
r#"
module Sink (
i: input logic,
) {}
module Top (
mem: input logic<2>,
o : output logic,
) {
var x: logic;
function touch (
a: input logic,
) -> logic {
x = a;
return 0;
}
inst u: Sink (
i: mem[touch(o)],
);
assign o = x;
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_output_selector_side_effect_is_recorded() {
assert_comb_loop(
"an instance output selector retains a called function global write",
r#"
module Source (
o: output logic,
) {
assign o = 0;
}
module Top (
o: output logic,
) {
var bus: logic<2>;
var x : logic;
function touch (
a: input logic,
) -> logic {
x = a;
return 0;
}
inst u: Source (
o: bus[touch(o)],
);
assign o = x;
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_output_selector_without_a_return_path_is_feed_forward() {
assert_comb_loop(
"an independent instance output selector remains feed-forward",
r#"
module Source (
o: output logic,
) {
assign o = 0;
}
module Top (
index: input logic,
seed : input logic,
o : output logic,
) {
var bus: logic<2>;
var x : logic;
function touch (
a: input logic,
) -> logic {
x = a;
return index;
}
inst u: Source (
o: bus[touch(seed)],
);
assign o = x;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_instance_shift_positions_an_instance_actual_left_shift_keeps_its_inserted_bit_loop_free()
{
assert_comb_loop(
"an instance actual left shift keeps its inserted bit loop-free",
r#"
module Child (
i: input logic<4>,
o: output logic<4>,
) {
assign o = i;
}
module Top (
o: output logic<4>,
) {
var value: logic<4>;
var passed: logic<4>;
inst u: Child (
i: value << 1,
o: passed,
);
assign value[0] = 0;
assign value[1] = passed[0];
assign value[3:2] = 0;
assign o = passed;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_instance_shift_positions_an_instance_actual_left_shift_detects_its_live_shifted_bit()
{
assert_comb_loop(
"an instance actual left shift detects its live shifted bit",
r#"
module Child (
i: input logic<4>,
o: output logic<4>,
) {
assign o = i;
}
module Top (
o: output logic<4>,
) {
var value: logic<4>;
var passed: logic<4>;
inst u: Child (
i: value << 1,
o: passed,
);
assign value[0] = passed[1];
assign value[3:1] = 0;
assign o = passed;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_instance_repeat_positions_an_instance_repeat_actual_keeps_an_unrelated_operand_loop_free()
{
assert_comb_loop(
"an instance repeat actual keeps an unrelated operand loop-free",
r#"
module Child (
i: input logic<3>,
o: output logic,
) {
assign o = i[2];
}
module Top (
o: output logic,
) {
var high: logic;
var low: logic;
var child_o: logic;
inst u: Child (
i: {high repeat 2, low},
o: child_o,
);
assign high = 0;
assign low = child_o;
assign o = child_o;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_instance_repeat_positions_an_instance_repeat_actual_detects_its_selected_repeated_operand()
{
assert_comb_loop(
"an instance repeat actual detects its selected repeated operand",
r#"
module Child (
i: input logic<3>,
o: output logic,
) {
assign o = i[2];
}
module Top (
o: output logic,
) {
var high: logic;
var low: logic;
var child_o: logic;
inst u: Child (
i: {high repeat 2, low},
o: child_o,
);
assign high = child_o;
assign low = 0;
assign o = child_o;
}
"#,
true,
);
}
#[test]
fn comb_loop_distinguishes_generic_module_specializations_an_enabled_generic_module_specialization_retains_its_feedthrough()
{
assert_comb_loop(
"an enabled generic-module specialization retains its feedthrough",
r#"
module Child #(
param ENABLE: u32 = 0,
)(
i: input logic,
o: output logic,
) {
if ENABLE :g_enabled {
assign o = i;
} else {
assign o = 0;
}
}
module Top (
o: output logic,
) {
var feedback: logic;
var passed: logic;
inst u: Child #(
ENABLE: 1,
)(
i: feedback,
o: passed,
);
assign feedback = passed;
assign o = feedback;
}
"#,
true,
);
}
#[test]
fn comb_loop_distinguishes_generic_module_specializations_a_disabled_generic_module_specialization_does_not_inherit_feedthrough()
{
assert_comb_loop(
"a disabled generic-module specialization does not inherit feedthrough",
r#"
module Child #(
param ENABLE: u32 = 1,
)(
i: input logic,
o: output logic,
) {
if ENABLE :g_enabled {
assign o = i;
} else {
assign o = 0;
}
}
module Top (
o: output logic,
) {
var feedback: logic;
var passed: logic;
inst u: Child #(
ENABLE: 0,
)(
i: feedback,
o: passed,
);
assign feedback = passed;
assign o = feedback;
}
"#,
false,
);
}
fn instance_logical_actual_code(expression: &str) -> String {
format!(
r#"
module Pass (
i: input logic,
o: output logic,
) {{
assign o = i;
}}
module Top (
o: output logic,
) {{
var feedback: logic;
var passed: logic;
inst u: Pass (
i: {expression},
o: passed,
);
assign feedback = passed;
assign o = passed;
}}
"#
)
}
#[test]
fn comb_loop_false_logical_and_instance_actual_drops_rhs() {
assert_comb_loop(
"a false logical-and instance actual drops its dead RHS",
&instance_logical_actual_code("1'b0 && feedback"),
false,
);
}
#[test]
fn comb_loop_true_logical_or_instance_actual_drops_rhs() {
assert_comb_loop(
"a true logical-or instance actual drops its dead RHS",
&instance_logical_actual_code("1'b1 || feedback"),
false,
);
}
#[test]
fn comb_loop_true_logical_and_instance_actual_retains_rhs() {
assert_comb_loop(
"a true logical-and instance actual retains its live RHS",
&instance_logical_actual_code("1'b1 && feedback"),
true,
);
}
#[test]
fn comb_loop_false_logical_or_instance_actual_retains_rhs() {
assert_comb_loop(
"a false logical-or instance actual retains its live RHS",
&instance_logical_actual_code("1'b0 || feedback"),
true,
);
}
#[test]
fn comb_loop_preserves_ternary_positions_across_boundaries_an_instance_ternary_actual_keeps_a_disjoint_bit_loop_free()
{
assert_comb_loop(
"an instance ternary actual keeps a disjoint bit loop-free",
r#"
module Low (
i: input logic<2>,
o: output logic,
) {
assign o = i[0];
}
module Top (
sel: input logic,
o : output logic,
) {
var value: logic<2>;
var passed: logic;
inst u: Low (
i: if sel ? value : 0,
o: passed,
);
assign value[0] = 0;
assign value[1] = passed;
assign o = passed;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_ternary_positions_across_boundaries_an_instance_ternary_actual_detects_its_corresponding_bit_loop()
{
assert_comb_loop(
"an instance ternary actual detects its corresponding-bit loop",
r#"
module Low (
i: input logic<2>,
o: output logic,
) {
assign o = i[0];
}
module Top (
sel: input logic,
o : output logic,
) {
var value: logic<2>;
var passed: logic;
inst u: Low (
i: if sel ? value : 0,
o: passed,
);
assign value[0] = passed;
assign value[1] = 0;
assign o = passed;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_unpacked_port_element_positions_an_unpacked_array_module_port_keeps_disjoint_elements_loop_free()
{
assert_comb_loop(
"an unpacked-array module port keeps disjoint elements loop-free",
r#"
module Child (
i: input logic [2],
o: output logic [2],
) {
assign o[0] = i[0];
assign o[1] = i[1];
}
module Top (
o: output logic,
) {
var child_i: logic [2];
var child_o: logic [2];
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[1];
assign child_i[1] = 0;
assign o = child_o[0];
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_unpacked_port_element_positions_an_unpacked_array_module_port_detects_same_element_feedback()
{
assert_comb_loop(
"an unpacked-array module port detects same-element feedback",
r#"
module Child (
i: input logic [2],
o: output logic [2],
) {
assign o[0] = i[0];
assign o[1] = i[1];
}
module Top (
o: output logic,
) {
var child_i: logic [2];
var child_o: logic [2];
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[0];
assign child_i[1] = 0;
assign o = child_o[0];
}
"#,
true,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_child_port_summary_must_not_turn_bit_disjoint_feedthrough_into_a_loop()
{
assert_comb_loop(
"a child port summary must not turn bit-disjoint feedthrough into a loop",
r#"
module Child (
i: input logic<2>,
o: output logic<2>,
) {
assign o[0] = i[1];
assign o[1] = 0;
}
module Top (
o: output logic<2>,
) {
var child_i: logic<2>;
var child_o: logic<2>;
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[0];
assign child_i[1] = 0;
assign o = child_o;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_region_preserving_child_feedthrough_still_reports_a_real_loop()
{
assert_comb_loop(
"a region-preserving child feedthrough still reports a real loop",
r#"
module Child (
i: input logic<2>,
o: output logic<2>,
) {
assign o[0] = i[0];
assign o[1] = 0;
}
module Top (
o: output logic<2>,
) {
var child_i: logic<2>;
var child_o: logic<2>;
inst u: Child (
i: child_i,
o: child_o,
);
assign child_i[0] = child_o[0];
assign child_i[1] = 0;
assign o = child_o;
}
"#,
true,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_an_opaque_systemverilog_component_cannot_prove_a_hard_loop()
{
assert_comb_loop(
"an opaque SystemVerilog component cannot prove a hard loop",
r#"
module Top (
o: output logic,
) {
var into_sv : logic;
var from_sv : logic;
inst u: $sv::Ext (
i_data: into_sv,
o_data: from_sv,
);
assign into_sv = from_sv;
assign o = from_sv;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_top_input_to_output_path_has_no_inferred_environment_return_edge()
{
assert_comb_loop(
"a top input-to-output path has no inferred environment return edge",
r#"
module Top (
i: input logic,
o: output logic,
) {
assign o = i;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_bit_precision_survives_two_module_boundaries() {
assert_comb_loop(
"bit precision survives two module boundaries",
r#"
module Leaf (
i: input logic<2>,
o: output logic<2>,
) {
assign o[0] = i[1];
assign o[1] = 0;
}
module Middle (
i: input logic<2>,
o: output logic<2>,
) {
inst u_leaf: Leaf (
i: i,
o: o,
);
}
module Top (
o: output logic<2>,
) {
var middle_i: logic<2>;
var middle_o: logic<2>;
inst u_middle: Middle (
i: middle_i,
o: middle_o,
);
assign middle_i[0] = middle_o[0];
assign middle_i[1] = 0;
assign o = middle_o;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_real_loop_survives_two_region_preserving_summaries() {
assert_comb_loop(
"a real loop survives two region-preserving summaries",
r#"
module Leaf (
i: input logic<2>,
o: output logic<2>,
) {
assign o[0] = i[0];
assign o[1] = 0;
}
module Middle (
i: input logic<2>,
o: output logic<2>,
) {
inst u_leaf: Leaf (
i: i,
o: o,
);
}
module Top (
o: output logic<2>,
) {
var middle_i: logic<2>;
var middle_o: logic<2>;
inst u_middle: Middle (
i: middle_i,
o: middle_o,
);
assign middle_i[0] = middle_o[0];
assign middle_i[1] = 0;
assign o = middle_o;
}
"#,
true,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_concatenated_instance_input_preserves_its_low_bit_source()
{
assert_comb_loop(
"a concatenated instance input preserves its low-bit source",
r#"
module Child (
i: input logic<2>,
o: output logic,
) {
assign o = i[0];
}
module Top (
o: output logic,
) {
var high : logic;
var low : logic;
var child_o : logic;
inst u: Child (
i: {high, low},
o: child_o,
);
assign high = child_o;
assign low = 0;
assign o = child_o;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_concatenated_instance_input_does_not_hide_a_real_low_bit_loop()
{
assert_comb_loop(
"a concatenated instance input does not hide a real low-bit loop",
r#"
module Child (
i: input logic<2>,
o: output logic,
) {
assign o = i[0];
}
module Top (
o: output logic,
) {
var high : logic;
var low : logic;
var child_o : logic;
inst u: Child (
i: {high, low},
o: child_o,
);
assign high = 0;
assign low = child_o;
assign o = child_o;
}
"#,
true,
);
}
#[test]
fn comb_loop_module_boundary_region_mapping_a_static_slice_connection_does_not_contaminate_its_sibling_bit()
{
assert_comb_loop(
"a static slice connection does not contaminate its sibling bit",
r#"
module Child (
i: input logic,
o: output logic,
) {
assign o = i;
}
module Top (
o: output logic<2>,
) {
var bus: logic<2>;
inst u: Child (
i: bus[1],
o: bus[0],
);
assign bus[1] = 0;
assign o = bus;
}
"#,
false,
);
}
#[test]
fn comb_loop_instance_summary_region_mapping_is_reused() {
const WIDTH: usize = 32;
let reduction = (0..WIDTH)
.map(|index| format!("x[{index}]"))
.collect::<Vec<_>>()
.join(" ^ ");
let outputs = (0..WIDTH)
.map(|index| format!("assign o[{index}] = mix(i);"))
.collect::<Vec<_>>()
.join("\n");
let clears = (1..WIDTH)
.map(|index| format!("assign feedback[{index}] = 0;"))
.collect::<Vec<_>>()
.join("\n");
assert_comb_loop(
"an instance summary Cartesian product retains parent feedback",
&format!(
r#"
module Child (
i: input logic<{WIDTH}>,
o: output logic<{WIDTH}>,
) {{
function mix (
x: input logic<{WIDTH}>,
) -> logic {{
return {reduction};
}}
{outputs}
}}
module Top (
o: output logic<{WIDTH}>,
) {{
var feedback: logic<{WIDTH}>;
var passed : logic<{WIDTH}>;
inst u: Child (
i: feedback,
o: passed,
);
assign feedback[0] = passed[0];
{clears}
assign o = passed;
}}
"#
),
true,
);
}
#[test]
fn comb_loop_module_formal_high_bit_ignores_short_unsigned_actual() {
assert_comb_loop(
"a module formal high bit does not read an unsigned short actual",
r#"
module High (i: input logic<4>, o: output logic) { assign o = i[3]; }
module Top (o: output logic) {
var value: logic<2>;
inst u: High (i: value, o: o);
assign value[0] = o;
assign value[1] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_unaligned_unpacked_instance_element_zero_is_disjoint() {
assert_unaligned_unpacked_instance_input(0, false);
}
#[test]
fn comb_loop_unaligned_unpacked_instance_element_one_retains_feedback() {
assert_unaligned_unpacked_instance_input(1, true);
}
#[test]
fn many_comb_declarations_build_the_module_context_once() {
const COUNT: usize = 256;
let variables = (0..COUNT)
.map(|index| format!("var value_{index}: logic;"))
.collect::<Vec<_>>()
.join("\n");
let assignments = (0..COUNT)
.map(|index| format!("assign value_{index} = 0;"))
.collect::<Vec<_>>()
.join("\n");
crate::comb_loop_detect::reset_module_context_entries();
assert_comb_loop(
"independent declarations reuse immutable module metadata",
&format!(
r#"
module Top (o: output logic) {{
{variables}
{assignments}
assign o = value_0;
}}
"#,
),
false,
);
assert!(
crate::comb_loop_detect::module_context_entries() <= COUNT + 4,
"module variable/function maps must not be cloned per declaration",
);
}
#[test]
fn independent_assignments_only_check_referenced_source_visibility() {
for count in [64, 256, 1_024] {
for ty in [format!("logic<{count}>"), format!("logic [{count}]")] {
let mut code = format!("module Top (i: input {ty}, o: output {ty}) {{\n");
for index in 0..count {
code.push_str(&format!("assign o[{index}] = i[{index}];\n"));
}
code.push_str("}\n");
crate::comb_loop_detect::reset_visible_source_probes();
assert!(analyze(&code).is_empty());
let probes = crate::comb_loop_detect::visible_source_probes();
assert!(
(count..=count * 4).contains(&probes),
"each declaration must inspect its own sources, not every module region: count={count}, probes={probes}"
);
assert!(comb_loop_analysis_is_complete(&code));
}
}
}
#[test]
fn comb_loop_module_summary_preserves_if_expression_arm_exclusivity() {
assert_comb_loop(
"a child summary must not combine mutually exclusive expression arms",
r#"
module Conditional (
i : input logic<2>,
sel: input logic,
o : output logic<2>,
) {
assign o = if sel ? {i[0], 1'b0} : {1'b0, i[1]};
}
module Top (
sel: input logic,
o : output logic,
) {
var state: logic<2>;
inst conditional: Conditional (
i : state,
sel: sel,
o : state,
);
assign o = |state;
}
"#,
false,
);
}
#[test]
fn comb_loop_module_summary_retains_feedback_reachable_in_one_expression_arm() {
assert_comb_loop(
"a child summary retains feedback reachable in one expression arm",
r#"
module Conditional (
i : input logic,
sel: input logic,
o : output logic,
) {
assign o = if sel ? i : 1'b0;
}
module Top (
sel: input logic,
o : output logic,
) {
var state: logic;
inst conditional: Conditional (
i : state,
sel: sel,
o : state,
);
assign o = state;
}
"#,
true,
);
}
fn shifted_recurrence_module_code(feedback_output: usize, feedback_input: usize) -> String {
let clears = (0..4)
.filter(|index| *index != feedback_input)
.map(|index| format!("assign child_i[{index}] = 0;"))
.collect::<Vec<_>>()
.join("\n");
format!(
r#"
module ShiftClosure (
i: input logic<4>,
o: output logic<4>,
) {{
var x: logic<4>;
assign x = (x << 1) | i;
assign o = x;
}}
module Top (o: output logic) {{
var child_i: logic<4>;
var child_o: logic<4>;
inst u: ShiftClosure (i: child_i, o: child_o);
assign child_i[{feedback_input}] = child_o[{feedback_output}];
{clears}
assign o = child_o[0];
}}
"#,
)
}
#[test]
fn comb_loop_module_summary_shift_closure_terminates_without_wraparound() {
assert_comb_loop(
"a truncated left-shift recurrence does not wrap its high input to its low output",
&shifted_recurrence_module_code(0, 3),
false,
);
}
#[test]
fn comb_loop_module_summary_shift_closure_retains_reachable_feedback() {
assert_comb_loop(
"a left-shift recurrence retains transitive low-to-high feedthrough",
&shifted_recurrence_module_code(3, 0),
true,
);
}
#[test]
fn comb_loop_wide_module_summary_shift_closure_is_sparse() {
assert_comb_loop(
"a wide truncated-shift recurrence must not enumerate its declared width",
r#"
module ShiftClosure (
i: input logic<1000000>,
o: output logic<1000000>,
) {
var x: logic<1000000>;
assign x = (x << 1) | i;
assign o = x;
}
module Top (o: output logic) {
var child_o: logic<1000000>;
inst u: ShiftClosure (i: 0, o: child_o);
assign o = child_o[0];
}
"#,
false,
);
}
fn instance_ring_code(feedback_bits: &str) -> String {
format!(
r#"
module RingA (b_in: input logic<4>, a_out: output logic<4>) {{
assign a_out[3:2] = b_in[1:0];
assign a_out[1:0] = 2'b01;
}}
module RingB (a_in: input logic<4>, b_out: output logic<4>) {{
assign b_out[1:0] = a_in[{feedback_bits}];
assign b_out[3:2] = 2'b10;
}}
module Top (o: output logic<4>) {{
var a_net: logic<4>;
var b_net: logic<4>;
inst u_a: RingA (b_in: b_net, a_out: a_net);
inst u_b: RingB (a_in: a_net, b_out: b_net);
assign o = a_net;
}}
"#
)
}
#[test]
fn comb_loop_instance_ring_acyclic_per_bit_is_not_a_loop() {
assert_comb_loop(
"a whole-port instance ring that is acyclic per bit is not a loop",
&instance_ring_code("1:0"),
false,
);
}
#[test]
fn comb_loop_instance_ring_cyclic_per_bit_is_a_loop() {
assert_comb_loop(
"the same ring closed over its own bits is a loop",
&instance_ring_code("3:2"),
true,
);
}
#[test]
fn comb_loop_module_boundary_struct_members_of_one_port_are_bit_disjoint() {
assert_comb_loop(
"struct members of one port are bit-disjoint across a module boundary",
r#"
package pk {
struct req_t {
addr : logic<8>,
valid: logic ,
}
struct rsp_t {
rdata: logic<8>,
ready: logic ,
}
}
module Regs (
req_i : input pk::req_t,
resp_o: output pk::rsp_t,
) {
always_comb {
resp_o.ready = 1'b1;
resp_o.rdata = '0;
if req_i.valid {
resp_o.rdata = req_i.addr;
}
}
}
module Top (
i_clk: input clock ,
i_rst: input reset ,
o_d : output logic<8>,
) {
var rq: pk::req_t;
var rs: pk::rsp_t;
inst u: Regs (req_i: rq, resp_o: rs);
var st: logic<8>;
always_comb {
rq.valid = 0;
rq.addr = st;
if rs.ready {
rq.valid = 1;
}
}
always_ff {
if_reset {
st = 0;
} else {
st = st + {1'b0 repeat 7, rs.rdata[0]};
}
}
assign o_d = st;
}
"#,
false,
);
}
#[test]
fn comb_loop_through_child_without_top_level_component() {
let code = r#"
module ChildAnd (
i_en: input logic,
i_d : input logic,
o_y : output logic,
) {
assign o_y = i_en & i_d;
}
module Top (
i_clk: input clock,
i_rst: input reset,
i_d : input logic,
o_z : output logic,
) {
var q: logic;
var y: logic;
let eff : logic = q | y;
let won : logic = eff & i_d;
let hold: logic = q & !won;
inst u: ChildAnd (i_en: !hold, i_d, o_y: y);
always_ff {
if_reset {
q = 1'b0;
} else {
q = y;
}
}
assign o_z = q;
}
"#;
symbol_table::clear();
attribute_table::clear();
doc_comment_table::clear();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(code, &"").unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
assert!(analyzer.analyze_pass1("prj", &parser.veryl).is_empty());
assert!(Analyzer::analyze_post_pass1().is_empty());
assert!(
analyzer
.analyze_pass2(&parser.veryl, &mut context, Some(&mut ir))
.is_empty()
);
let has_loop = |ir: &Ir| {
crate::comb_loop_detect::check(ir)
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. }))
};
assert!(has_loop(&ir), "loop must be found on the full IR");
let child = resource_table::insert_str("ChildAnd");
ir.components
.retain(|c| !matches!(c, crate::ir::Component::Module(m) if m.name == child));
assert_eq!(ir.components.len(), 1);
assert!(
has_loop(&ir),
"loop must be found when the child has no top-level component"
);
}
#[test]
fn comb_loop_generic_specializations_report_one_diagnostic() {
let errors = analyze(
r#"
module Child #(
param W: u32 = 4,
) (
o: output logic<W>,
) {
var t: logic<W>;
always_comb {
t = t + 1;
o = t;
}
}
module Top (
a: output logic<4>,
b: output logic<8>,
) {
inst u0: Child #(W: 4) (o: a);
inst u1: Child #(W: 8) (o: b);
}
"#,
);
let loops = errors
.iter()
.filter(|error| matches!(error, AnalyzerError::CombinationalLoop { .. }))
.count();
assert_eq!(loops, 1, "{errors:?}");
}