Skip to content

Commit d289d4e

Browse files
committed
fix(analyzer): split multidimensional array prefixes
1 parent 31d6f44 commit d289d4e

2 files changed

Lines changed: 131 additions & 77 deletions

File tree

crates/analyzer/src/comb_loop_detect.rs

Lines changed: 131 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,34 @@ use daggy::petgraph::visit::EdgeRef;
2828
use std::collections::VecDeque;
2929
use veryl_parser::resource_table::StrId;
3030

31-
/// One array element. Bit precision lives in the sparse partition masks.
32-
type IdxKey = (VarId, usize);
31+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
32+
struct ArraySpan {
33+
start: usize,
34+
length: usize,
35+
}
36+
37+
impl ArraySpan {
38+
fn end(self) -> Option<usize> {
39+
self.start.checked_add(self.length)
40+
}
41+
42+
fn overlaps(self, other: Self) -> bool {
43+
let Some(left_end) = self.end() else {
44+
return false;
45+
};
46+
let Some(right_end) = other.end() else {
47+
return false;
48+
};
49+
self.start < right_end && other.start < left_end
50+
}
51+
}
52+
53+
/// One split unpacked-array interval. Bit precision lives in the masks.
54+
type IdxKey = (VarId, ArraySpan);
3355

3456
/// `(VarId, array_idx, range_idx)`. `range_idx` indexes the variable's
3557
/// `BitPartition`, so bit-disjoint reads/writes form disjoint nodes.
36-
type NodeKey = (VarId, usize, usize);
37-
38-
/// Unpacked elements not separated by a constant-index access are represented
39-
/// by one split piece, regardless of the declared array length.
40-
const SPLIT_REMAINDER_INDEX: usize = usize::MAX;
58+
type NodeKey = (VarId, ArraySpan, usize);
4159

4260
/// Per `IdxKey`, atomic bit-range masks. Two bits are in the same range
4361
/// iff they appear in the same set of per-decl masks.
@@ -62,26 +80,19 @@ impl BitPartition {
6280
.collect()
6381
}
6482

65-
fn overlapping_access(&self, id: VarId, index: usize, mask: &BigUint) -> Vec<NodeKey> {
66-
if index != SPLIT_REMAINDER_INDEX {
67-
return self
68-
.overlapping((id, index), mask)
69-
.into_iter()
70-
.map(|range| (id, index, range))
71-
.collect();
72-
}
73-
83+
fn overlapping_access(&self, id: VarId, access: ArraySpan, mask: &BigUint) -> Vec<NodeKey> {
7484
let zero = BigUint::default();
7585
let mut keys = self
7686
.ranges
7787
.iter()
7888
.filter(|((object, _), _)| *object == id)
79-
.flat_map(|((_, split_index), ranges)| {
89+
.filter(|((_, split), _)| split.overlaps(access))
90+
.flat_map(|((_, split), ranges)| {
8091
ranges
8192
.iter()
8293
.enumerate()
8394
.filter(|(_, range)| (*range & mask) != zero)
84-
.map(|(range, _)| (id, *split_index, range))
95+
.map(|(range, _)| (id, *split, range))
8596
})
8697
.collect::<Vec<_>>();
8798
keys.sort_unstable();
@@ -221,7 +232,7 @@ fn atomic_ranges(masks: &[BigUint], _width: usize) -> Vec<BigUint> {
221232
}
222233

223234
fn build_bit_partition(module: &Module, ctx: &mut Context) -> BitPartition {
224-
let mut masks: HashMap<(VarId, usize), Vec<BigUint>> = HashMap::default();
235+
let mut masks: HashMap<IdxKey, Vec<BigUint>> = HashMap::default();
225236

226237
for declaration in &module.declarations {
227238
if let Declaration::Comb(comb) = declaration {
@@ -237,7 +248,16 @@ fn build_bit_partition(module: &Module, ctx: &mut Context) -> BitPartition {
237248
for out in &inst.outputs {
238249
for dst in &out.dst {
239250
if let Some((idx, mask)) = eval_dst_mask(dst, &module.variables, ctx) {
240-
masks.entry((dst.id, idx)).or_default().push(mask);
251+
masks
252+
.entry((
253+
dst.id,
254+
ArraySpan {
255+
start: idx,
256+
length: 1,
257+
},
258+
))
259+
.or_default()
260+
.push(mask);
241261
}
242262
}
243263
}
@@ -252,32 +272,70 @@ fn build_bit_partition(module: &Module, ctx: &mut Context) -> BitPartition {
252272
}
253273
}
254274

255-
let mut ranges: HashMap<(VarId, usize), Vec<BigUint>> = HashMap::default();
256-
for (key, mut ms) in masks {
275+
let ranges = split_array_masks(module, masks);
276+
277+
BitPartition { ranges }
278+
}
279+
280+
fn split_array_masks(
281+
module: &Module,
282+
masks: HashMap<IdxKey, Vec<BigUint>>,
283+
) -> HashMap<IdxKey, Vec<BigUint>> {
284+
let mut accesses: HashMap<VarId, Vec<(ArraySpan, BigUint)>> = HashMap::default();
285+
for ((id, span), masks) in masks {
286+
for mask in masks {
287+
accesses.entry(id).or_default().push((span, mask));
288+
}
289+
}
290+
291+
let mut ranges = HashMap::default();
292+
for (id, accesses) in accesses {
293+
let mut boundaries = Vec::with_capacity(accesses.len() * 2);
294+
for (span, _) in &accesses {
295+
if span.length == 0 {
296+
continue;
297+
}
298+
boundaries.push(span.start);
299+
if let Some(end) = span.end() {
300+
boundaries.push(end);
301+
}
302+
}
303+
boundaries.sort_unstable();
304+
boundaries.dedup();
305+
257306
let width = module
258307
.variables
259-
.get(&key.0)
260-
.and_then(|v| v.total_width())
308+
.get(&id)
309+
.and_then(Variable::total_width)
261310
.unwrap_or(1);
262-
// Small values are cheap to scalarize and doing so lets an SSA value
263-
// cross a function boundary without collapsing independently used
264-
// bits into one version. Wide values remain split only at observed
265-
// access boundaries.
266-
if width <= 64 {
267-
ms.extend((0..width).map(|bit| BigUint::from(1u32) << bit));
268-
}
269-
let parts = atomic_ranges(&ms, width);
270-
if !parts.is_empty() {
271-
ranges.insert(key, parts);
311+
for boundary in boundaries.windows(2) {
312+
let split = ArraySpan {
313+
start: boundary[0],
314+
length: boundary[1] - boundary[0],
315+
};
316+
let mut split_masks = accesses
317+
.iter()
318+
.filter(|(access, _)| access.overlaps(split))
319+
.map(|(_, mask)| mask.clone())
320+
.collect::<Vec<_>>();
321+
if split_masks.is_empty() {
322+
continue;
323+
}
324+
if width <= 64 {
325+
split_masks.extend((0..width).map(|bit| BigUint::from(1u32) << bit));
326+
}
327+
let parts = atomic_ranges(&split_masks, width);
328+
if !parts.is_empty() {
329+
ranges.insert((id, split), parts);
330+
}
272331
}
273332
}
274-
275-
BitPartition { ranges }
333+
ranges
276334
}
277335

278336
fn collect_expr_masks(
279337
expr: &Expression,
280-
out: &mut HashMap<(VarId, usize), Vec<BigUint>>,
338+
out: &mut HashMap<IdxKey, Vec<BigUint>>,
281339
ctx: &mut Context,
282340
) {
283341
match expr {
@@ -311,7 +369,7 @@ fn collect_expr_masks(
311369

312370
fn collect_factor_masks(
313371
factor: &Factor,
314-
out: &mut HashMap<(VarId, usize), Vec<BigUint>>,
372+
out: &mut HashMap<IdxKey, Vec<BigUint>>,
315373
ctx: &mut Context,
316374
) {
317375
match factor {
@@ -331,7 +389,7 @@ fn collect_factor_masks(
331389

332390
fn collect_statement_masks(
333391
statements: &[Statement],
334-
out: &mut HashMap<(VarId, usize), Vec<BigUint>>,
392+
out: &mut HashMap<IdxKey, Vec<BigUint>>,
335393
ctx: &mut Context,
336394
) {
337395
for statement in statements {
@@ -706,8 +764,12 @@ fn collect_dst_node_keys(
706764
let Some((idx, mask)) = eval_dst_mask(dst, parent_vars, ctx) else {
707765
return;
708766
};
709-
for r in bit_part.overlapping((dst.id, idx), &mask) {
710-
out.push((dst.id, idx, r));
767+
let span = ArraySpan {
768+
start: idx,
769+
length: 1,
770+
};
771+
for r in bit_part.overlapping((dst.id, span), &mask) {
772+
out.push((dst.id, span, r));
711773
}
712774
}
713775

@@ -1633,7 +1695,7 @@ impl<'a> SsaProcedure<'a> {
16331695
let requested =
16341696
((mask >> low) << formal_offset) & ValueBigUint::gen_mask(formal_width);
16351697
for formal_key in self.keys_for_id(formal) {
1636-
if formal_key.1 != 0 {
1698+
if formal_key.1.start != 0 {
16371699
continue;
16381700
}
16391701
let Some(formal_mask) = self.key_mask(formal_key) else {
@@ -1732,11 +1794,10 @@ fn expression_has_unknown(expression: &Expression) -> bool {
17321794
}
17331795

17341796
/// Mirrors the masking logic of `AssignDestination::eval_assign`.
1735-
fn dst_writes(dst: &AssignDestination, ctx: &mut Context) -> Vec<(usize, BigUint)> {
1797+
fn dst_writes(dst: &AssignDestination, ctx: &mut Context) -> Vec<(ArraySpan, BigUint)> {
17361798
let Some(variable) = ctx.get_variable_info(dst.id) else {
17371799
return Vec::new();
17381800
};
1739-
let is_index_const = dst.index.is_const();
17401801
let is_select_const = dst.select.is_const();
17411802

17421803
let mask = if !is_select_const {
@@ -1748,35 +1809,17 @@ fn dst_writes(dst: &AssignDestination, ctx: &mut Context) -> Vec<(usize, BigUint
17481809
ValueBigUint::gen_mask_range(beg, end)
17491810
};
17501811

1751-
if variable.r#type.total_array().unwrap_or(2) > 1 && (!is_index_const || dst.index.0.is_empty())
1752-
{
1753-
return vec![(SPLIT_REMAINDER_INDEX, mask)];
1754-
}
1755-
1756-
let range = if !is_index_const {
1757-
variable.r#type.array.calc_range(&[])
1758-
} else {
1759-
let Some(index) = dst.index.eval_value(ctx) else {
1760-
return Vec::new();
1761-
};
1762-
variable.r#type.array.calc_range(&index)
1763-
};
1764-
1765-
let mut out = Vec::new();
1766-
if let Some((beg, end)) = range {
1767-
for i in beg..=end {
1768-
out.push((i, mask.clone()));
1769-
}
1770-
}
1771-
out
1812+
array_access_span(&dst.index, &variable.r#type, ctx)
1813+
.map(|span| vec![(span, mask)])
1814+
.unwrap_or_default()
17721815
}
17731816

17741817
fn var_reads(
17751818
id: VarId,
17761819
index: &VarIndex,
17771820
select: &VarSelect,
17781821
ctx: &mut Context,
1779-
) -> Vec<(usize, BigUint)> {
1822+
) -> Vec<(ArraySpan, BigUint)> {
17801823
let Some(variable) = ctx.variables.get(&id).cloned() else {
17811824
return Vec::new();
17821825
};
@@ -1787,16 +1830,28 @@ fn var_reads(
17871830
} else {
17881831
conservative_select_mask(select, &variable.r#type, ctx)
17891832
};
1790-
if variable.r#type.total_array().unwrap_or(2) > 1 && (!index.is_const() || index.0.is_empty()) {
1791-
return vec![(SPLIT_REMAINDER_INDEX, mask)];
1792-
}
1793-
if index.is_const()
1794-
&& let Some(idx_path) = index.eval_value(ctx)
1795-
&& let Some(flat) = variable.r#type.array.calc_index(&idx_path)
1796-
{
1797-
return vec![(flat, mask)];
1798-
}
1799-
vec![(SPLIT_REMAINDER_INDEX, mask)]
1833+
array_access_span(index, &variable.r#type, ctx)
1834+
.map(|span| vec![(span, mask)])
1835+
.unwrap_or_default()
1836+
}
1837+
1838+
fn array_access_span(
1839+
index: &VarIndex,
1840+
r#type: &crate::ir::Type,
1841+
ctx: &mut Context,
1842+
) -> Option<ArraySpan> {
1843+
let prefix_len = index
1844+
.0
1845+
.iter()
1846+
.take_while(|expression| expression.comptime().is_const)
1847+
.count();
1848+
let prefix = VarIndex(index.0[..prefix_len].to_vec());
1849+
let values = prefix.eval_value(ctx)?;
1850+
let (start, inclusive_end) = r#type.array.calc_range(&values)?;
1851+
Some(ArraySpan {
1852+
start,
1853+
length: inclusive_end.checked_sub(start)?.checked_add(1)?,
1854+
})
18001855
}
18011856

18021857
fn conservative_select_mask(

crates/analyzer/src/tests/comb_loop_sparse_tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ fn comb_loop_aliases_overlapping_unknown_regions_overlapping_dynamic_prefixes_re
213213
}
214214

215215
#[test]
216-
#[ignore = "comb-loop migration: false positive; sparse and dynamic regions"]
217216
fn comb_loop_aliases_overlapping_unknown_regions_disjoint_dynamic_prefixes_remain_independent() {
218217
assert_comb_loop(
219218
"disjoint dynamic prefixes remain independent",

0 commit comments

Comments
 (0)