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_incomplete_assignment_without_comb_loop(case: &str, code: &str) {
let errors = analyze(code);
assert!(
errors
.iter()
.any(|error| matches!(error, AnalyzerError::UncoveredBranch { .. })),
"{case}: retained entry state must be diagnosed as an incomplete assignment: {errors:#?}"
);
assert!(
errors
.iter()
.all(|error| !matches!(error, AnalyzerError::CombinationalLoop { .. })),
"{case}: state retention is not combinational feedback: {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 unaligned_instance_function_actual_code(actual: &str) -> String {
format!(
r#"
module Child (i: input logic, o: output logic) {{ assign o = i; }}
module Top (o: output logic) {{
var feedback: logic;
var passed: logic;
function only_a (a: input logic, b: input logic) -> logic {{ return !a; }}
inst u: Child (i: {actual}, o: passed);
assign feedback = passed;
assign o = passed;
}}
"#
)
}
comb_loop_case!(
comb_loop_instance_actual_function_retains_module_capture,
"an instance actual function retains a module-scope capture",
r#"
module Child (i: input logic, o: output logic) { assign o = i; }
module Top (o: output logic) {
var x: logic;
var passed: logic;
function get_x () -> logic { return x; }
inst u: Child (i: get_x(), o: passed);
assign x = passed;
assign o = passed;
}
"#,
true
);
comb_loop_case!(
comb_loop_instance_actual_function_keeps_disjoint_capture,
"an instance actual function keeps a disjoint captured bit loop-free",
r#"
module Child (i: input logic, o: output logic) { assign o = i; }
module Top (o: output logic) {
var x: logic<2>;
var passed: logic;
function get_high () -> logic { return x[1]; }
inst u: Child (i: get_high(), o: passed);
assign x[0] = passed;
assign x[1] = 0;
assign o = passed;
}
"#,
false
);
comb_loop_case!(
comb_loop_unaligned_function_return_ignores_unused_actual,
"an unaligned function return ignores an unused actual",
unaligned_instance_function_actual_code("only_a(0, feedback)"),
false
);
comb_loop_case!(
comb_loop_unaligned_function_return_retains_used_actual,
"an unaligned function return retains its used actual",
unaligned_instance_function_actual_code("only_a(feedback, 0)"),
true
);
#[test]
fn comb_loop_core_semantics_and_region_regressions_function_call_caller_side_feedthrough_links_read_x_write_x()
{
let code = r#"
module ModuleA (
a: input logic<8>,
b: output logic<8>,
) {
function ident (
x: input logic<8>,
) -> logic<8> {
return x;
}
var c: logic<8>;
assign b = ident(c);
assign c = b;
}
"#;
let errors = analyze(code);
assert!(matches!(errors[0], AnalyzerError::CombinationalLoop { .. }));
}
#[test]
fn comb_loop_statement_order_and_observer_semantics_function_summaries_come_from_the_specialized_body_merely_evaluating_an()
{
let code = r#"
module Top (
o: output logic,
) {
function ignore (
unused: input logic,
) -> logic {
return 0;
}
var feedback: logic;
assign o = ignore(feedback);
assign feedback = o;
}
"#;
let errors = analyze(code);
assert!(
!errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. })),
"unused function argument formed a false loop: {errors:?}"
);
}
#[test]
fn comb_loop_function_global_read_contributes_value_dependency_a_called_function_retains_a_captured_module_scope_read()
{
assert_comb_loop(
"a called function retains a captured module-scope read",
r#"
module Top (
o: output logic,
) {
var x: logic;
function get_x () -> logic {
return x;
}
always_comb {
o = get_x();
}
always_comb {
x = o;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_function_global_read_contributes_value_dependency_a_captured_function_read_remains_feed_forward_without_a_return_path()
{
assert_comb_loop(
"a captured function read remains feed-forward without a return path",
r#"
module Top (
i: input logic,
o: output logic,
) {
var x: logic;
function get_x () -> logic {
return x;
}
always_comb {
o = get_x();
}
always_comb {
x = i;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_function_global_write_contributes_procedural_effect_a_called_function_retains_a_captured_module_scope_write()
{
assert_comb_loop(
"a called function retains a captured module-scope write",
r#"
module Top (
o: output logic,
) {
var x: logic;
function set_x (
a: input logic,
) {
x = a;
}
always_comb {
set_x(o);
}
always_comb {
o = x;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_function_global_write_contributes_procedural_effect_a_captured_function_write_remains_feed_forward_without_a_return_path()
{
assert_comb_loop(
"a captured function write remains feed-forward without a return path",
r#"
module Top (
i: input logic,
o: output logic,
) {
var x: logic;
function set_x (
a: input logic,
) {
x = a;
}
always_comb {
set_x(i);
}
always_comb {
o = x;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_vector_function_return_bits_a_vector_function_return_preserves_bit_identity()
{
assert_comb_loop(
"a vector function return preserves bit identity",
r#"
module Top (
o: output logic<2>,
) {
function identity (
x: input logic<2>,
) -> logic<2> {
return x;
}
var value: logic<2>;
assign o = identity(value);
assign value[0] = 0;
assign value[1] = o[0];
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_vector_function_return_bits_a_vector_function_return_retains_same_bit_feedback()
{
assert_comb_loop(
"a vector function return retains same-bit feedback",
r#"
module Top (
o: output logic<2>,
) {
function identity (
x: input logic<2>,
) -> logic<2> {
return x;
}
var value: logic<2>;
assign o = identity(value);
assign value[0] = o[0];
assign value[1] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_function_write_without_value_dependency_is_recorded() {
assert_comb_loop(
"a constant captured function write participates in procedural order",
r#"
module Top (
o: output logic,
) {
var x: logic;
function clear_x () {
x = 0;
}
always_comb {
clear_x();
o = x;
x = o;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_observer_function_side_effect_is_recorded() {
assert_comb_loop(
"a display argument retains a called function global write",
r#"
module Top (
o: output logic,
) {
var x: logic;
function touch (
a: input logic,
) -> logic {
x = a;
return 0;
}
always_comb {
$display("touch=%d", touch(o));
}
always_comb {
o = x;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_actual_function_side_effect_is_recorded() {
assert_comb_loop(
"an instance input actual retains a called function global write",
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: touch(o),
);
assign o = x;
}
"#,
true,
);
}
#[test]
#[ignore = "comb-loop follow-up: false negative; multiple captured writes require constrained dependency import"]
fn comb_loop_instance_actual_preserves_dependencies_between_captured_writes() {
assert_comb_loop(
"an instance actual preserves dependencies between captured writes",
r#"
module Sink (i: input logic) {}
module Top {
var q: logic;
var p: logic;
function f (a: input logic) -> logic {
p = q;
q = a;
return 0;
}
inst u: Sink (i: f(0));
always_comb { q = p; }
}
"#,
true,
);
}
#[test]
fn comb_loop_instance_actual_evaluates_an_unused_function_argument_once() {
assert_comb_loop(
"an unused outer argument retains its evaluated function side effect",
r#"
module Sink (
i: input logic,
) {}
module Top (
o: output logic,
) {
var x: logic;
function touch (
a: input logic,
) -> logic {
x = a;
return 0;
}
function ignore (
unused: input logic,
) -> logic {
return 0;
}
inst u: Sink (
i: ignore(touch(o)),
);
assign o = x;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_vector_function_output_bits() {
assert_comb_loop(
"a vector function output argument preserves bit identity",
r#"
module Top (
o: output logic<2>,
) {
function copy (
x: input logic<2>,
y: output logic<2>,
) {
y = x;
}
var value: logic<2>;
always_comb {
copy(value, o);
}
assign value[0] = 0;
assign value[1] = o[0];
}
"#,
false,
);
}
#[test]
fn comb_loop_function_output_state_does_not_leak_between_calls() {
assert_comb_loop(
"a conditionally assigned function output starts with fresh state on each call",
r#"
module Top (
p: output logic,
q: output logic,
) {
function f (
x: input logic,
y: output logic,
) {
if x {
y = 1;
}
}
var a: logic;
var b: logic;
always_comb {
f(a, p);
f(b, q);
}
assign a = q;
assign b = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_function_output_retains_same_call_control_feedback() {
assert_comb_loop(
"a function output retains control feedback within the same call",
r#"
module Top (
p: output logic,
q: output logic,
) {
function f (
x: input logic,
y: output logic,
) {
if x {
y = 1;
}
}
var a: logic;
var b: logic;
always_comb {
f(a, p);
f(b, q);
}
assign a = 0;
assign b = q;
}
"#,
true,
);
}
#[test]
fn comb_loop_function_local_state_does_not_leak_between_calls() {
assert_comb_loop(
"a function local starts with fresh state on each call",
r#"
module Top (
p: output logic,
q: output logic,
) {
function f (
x: input logic,
y: output logic,
) {
var temporary: logic;
if x {
temporary = 1;
}
y = temporary;
}
var a: logic;
var b: logic;
always_comb {
f(a, p);
f(b, q);
}
assign a = q;
assign b = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_function_return_state_does_not_leak_between_calls() {
assert_comb_loop(
"a function return starts with fresh state on each call",
r#"
module Top (
p: output logic,
q: output logic,
) {
function f (
x: input logic,
) -> logic {
if x {
return 1;
}
}
var a: logic;
var b: logic;
assign p = f(a);
assign q = f(b);
assign a = q;
assign b = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_wide_function_output_bits_without_scalarization() {
assert_comb_loop(
"a wide function output keeps disjoint endpoint bits independent",
r#"
module Top (
o: output logic<128>,
) {
function copy (
x: input logic<128>,
y: output logic<128>,
) {
y = x;
}
var value: logic<128>;
always_comb {
copy(value, o);
}
assign value[126:0] = 0;
assign value[127] = o[0];
}
"#,
false,
);
}
#[test]
fn comb_loop_wide_function_output_retains_matching_endpoint_feedback() {
assert_comb_loop(
"a wide function output retains feedback at the matching endpoint",
r#"
module Top (
o: output logic<128>,
) {
function copy (
x: input logic<128>,
y: output logic<128>,
) {
y = x;
}
var value: logic<128>;
always_comb {
copy(value, o);
}
assign value[126:0] = 0;
assign value[127] = o[127];
}
"#,
true,
);
}
#[test]
fn comb_loop_split_destination_reuses_one_function_evaluation() {
const WIDTH: usize = 256;
let observed_bits = (0..WIDTH)
.map(|bit| format!("assign observed[{bit}] = result[{bit}];"))
.collect::<Vec<_>>()
.join("\n");
crate::comb_loop_detect::reset_function_evaluation_count();
let errors = analyze(&format!(
r#"
module Top (
i : input logic<{WIDTH}>,
observed: output logic<{WIDTH}>,
) {{
function identity (
x: input logic<{WIDTH}>,
) -> logic<{WIDTH}> {{
return x;
}}
var result: logic<{WIDTH}>;
assign result = identity(i);
{observed_bits}
}}
"#
));
assert!(
errors.is_empty(),
"split observation of one function result is acyclic: {errors:#?}"
);
assert_eq!(
crate::comb_loop_detect::function_evaluation_count(),
1,
"splitting the destination must not reevaluate the same function call"
);
assert_eq!(
crate::comb_loop_detect::function_result_version_count(),
WIDTH,
"each split destination must request only its matching return region"
);
assert!(
crate::comb_loop_detect::function_result_region_probe_count() <= WIDTH * 12,
"return-region lookup must be logarithmic rather than scanning all regions per bit"
);
}
#[test]
fn comb_loop_static_loop_reevaluates_nested_function_actuals() {
crate::comb_loop_detect::reset_function_evaluation_count();
assert_comb_loop(
"a nested call is reevaluated when its static-loop actual changes",
r#"
module Top (
o: output logic,
) {
function inner (
x: input logic,
) -> logic {
return x;
}
function outer (
x: input logic<2>,
) -> logic {
var result: logic;
result = 0;
for i in 0..2 {
if inner(x[i]) {
result = 1;
} else {
result = 0;
}
}
return result;
}
var feedback: logic;
assign o = outer({feedback, 1'b0});
assign feedback = o;
}
"#,
true,
);
assert_eq!(
crate::comb_loop_detect::function_barrier_evaluation_count(),
2,
"both static-loop invocations must cross the callee cache barrier"
);
}
#[test]
fn comb_loop_preserves_split_function_return_bits() {
assert_comb_loop(
"a concatenated function return preserves each source bit",
r#"
module Top (
o: output logic<2>,
) {
function combine_bits (
high: input logic,
low : input logic,
) -> logic<2> {
return {high, low};
}
var high: logic;
var low: logic;
assign o = combine_bits(high, low);
assign high = o[0];
assign low = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_distinguishes_generic_function_specializations() {
let errors = analyze_with_large_stack(
r#"
module Top (
o: output logic,
) {
function recurse::<N: u32> (
x: input logic,
) -> logic {
gen M: u32 = N - 1;
if N == 1 {
return x;
} else {
return recurse::<M>(x);
}
}
var feedback: logic;
var passed: logic;
assign passed = recurse::<2>(feedback);
assign feedback = passed;
assign o = feedback;
}
"#,
);
let actual = errors
.iter()
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. }));
assert!(
actual,
"finite generic recursion retains the specialized feedthrough: {errors:?}"
);
}
#[test]
fn comb_loop_short_circuits_instance_actual_side_effects_a_constant_dead_instance_actual_branch_has_no_function_side_effect()
{
assert_comb_loop(
"a constant-dead instance actual branch has no function side effect",
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: if 1'b1 ? 0 : touch(o),
);
assign o = x;
}
"#,
false,
);
}
#[test]
fn comb_loop_short_circuits_instance_actual_side_effects_a_constant_taken_instance_actual_branch_retains_its_function_side_effect()
{
assert_comb_loop(
"a constant-taken instance actual branch retains its function side effect",
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: if 1'b0 ? 0 : touch(o),
);
assign o = x;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_ternary_positions_across_boundaries_a_function_ternary_actual_keeps_a_disjoint_bit_loop_free()
{
assert_comb_loop(
"a function ternary actual keeps a disjoint bit loop-free",
r#"
module Top (
sel: input logic,
o : output logic,
) {
function low (
x: input logic<2>,
) -> logic {
return x[0];
}
var value: logic<2>;
assign o = low(if sel ? value : 0);
assign value[0] = 0;
assign value[1] = o;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_ternary_positions_across_boundaries_a_function_ternary_actual_detects_its_corresponding_bit_loop()
{
assert_comb_loop(
"a function ternary actual detects its corresponding-bit loop",
r#"
module Top (
sel: input logic,
o : output logic,
) {
function low (
x: input logic<2>,
) -> logic {
return x[0];
}
var value: logic<2>;
assign o = low(if sel ? value : 0);
assign value[0] = o;
assign value[1] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_unpacked_function_actual_positions_an_unpacked_function_actual_keeps_disjoint_elements_loop_free()
{
assert_comb_loop(
"an unpacked function actual keeps disjoint elements loop-free",
r#"
module Top (
o: output logic,
) {
function high (
x: input logic [2],
) -> logic {
return x[1];
}
var value: logic [2];
assign o = high(value);
assign value[0] = o;
assign value[1] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_unpacked_function_actual_positions_an_unpacked_function_actual_detects_same_element_feedback()
{
assert_comb_loop(
"an unpacked function actual detects same-element feedback",
r#"
module Top (
o: output logic,
) {
function high (
x: input logic [2],
) -> logic {
return x[1];
}
var value: logic [2];
assign o = high(value);
assign value[0] = 0;
assign value[1] = o;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_function_actual_shift_positions_a_function_actual_left_shift_keeps_its_inserted_bit_loop_free()
{
assert_comb_loop(
"a function actual left shift keeps its inserted bit loop-free",
r#"
module Top (
o: output logic,
) {
function low (
x: input logic<4>,
) -> logic {
return x[0];
}
var value: logic<4>;
assign o = low(value << 1);
assign value[0] = 0;
assign value[1] = o;
assign value[3:2] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_function_actual_shift_positions_a_function_actual_left_shift_detects_its_live_shifted_bit()
{
assert_comb_loop(
"a function actual left shift detects its live shifted bit",
r#"
module Top (
o: output logic,
) {
function bit_one (
x: input logic<4>,
) -> logic {
return x[1];
}
var value: logic<4>;
assign o = bit_one(value << 1);
assign value[0] = o;
assign value[3:1] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_preserves_function_concat_output_positions_a_concatenated_function_output_keeps_a_disjoint_bit_loop_free()
{
assert_comb_loop(
"a concatenated function output keeps a disjoint bit loop-free",
r#"
module Top (
o: output logic<2>,
) {
function copy (
x: input logic<2>,
y: output logic<2>,
) {
y = x;
}
var value: logic<2>;
always_comb {
copy(value, {o[1], o[0]});
}
assign value[0] = 0;
assign value[1] = o[0];
}
"#,
false,
);
}
#[test]
fn comb_loop_preserves_function_concat_output_positions_a_concatenated_function_output_detects_its_corresponding_bit_loop()
{
assert_comb_loop(
"a concatenated function output detects its corresponding-bit loop",
r#"
module Top (
o: output logic<2>,
) {
function copy (
x: input logic<2>,
y: output logic<2>,
) {
y = x;
}
var value: logic<2>;
always_comb {
copy(value, {o[1], o[0]});
}
assign value[0] = o[0];
assign value[1] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_function_local_partial_writes_are_ordered()
{
assert_comb_loop(
"function local partial writes are ordered",
r#"
module Top (
d: input logic<8>,
q: output logic<8>,
) {
function swap_nibbles (
x: input logic<8>,
) -> logic<8> {
var tmp: logic<8>;
tmp[7:4] = x[3:0];
tmp[3:0] = x[7:4];
return tmp;
}
assign q = swap_nibbles(d);
}
"#,
false,
);
}
#[test]
fn comb_loop_statement_order_and_control_flow_regressions_nested_function_summary_preserves_a_real_cycle()
{
assert_comb_loop(
"nested function summary preserves a real cycle",
r#"
module Top (
o: output logic,
) {
function inner (x: input logic) -> logic {
return x;
}
function outer (x: input logic) -> logic {
return inner(x);
}
assign o = outer(o);
}
"#,
true,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_blocking_assignment_chain_uses_the_immediately_preceding_definition()
{
assert_comb_loop(
"blocking assignment chain uses the immediately preceding definition",
r#"
module Top (
a: input logic<8>,
o: output logic<8>,
) {
var tmp: logic<8>;
always_comb {
tmp = a;
tmp = tmp + 8'd1;
tmp = tmp << 1;
o = tmp;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_a_later_full_overwrite_kills_an_earlier_partial_entry_read()
{
assert_comb_loop(
"a later full overwrite kills an earlier partial entry read",
r#"
module Top (
o: output logic<2>,
) {
always_comb {
o[0] = o[1];
o = 0;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_a_full_overwrite_dominates_a_later_partial_read()
{
assert_comb_loop(
"a full overwrite dominates a later partial read",
r#"
module Top (
o: output logic<2>,
) {
always_comb {
o = 0;
o[0] = o[1];
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_both_branch_arms_define_the_value_consumed_after_the_merge()
{
assert_comb_loop(
"both branch arms define the value consumed after the merge",
r#"
module Top (
sel: input logic,
a : input logic,
b : input logic,
o : output logic,
) {
var selected: logic;
always_comb {
if sel {
selected = a;
} else {
selected = b;
}
o = selected;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_function_local_copy_retains_bit_precision_at_the_return()
{
assert_comb_loop(
"function local copy retains bit precision at the return",
r#"
module Top (
o: output logic,
) {
function low (x: input logic<8>) -> logic {
var tmp: logic<8>;
tmp = x;
return tmp[0];
}
var value: logic<8>;
assign o = low(value);
assign value[7] = o;
assign value[6:0] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_function_branch_condition_is_a_control_dependency_of_its_return()
{
assert_comb_loop(
"function branch condition is a control dependency of its return",
r#"
module Top (
o: output logic,
) {
function gated (x: input logic<8>) -> logic {
if x[7] {
return x[0];
} else {
return 0;
}
}
var value: logic<8>;
assign o = gated(value);
assign value[7] = o;
assign value[6:0] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_function_branch_ignores_a_bit_absent_from_value_and_control_flow()
{
assert_comb_loop(
"function branch ignores a bit absent from value and control flow",
r#"
module Top (
o: output logic,
) {
function gated (x: input logic<8>) -> logic {
if x[7] {
return x[0];
} else {
return 0;
}
}
var value: logic<8>;
assign o = gated(value);
assign value[6] = o;
assign value[7] = 0;
assign value[5:0] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_function_output_writeback_participates_in_procedural_order()
{
assert_comb_loop(
"function output writeback participates in procedural order",
r#"
module Top (
o: output logic,
) {
function copy (
x: input logic,
y: output logic,
) {
y = x;
}
var tmp: logic;
always_comb {
copy(o, tmp);
o = tmp;
}
}
"#,
true,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_static_array_elements_remain_distinct_regions()
{
assert_comb_loop(
"static array elements remain distinct regions",
r#"
module Top (
a: input logic<8>,
o: output logic<8>,
) {
var mem: logic<8> [2];
always_comb {
mem[0] = a;
mem[1] = mem[0];
o = mem[1];
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_read_before_write_across_static_array_elements_is_a_real_loop()
{
assert_comb_loop(
"read-before-write across static array elements is a real loop",
r#"
module Top (
o: output logic<8>,
) {
var mem: logic<8> [2];
always_comb {
mem[0] = mem[1];
mem[1] = mem[0];
o = mem[1];
}
}
"#,
true,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_static_struct_members_remain_distinct_regions()
{
assert_comb_loop(
"static struct members remain distinct regions",
r#"
module Top (
a: input logic<8>,
o: output logic<8>,
) {
struct Pair {
low : logic<8>,
high: logic<8>,
}
var pair: Pair;
always_comb {
pair.low = a;
pair.high = pair.low;
o = pair.high;
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_sparse_accesses_do_not_scale_with_a_huge_declared_width()
{
assert_comb_loop(
"sparse accesses do not scale with a huge declared width",
r#"
module Top (
a: input logic,
o: output logic,
) {
var huge: logic<1000000>;
always_comb {
huge[0] = a;
o = huge[999999];
}
}
"#,
false,
);
}
#[test]
fn comb_loop_region_and_function_mapping_regressions_dynamic_same_object_aliasing_uses_the_whole_longest_static_prefix()
{
assert_comb_loop(
"dynamic same-object aliasing uses the whole longest static prefix",
r#"
module Top (
index: input logic<2>,
o : output logic,
) {
var values: logic [4];
always_comb {
values[index] = o;
o = values[0];
}
}
"#,
true,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_must_not_taint_a_disjoint_actual_bit()
{
assert_comb_loop(
"function bit-select must not taint a disjoint actual bit",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
var value: logic<8>;
assign o = bit_zero(value);
assign value[0] = 0;
assign value[7] = o;
assign value[6:1] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_must_retain_same_bit_feedback()
{
assert_comb_loop(
"function bit-select must retain same-bit feedback",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
var value: logic<8>;
assign o = bit_zero(value);
assign value[0] = o;
assign value[7:1] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_through_concatenation_ignores_high_operands()
{
assert_comb_loop(
"function bit-select through concatenation ignores high operands",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
var value: logic<7>;
assign o = bit_zero({value, 1'b0});
assign value[6] = o;
assign value[5:0] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_through_concatenation_retains_low_operand()
{
assert_comb_loop(
"function bit-select through concatenation retains low operand",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
assign o = bit_zero({7'b0, o});
}
"#,
true,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_through_an_actual_slice_uses_its_low_bit()
{
assert_comb_loop(
"function bit-select through an actual slice uses its low bit",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
var value: logic<16>;
assign o = bit_zero(value[15:8]);
assign value[15] = o;
assign value[14:0] = 0;
}
"#,
false,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_bit_select_through_an_actual_slice_retains_its_low_bit_feedback()
{
assert_comb_loop(
"function bit-select through an actual slice retains its low-bit feedback",
r#"
module Top (
o: output logic,
) {
function bit_zero (x: input logic<8>) -> logic {
return x[0];
}
var value: logic<16>;
assign o = bit_zero(value[15:8]);
assign value[8] = o;
assign value[15:9] = 0;
assign value[7:0] = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_alias_and_opaque_effect_boundaries_function_region_crossing_a_concatenation_boundary_retains_both_operands()
{
assert_comb_loop(
"function region crossing a concatenation boundary retains both operands",
r#"
module Top (
o: output logic<2>,
) {
function middle (x: input logic<8>) -> logic<2> {
return x[4:3];
}
var high: logic<4>;
var low : logic<4>;
assign o = middle({high, low});
assign high[0] = o[1];
assign high[3:1] = 0;
assign low = 0;
}
"#,
true,
);
}
#[test]
fn comb_loop_function_capture_coverage_obeys_caller_order() {
for body in [
"value = 0; write_selected(index);",
"write_selected(index); value = 0;",
] {
let errors = analyze(&format!(
r#"
module Top (
index: input logic<2>,
o : output logic,
) {{
var value: logic<4>;
function write_selected (
index: input logic<2>,
) {{
value[index] = 1;
}}
always_comb {{
{body}
o = value[0];
}}
}}
"#
));
assert!(errors.is_empty(), "caller ordering is valid: {errors:#?}");
}
}
#[test]
#[ignore = "SSA latch coverage follow-up after comb-loop migration: captured function write"]
fn comb_loop_function_capture_without_default_retains_coverage() {
assert_incomplete_assignment_without_comb_loop(
"a captured weak write without a caller default remains incomplete",
r#"
module Top (
index: input logic<2>,
o : output logic,
) {
var value: logic<4>;
function write_selected (
index: input logic<2>,
) {
value[index] = 1;
}
always_comb {
write_selected(index);
o = value[0];
}
}
"#,
);
}
#[test]
#[ignore = "SSA latch coverage follow-up after comb-loop migration: uncalled function output"]
fn comb_loop_uncalled_function_still_checks_output_coverage() {
assert_incomplete_assignment_without_comb_loop(
"an uncalled function still has to assign its output on every path",
r#"
module Top (
o: output logic,
) {
function maybe_write (
n : input logic<32>,
value: output logic,
) {
for _index in 0..n {
value = 1;
}
}
assign o = 0;
}
"#,
);
}
#[test]
fn comb_loop_function_summary_fanout_is_memoized() {
let mut functions = String::from(
r#"
function f0 (
x: input logic,
) -> logic {
return x;
}
"#,
);
for depth in 1..=14 {
functions.push_str(&format!(
r#"
function f{depth} (
x: input logic,
) -> logic {{
return f{previous}(x) ^ f{previous}(x);
}}
"#,
previous = depth - 1,
));
}
let errors = analyze(&format!(
r#"
module Top (
i: input logic,
o: output logic,
) {{
{functions}
assign o = f14(i);
}}
"#
));
assert!(
errors.is_empty(),
"acyclic function fanout is valid: {errors:#?}"
);
}
#[test]
fn function_summaries_reuse_module_metadata() {
const COUNT: usize = 16;
let padding = (0..COUNT)
.map(|index| format!("var padding_{index}: logic;"))
.collect::<Vec<_>>()
.join("\n");
let mut functions = String::new();
let mut calls = String::new();
for index in 0..COUNT {
functions.push_str(&format!(
"function function_{index} (value: input logic) -> logic {{ return value; }}\n"
));
calls.push_str(&format!("padding_{index} = function_{index}(seed);\n"));
}
let code = format!(
r#"
module Top (seed: input logic, o: output logic) {{
{padding}
{functions}
always_comb {{
{calls}
o = padding_0;
}}
}}
"#
);
crate::comb_loop_detect::reset_module_context_entries();
let errors = analyze(&code);
assert!(
errors.is_empty(),
"independent calls are acyclic: {errors:#?}"
);
assert!(
crate::comb_loop_detect::module_context_entries() <= COUNT * 6,
"function summaries must share their module metadata: {}",
crate::comb_loop_detect::module_context_entries(),
);
}
#[test]
fn comb_loop_early_return_excludes_unreachable_function_dependency() {
assert_comb_loop(
"a return makes the following function dependency unreachable",
r#"
module Top (
o: output logic,
) {
function choose (
feedback: input logic,
) -> logic {
return 0;
return feedback;
}
var feedback: logic;
assign o = choose(feedback);
assign feedback = o;
}
"#,
false,
);
}
#[test]
fn comb_loop_early_branch_return_preserves_its_reachable_dependency() {
assert_comb_loop(
"an early branch return remains an alternative to the fallback return",
r#"
module Top (
select: input logic,
o : output logic,
) {
function choose (
select : input logic,
feedback: input logic,
) -> logic {
if select {
return feedback;
}
return 0;
}
var feedback: logic;
assign o = choose(select, feedback);
assign feedback = o;
}
"#,
true,
);
}
#[test]
fn comb_loop_all_branch_returns_exclude_following_dependency() {
assert_comb_loop(
"no path reaches a statement after both branches return",
r#"
module Top (
select: input logic,
o : output logic,
) {
function choose (
select : input logic,
feedback: input logic,
) -> logic {
if select {
return 0;
} else {
return 0;
}
return feedback;
}
var feedback: logic;
assign o = choose(select, feedback);
assign feedback = o;
}
"#,
false,
);
}
#[test]
fn comb_loop_branch_returns_preserve_reachable_function_dependency() {
assert_comb_loop(
"a reachable branch return preserves its feedback dependency",
r#"
module Top (
select: input logic,
o : output logic,
) {
function choose (
select : input logic,
feedback: input logic,
) -> logic {
if select {
return feedback;
} else {
return 0;
}
}
var feedback: logic;
assign o = choose(select, feedback);
assign feedback = o;
}
"#,
true,
);
}
#[test]
fn comb_loop_function_formal_high_bit_ignores_short_unsigned_actual() {
assert_comb_loop(
"a function formal high bit does not read an unsigned short actual",
r#"
module Top (o: output logic) {
var value: logic<2>;
function high (i: input logic<4>) -> logic { return i[3]; }
assign o = high(value);
assign value[0] = o;
assign value[1] = 0;
}
"#,
false,
);
}