Skip to content

Commit 27b3fc9

Browse files
authored
Merge pull request #3159 from tignear/test/comb-loop-analysis
Add behavioral tests for combinational-loop analysis
2 parents 6cb4b22 + 39c88da commit 27b3fc9

11 files changed

Lines changed: 7978 additions & 0 deletions

crates/analyzer/src/tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ use veryl_metadata::{Lint, Metadata, ProjectProperty};
77
use veryl_parser::Parser;
88
use veryl_parser::doc_comment_table;
99

10+
mod comb_loop_diagnostic_tests;
11+
12+
mod comb_loop_incomplete_tests;
13+
14+
mod comb_loop_interface_tests;
15+
16+
mod comb_loop_interface_function_tests;
17+
18+
mod comb_loop_procedural_tests;
19+
20+
mod comb_loop_retained_state_tests;
21+
22+
mod comb_loop_function_tests;
23+
24+
mod comb_loop_module_tests;
25+
26+
mod comb_loop_positional_tests;
27+
28+
mod comb_loop_sparse_tests;
29+
1030
#[track_caller]
1131
fn analyze(code: &str) -> Vec<AnalyzerError> {
1232
symbol_table::clear();
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Diagnostic ownership and provenance coverage for comb-loop analysis.
2+
use super::*;
3+
4+
fn captured_coverage_observation() -> (usize, Option<usize>, Option<usize>) {
5+
// Why this case exists: function summary coverage is mapped back into the
6+
// caller one captured region at a time. A caller default for value[1] must
7+
// prevent that bit's function assignment from appearing in value[0]'s
8+
// remaining coverage diagnostic.
9+
let code = r#"
10+
module Top (
11+
n: input logic<32>,
12+
o: output logic,
13+
) {
14+
var value: logic<2>;
15+
function write_bits (
16+
n: input logic<32>,
17+
) {
18+
for _index in 0..n {
19+
value[0] = 1;
20+
}
21+
for _index in 0..n {
22+
value[1] = 1;
23+
}
24+
}
25+
always_comb {
26+
value[1] = 0;
27+
write_bits(n);
28+
o = value[0];
29+
}
30+
}
31+
"#;
32+
let errors = analyze(code);
33+
let coverage = errors
34+
.iter()
35+
.filter_map(|error| match error {
36+
AnalyzerError::UncoveredBranch {
37+
error_locations, ..
38+
} => Some(error_locations),
39+
_ => None,
40+
})
41+
.collect::<Vec<_>>();
42+
(
43+
coverage.len(),
44+
coverage.first().map(|sites| sites.len()),
45+
coverage
46+
.first()
47+
.and_then(|sites| sites.first())
48+
.map(|site| site.offset()),
49+
)
50+
}
51+
52+
#[test]
53+
#[ignore = "SSA latch coverage follow-up after comb-loop migration: captured-region diagnostic count"]
54+
fn comb_loop_captured_coverage_has_one_diagnostic() {
55+
assert_eq!(captured_coverage_observation().0, 1);
56+
}
57+
58+
#[test]
59+
#[ignore = "SSA latch coverage follow-up after comb-loop migration: captured-region site count"]
60+
fn comb_loop_captured_coverage_has_one_site() {
61+
assert_eq!(captured_coverage_observation().1, Some(1));
62+
}
63+
64+
#[test]
65+
#[ignore = "SSA latch coverage follow-up after comb-loop migration: captured-region site provenance"]
66+
fn comb_loop_captured_coverage_uses_retained_bit_site() {
67+
let code = r#"
68+
module Top (
69+
n: input logic<32>,
70+
o: output logic,
71+
) {
72+
var value: logic<2>;
73+
function write_bits (
74+
n: input logic<32>,
75+
) {
76+
for _index in 0..n {
77+
value[0] = 1;
78+
}
79+
for _index in 0..n {
80+
value[1] = 1;
81+
}
82+
}
83+
always_comb {
84+
value[1] = 0;
85+
write_bits(n);
86+
o = value[0];
87+
}
88+
}
89+
"#;
90+
assert_eq!(captured_coverage_observation().2, code.find("value[0] = 1"));
91+
}
92+
93+
fn branch_weak_write_observation() -> (usize, Option<usize>, bool) {
94+
// Why this case exists: distinct weak writes can reach the same retained
95+
// object through different branches. Coverage reporting should present one
96+
// coherent variable diagnostic containing both assignment sites.
97+
let errors = analyze(
98+
r#"
99+
module Top (
100+
condition: input logic,
101+
left : input logic<2>,
102+
right : input logic<2>,
103+
o : output logic,
104+
) {
105+
var value: logic<4>;
106+
always_comb {
107+
if condition {
108+
value[left] = 1;
109+
} else {
110+
value[right] = 0;
111+
}
112+
o = value[0];
113+
}
114+
}
115+
"#,
116+
);
117+
let coverage = errors
118+
.iter()
119+
.filter(|error| matches!(error, AnalyzerError::UncoveredBranch { .. }))
120+
.collect::<Vec<_>>();
121+
let site_count = coverage.first().and_then(|error| match error {
122+
AnalyzerError::UncoveredBranch {
123+
error_locations, ..
124+
} => Some(error_locations.len()),
125+
_ => None,
126+
});
127+
let has_loop = errors
128+
.iter()
129+
.any(|error| matches!(error, AnalyzerError::CombinationalLoop { .. }));
130+
(coverage.len(), site_count, has_loop)
131+
}
132+
133+
#[test]
134+
#[ignore = "SSA latch coverage follow-up after comb-loop migration: merge weak-write diagnostic count"]
135+
fn comb_loop_branch_weak_writes_share_one_coverage_diagnostic() {
136+
assert_eq!(branch_weak_write_observation().0, 1);
137+
}
138+
139+
#[test]
140+
#[ignore = "SSA latch coverage follow-up after comb-loop migration: merge weak-write sites"]
141+
fn comb_loop_branch_weak_write_diagnostic_contains_both_sites() {
142+
assert_eq!(branch_weak_write_observation().1, Some(2));
143+
}
144+
145+
#[test]
146+
fn comb_loop_branch_weak_writes_do_not_create_feedback() {
147+
assert!(!branch_weak_write_observation().2);
148+
}
149+
150+
#[test]
151+
fn comb_loop_dynamic_loop_coverage_is_not_duplicated() {
152+
// Why this case exists: both legacy branch bookkeeping and MemorySSA can
153+
// observe the missing path inside a dynamic loop. Coverage ownership must
154+
// produce one warning rather than appending the same warning twice.
155+
let errors = analyze(
156+
r#"
157+
module Top (
158+
n : input logic<32>,
159+
condition: input logic,
160+
o : output logic,
161+
) {
162+
var value: logic;
163+
always_comb {
164+
for _index in 0..n {
165+
if condition {
166+
value = 1;
167+
}
168+
}
169+
o = value;
170+
}
171+
}
172+
"#,
173+
);
174+
assert_eq!(
175+
errors
176+
.iter()
177+
.filter(|error| matches!(error, AnalyzerError::UncoveredBranch { .. }))
178+
.count(),
179+
1,
180+
"dynamic-loop coverage must be reported once: {errors:#?}"
181+
);
182+
}
183+
184+
fn dynamic_loop_coverage_observation() -> (usize, Option<usize>, Option<usize>) {
185+
// Why this case exists: value[1] is fully defined even though it is also
186+
// written in a runtime loop. Its loop assignment must not be reported as a
187+
// covered site for the independently retained value[0].
188+
let code = r#"
189+
module Top (
190+
n : input logic<32>,
191+
condition: input logic,
192+
o : output logic<2>,
193+
) {
194+
var value: logic<2>;
195+
always_comb {
196+
value[1] = 0;
197+
for _index in 0..n {
198+
value[1] = 1;
199+
}
200+
if condition {
201+
value[0] = 1;
202+
}
203+
o = value;
204+
}
205+
}
206+
"#;
207+
let errors = analyze(code);
208+
let coverage = errors
209+
.iter()
210+
.filter_map(|error| match error {
211+
AnalyzerError::UncoveredBranch {
212+
error_locations, ..
213+
} => Some(error_locations),
214+
_ => None,
215+
})
216+
.collect::<Vec<_>>();
217+
(
218+
coverage.len(),
219+
coverage.first().map(|sites| sites.len()),
220+
coverage
221+
.first()
222+
.and_then(|sites| sites.first())
223+
.map(|site| site.offset()),
224+
)
225+
}
226+
227+
#[test]
228+
fn comb_loop_dynamic_loop_coverage_has_one_diagnostic() {
229+
assert_eq!(dynamic_loop_coverage_observation().0, 1);
230+
}
231+
232+
#[test]
233+
fn comb_loop_dynamic_loop_coverage_has_one_site() {
234+
assert_eq!(dynamic_loop_coverage_observation().1, Some(1));
235+
}
236+
237+
#[test]
238+
fn comb_loop_dynamic_loop_coverage_site_stays_region_local() {
239+
let code = r#"
240+
module Top (
241+
n : input logic<32>,
242+
condition: input logic,
243+
o : output logic<2>,
244+
) {
245+
var value: logic<2>;
246+
always_comb {
247+
value[1] = 0;
248+
for _index in 0..n {
249+
value[1] = 1;
250+
}
251+
if condition {
252+
value[0] = 1;
253+
}
254+
o = value;
255+
}
256+
}
257+
"#;
258+
assert_eq!(
259+
dynamic_loop_coverage_observation().2,
260+
code.find("value[0] = 1")
261+
);
262+
}

0 commit comments

Comments
 (0)