Skip to main content

vj_sim/constraints/build/
arrays.rs

1//! Array constraints for the [`Builder`] (plan tasks C11, C12, C14; IEEE
2//! 1800-2023 18.4, 18.5.7).
3//!
4//! A `rand` dynamic array or queue property is one *array* of the program
5//! ([`ArrayInfo`]); constraints over it build array-level IR
6//! ([`ArrayNode`]) that does not yet know how many elements the array has, and
7//! the run-time expansion ([`super::super::array`]) turns that into scalar IR
8//! once the sizes are decided:
9//!
10//! * `A.size` / `A.size()` is [`ArrayNode::Size`] (18.4: a size constraint
11//!   resizes the array);
12//! * `A[i]` is [`ArrayNode::Elem`]; the index is a constant, a state variable,
13//!   a loop variable or the array size, never a random variable (the elements
14//!   are the random variables, so a random index would be a selection among
15//!   them: [`BuildError::BadArray`]);
16//! * `foreach (A[i]) body` is [`ArrayNode::Foreach`] over a loop variable the
17//!   body reads as [`ArrayNode::Loop`] (18.5.7.1). The loop variable's name is
18//!   not in the semantic tree, so it is recovered from the syntax tree;
19//! * `A.sum() with (e)` (and `product`, `and`, `or`, `xor`) is
20//!   [`ArrayNode::Reduce`] (18.5.7.2), its `item` and `item.index` bound the
21//!   same way.
22//!
23//! A `foreach` over a *packed* value or a fixed-size unpacked array with
24//! declared elements has a size known now, so it unrolls immediately with the
25//! loop variable a constant.
26//!
27//! # Limits (each a typed error, never a silent drop)
28//!
29//! * A multi-dimensional `foreach` unrolls only over fixed-size dimensions; an array of
30//!   dynamic arrays, a dynamic dimension in `foreach (m[i, j])` and a `randc` array are
31//!   [`BuildError::Unsupported`].
32//! * Elements are integral (packed structs included, 18.4: a packed struct is
33//!   one variable); an unpacked-struct or class-handle element is
34//!   [`BuildError::NonIntegral`].
35//! * An associative array has no randomizable size or index (18.4): a `rand`
36//!   one has its existing values redrawn (and nothing else), and any
37//!   constraint on its size or elements is [`BuildError::BadArray`].
38//! * A `dist`, `solve ... before` or real constraint inside a `foreach` body
39//!   (it would have to be one per element) and a `dist` over array elements
40//!   are [`BuildError::Unsupported`].
41
42use super::aggregate::{RawStep, Sel, aggregate_chain};
43use super::real::strip_conversions;
44use super::*;
45use crate::constraints::ir::{ArrayId, ArrayNode, LoopId, ReduceOp};
46use sv_lang::KnownSystemName;
47use sv_lang::kinds::SymbolKind;
48use sv_lang::nodes::LoopConstraintSyntax;
49
50/// A dynamic array or queue the constraints reach.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct ArrayInfo {
53    /// The property's key.
54    pub symbol: SymbolId,
55    /// Its declared name.
56    pub name: String,
57    /// The element type (a 4-state element is solved as 2-state).
58    pub elem_ty: Ty,
59    /// Whether the property is `rand`: its size and elements are then random
60    /// variables of the program (an inactive `rand_mode` is decided at run
61    /// time). A non-`rand` array a constraint reads is state.
62    pub rand: bool,
63    /// For an array of an enum type, the named constants every element takes
64    /// (LRM 6.19.3, 18.3).
65    pub elem_domain: Option<Vec<BigUint>>,
66    /// An associative array (only ever `rand`, and only its existing values
67    /// are drawn: no constraint can name it, 18.4).
68    pub assoc: bool,
69    /// An array of dynamic arrays or queues (`rand int a[][]`, 18.4): its
70    /// elements are rows, and [`Self::elem_ty`] / [`Self::elem_domain`] describe
71    /// the leaf elements of the rows.
72    pub nested: bool,
73    /// For the row template of an array of arrays ([`ArrayNode::RowScope`]):
74    /// that array. A template is not a property; it is how a constraint names
75    /// "the row `a[i]` selects".
76    pub template_of: Option<ArrayId>,
77    /// For an array of fixed-size rows (`rand int b[][4]`): the row length `N`;
78    /// every row is pinned to `N` elements by an implicit constraint and stored
79    /// inline in its outer element. Zero for dynamic rows.
80    pub inner_fixed: u32,
81    /// How many levels of rows lie below this array: 0 for a plain dynamic
82    /// array or queue, 1 for `int a[][]` / `int b[][4]`, 2 for `int a[][][]`.
83    /// Zero exactly when [`Self::nested`] is false. A row template has one
84    /// level fewer than the array it is the template of.
85    pub row_depth: u32,
86    /// Where the array's handle sits in its property when it is a leaf of an
87    /// aggregate (a row of `rand int a[3][]`, a queue member of a struct).
88    pub window: Option<Window>,
89}
90
91/// The shape of the array `e` names. A row `b[i]` of `rand int b[][4]` is
92/// stored inline but is one row of the outer array's template, so it counts as
93/// a dynamic array like any other row.
94pub(super) fn expr_shape(e: Expression<'_>) -> Option<ArrayShape> {
95    let shape = array_shape(e.expr_type()?);
96    if shape == Some(ArrayShape::Fixed) {
97        let inner = strip_conversions(e);
98        if inner.kind() == ExpressionKind::ElementSelect
99            && inner
100                .select_value()
101                .and_then(|v| v.expr_type())
102                .and_then(array_shape)
103                == Some(ArrayShape::Collection)
104        {
105            return Some(ArrayShape::Collection);
106        }
107    }
108    shape
109}
110
111/// An array operand: the array and the row scopes (template, row index) it is
112/// read through, outermost first ([`Builder::array_ref`]).
113pub(super) type ArrayRef = (ArrayId, Vec<(ArrayId, Expr)>);
114
115/// What a loop name stands for while its body is built.
116#[derive(Clone, Debug)]
117pub(super) enum LoopBind {
118    /// A run-time loop variable ([`ArrayNode::Loop`]).
119    Runtime(LoopId, Ty),
120    /// A constant index of an unrolled loop.
121    Const(i64),
122    /// The element an unrolled reduction over a fixed-size array is at: the
123    /// expression of that element.
124    Elem(Expr),
125}
126
127/// One name bound by a `foreach` or a reduction's iterator.
128#[derive(Clone, Debug)]
129pub(super) struct LoopScope {
130    name: String,
131    /// What the name itself reads: the index of a `foreach` variable, the
132    /// element of a reduction's `item`.
133    value: LoopBind,
134    /// What `name.index` reads (a reduction iterator only).
135    index: Option<LoopBind>,
136}
137
138/// How an array-typed expression is stored.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub(super) enum ArrayShape {
141    /// A dynamic array or queue: its size is a run-time value.
142    Collection,
143    /// An associative array.
144    Assoc,
145    /// A fixed-size unpacked array.
146    Fixed,
147}
148
149pub(super) fn array_shape(t: Type<'_>) -> Option<ArrayShape> {
150    let c = t.canonical();
151    if c.is_associative_array() {
152        Some(ArrayShape::Assoc)
153    } else if c.is_dynamically_sized_array() || c.is_queue() {
154        Some(ArrayShape::Collection)
155    } else if c.is_unpacked_array() {
156        Some(ArrayShape::Fixed)
157    } else {
158        None
159    }
160}
161
162fn bad_array(what: impl Into<String>) -> BuildError {
163    BuildError::BadArray { what: what.into() }
164}
165
166/// The value of a constant `Expr` of type `ty` as a signed integer.
167pub(super) fn const_i64(ty: Ty, v: &BigUint) -> Option<i64> {
168    let signed = BigInt::from(v.clone());
169    let signed = if ty.signed && v.bit(u64::from(ty.width) - 1) {
170        signed - (BigInt::one() << ty.width)
171    } else {
172        signed
173    };
174    i64::try_from(signed).ok()
175}
176
177impl Builder<'_> {
178    pub(super) fn arrays_allowed(&self, what: &str) -> Result<(), BuildError> {
179        match self.no_arrays {
180            None => Ok(()),
181            Some(inside) => Err(unsupported(
182                format!("{what} inside {inside}"),
183                "C14 (array-level values have no weight or bound at build time)",
184            )),
185        }
186    }
187
188    /// Registers the `rand` dynamic-array or queue property `sym` as an
189    /// active random array and returns its id (idempotent). Its size and
190    /// every element are random variables at run time (IEEE 1800-2023 18.4).
191    pub fn declare_rand_array(&mut self, sym: Symbol<'_>) -> Result<u32, BuildError> {
192        let uid = sym.uid();
193        let name = sym.name().to_string();
194        match sym.rand_mode() {
195            RandMode::None => return Err(BuildError::NotRand { name }),
196            RandMode::RandC => {
197                return Err(unsupported(
198                    format!("the `randc` array `{name}`: per-element cycling"),
199                    "C14",
200                ));
201            }
202            RandMode::Rand => {}
203        }
204        if let Some(&id) = self.array_ix.get(&uid) {
205            self.arrays[id as usize].rand = true;
206            return Ok(id);
207        }
208        let vt = sym.value_type().ok_or_else(|| BuildError::Missing {
209            what: format!("value type for `{name}`"),
210        })?;
211        let info = self.array_info(uid.clone(), name, vt, true)?;
212        let id = self.arrays.len() as u32;
213        self.arrays.push(info);
214        self.array_ix.insert(uid, id);
215        Ok(id)
216    }
217
218    /// Registers the dynamic-array or queue `sym` as an active random array
219    /// whether or not it is declared `rand` (LRM 18.11, 18.12: an argument of
220    /// `randomize(v)` / `std::randomize(v)` is random for that call). A `randc`
221    /// array is refused as in [`Self::declare_rand_array`]. Idempotent.
222    pub fn declare_array_forced(&mut self, sym: Symbol<'_>) -> Result<u32, BuildError> {
223        let uid = sym.uid();
224        let name = sym.name().to_string();
225        if matches!(sym.rand_mode(), RandMode::RandC) {
226            return Err(unsupported(
227                format!("the `randc` array `{name}`: per-element cycling"),
228                "C14",
229            ));
230        }
231        if let Some(&id) = self.array_ix.get(&uid) {
232            self.arrays[id as usize].rand = true;
233            return Ok(id);
234        }
235        let vt = sym.value_type().ok_or_else(|| BuildError::Missing {
236            what: format!("value type for `{name}`"),
237        })?;
238        let info = self.array_info(uid.clone(), name, vt, true)?;
239        let id = self.arrays.len() as u32;
240        self.arrays.push(info);
241        self.array_ix.insert(uid, id);
242        Ok(id)
243    }
244
245    pub(super) fn array_info(
246        &self,
247        symbol: SymbolId,
248        name: String,
249        vt: Type<'_>,
250        rand: bool,
251    ) -> Result<ArrayInfo, BuildError> {
252        match array_shape(vt) {
253            Some(ArrayShape::Collection) => {}
254            // Declared `rand` it is registered so its values are redrawn; a
255            // constraint operand is refused where the operand is resolved
256            // ([`Builder::array_of`]).
257            Some(ArrayShape::Assoc) if rand => {}
258            Some(ArrayShape::Assoc) => return Err(Self::assoc_operand(&name)),
259            // A fixed-size array is not one array-level value: its elements
260            // are leaf variables (`super::aggregate`), and the methods that
261            // take the whole array unroll over them.
262            Some(ArrayShape::Fixed) => {
263                return Err(unsupported(
264                    format!(
265                        "the fixed-size unpacked array `{name}` as a whole-array constraint \
266                         operand (name its elements, or use a reduction method or `foreach`)"
267                    ),
268                    "C14",
269                ));
270            }
271            None => {
272                return Err(BuildError::NonIntegral {
273                    what: format!(
274                        "`{name}` of type `{}`, which is not an array",
275                        vt.to_sv_string()
276                    ),
277                });
278            }
279        }
280        let et = vt
281            .canonical()
282            .element_type()
283            .ok_or_else(|| BuildError::Missing {
284                what: format!("element type of the array `{name}`"),
285            })?;
286        // An array of dynamic arrays or queues: the leaf is the element of its
287        // rows (LRM 7.4.5, 18.4); the rows are solved as arrays of their own,
288        // one level at a time. Only the innermost dimension may be a fixed-size
289        // one (`int b[][4]`, `int c[][][4]`), stored inline in its outer element.
290        let mut row_depth = 0u32;
291        let mut inner_fixed = 0u32;
292        let mut leaf = et;
293        if array_shape(vt) == Some(ArrayShape::Collection) {
294            let mut cur = vt;
295            loop {
296                let elem = cur
297                    .canonical()
298                    .element_type()
299                    .ok_or_else(|| BuildError::Missing {
300                        what: format!("element type of the array `{name}`"),
301                    })?;
302                match array_shape(elem) {
303                    None => {
304                        leaf = elem;
305                        break;
306                    }
307                    Some(ArrayShape::Collection) => {
308                        row_depth += 1;
309                        cur = elem;
310                    }
311                    Some(ArrayShape::Fixed) => {
312                        let row = elem.canonical().element_type();
313                        let Some(r) = row.filter(|r| array_shape(*r).is_none()) else {
314                            return Err(unsupported(
315                                format!(
316                                    "`{name}`: a fixed-size row whose elements are arrays (a \
317                                     fixed-size dimension in the middle of an array of arrays)"
318                                ),
319                                "C14",
320                            ));
321                        };
322                        // `int b[][4]`: rows of a fixed length, stored inline. The
323                        // position of element `j` is `j - lo`, and a dynamic row's
324                        // elements are numbered from 0, so `lo` must be 0.
325                        let range = elem.canonical().fixed_unpacked_array_range();
326                        let (lo, hi) = (
327                            i64::from(range.left).min(i64::from(range.right)),
328                            i64::from(range.left).max(i64::from(range.right)),
329                        );
330                        if lo != 0 {
331                            return Err(unsupported(
332                                format!(
333                                    "`{name}`: a fixed-size row whose lowest index is not 0 \
334                                     (`[{}:{}]`)",
335                                    range.left, range.right
336                                ),
337                                "C14",
338                            ));
339                        }
340                        row_depth += 1;
341                        inner_fixed = (hi + 1) as u32;
342                        leaf = r;
343                        break;
344                    }
345                    Some(ArrayShape::Assoc) => {
346                        return Err(unsupported(
347                            format!(
348                                "`{name}`: a dynamic array or queue whose elements are \
349                                 fixed-size or associative arrays"
350                            ),
351                            "C14",
352                        ));
353                    }
354                }
355            }
356        }
357        let nested = row_depth > 0;
358        let elem_ty = ty_of(Some(leaf), &format!("an element of the array `{name}`"))?;
359        let elem_domain = Self::enum_values(leaf, elem_ty)?;
360        Ok(ArrayInfo {
361            symbol,
362            name,
363            elem_ty,
364            rand,
365            elem_domain,
366            assoc: array_shape(vt) == Some(ArrayShape::Assoc),
367            nested,
368            template_of: None,
369            inner_fixed,
370            row_depth,
371            window: None,
372        })
373    }
374
375    fn assoc_operand(name: &str) -> BuildError {
376        bad_array(format!(
377            "`{name}` is an associative array: its size and indices are not randomizable and \
378             its elements are not constraint operands (IEEE 1800-2023 18.4)"
379        ))
380    }
381
382    /// The array a constraint operand names: a dynamic array or queue property
383    /// of this class, registered on first use (as state when it is not
384    /// `rand`).
385    pub(super) fn array_of(&mut self, e: Expression<'_>) -> Result<ArrayId, BuildError> {
386        match self.array_ref(e)? {
387            (id, rows) if rows.is_empty() && self.arrays[id as usize].nested => Err(unsupported(
388                format!(
389                    "the array of arrays `{}` as a whole-array operand here (supported: `.size`, \
390                     `foreach`, and its rows' sizes, elements and reductions)",
391                    self.arrays[id as usize].name
392                ),
393                "C14",
394            )),
395            (id, rows) if rows.is_empty() => Ok(id),
396            (id, _) => Err(unsupported(
397                format!(
398                    "a row of the array of arrays `{}` as a whole-array operand (name its \
399                     elements `a[i][j]`, or use `.size`, a reduction method, `inside` or \
400                     `foreach` on the row)",
401                    self.arrays[self.arrays[id as usize].template_of.unwrap_or(id) as usize].name
402                ),
403                "C14",
404            )),
405        }
406    }
407
408    /// The row template of the array of arrays `outer` (created on first use).
409    pub(super) fn row_template(&mut self, outer: ArrayId) -> ArrayId {
410        if let Some(&t) = self.templates.get(&outer) {
411            return t;
412        }
413        let o = &self.arrays[outer as usize];
414        let info = ArrayInfo {
415            symbol: o.symbol.clone(),
416            name: format!("{}[]", o.name),
417            elem_ty: o.elem_ty,
418            rand: false,
419            elem_domain: o.elem_domain.clone(),
420            assoc: false,
421            nested: o.row_depth > 1,
422            template_of: Some(outer),
423            inner_fixed: 0,
424            row_depth: o.row_depth.saturating_sub(1),
425            window: None,
426        };
427        let id = self.arrays.len() as ArrayId;
428        self.arrays.push(info);
429        self.templates.insert(outer, id);
430        id
431    }
432
433    /// An array operand: the array itself, or for `a[i]` of an array of arrays
434    /// the row template and the row expression `i` ([`ArrayNode::RowScope`]).
435    /// For `a[i][j]` of an array of arrays of arrays the chain holds one scope
436    /// per level, outermost first, and the array is the innermost template.
437    pub(super) fn array_ref(&mut self, e: Expression<'_>) -> Result<ArrayRef, BuildError> {
438        let e = strip_conversions(e);
439        if e.kind() == ExpressionKind::ElementSelect
440            && let Some(id) = self.array_leaf(e)?
441        {
442            return Ok((id, Vec::new()));
443        }
444        if e.kind() == ExpressionKind::ElementSelect {
445            let base = e.select_value().ok_or_else(|| BuildError::Missing {
446                what: "base of a row select".into(),
447            })?;
448            let sel = e.selector().ok_or_else(|| BuildError::Missing {
449                what: "index of a row select".into(),
450            })?;
451            let (outer, mut chain) = self.array_ref(base)?;
452            if !self.arrays[outer as usize].nested {
453                return Err(unsupported(
454                    format!(
455                        "an element select `{}[..]` as an array operand: it is not an array of \
456                         arrays",
457                        self.arrays[outer as usize].name
458                    ),
459                    "C14",
460                ));
461            }
462            self.arrays_allowed("a row of an array of arrays")?;
463            let row = self.expr(sel)?;
464            let mut has_var = false;
465            row.for_each_var(&mut |_, _| has_var = true);
466            if has_var {
467                return Err(bad_array(format!(
468                    "a random variable in the row index of the array `{}`: an index is a \
469                     constant, a state variable, a loop variable or the size (IEEE 1800-2023 \
470                     18.5.7.1)",
471                    self.arrays[outer as usize].name
472                )));
473            }
474            let t = self.row_template(outer);
475            chain.push((t, row));
476            return Ok((t, chain));
477        }
478        Ok((self.array_named(e)?, Vec::new()))
479    }
480
481    fn reject_nested(&self, a: ArrayId, what: &str) -> Result<(), BuildError> {
482        if self.arrays[a as usize].nested {
483            return Err(unsupported(
484                format!(
485                    "{what} over the array of arrays `{}` itself (name a row `a[i]`)",
486                    self.arrays[a as usize].name
487                ),
488                "C14",
489            ));
490        }
491        Ok(())
492    }
493
494    /// `body` with the rows of `chain` bound, outermost scope outermost.
495    fn row_scoped(chain: Vec<(ArrayId, Expr)>, body: Expr) -> Expr {
496        chain.into_iter().rev().fold(body, |body, (t, row)| {
497            Expr::Array(ArrayNode::RowScope {
498                template: t,
499                row: Box::new(row),
500                body: Box::new(body),
501            })
502        })
503    }
504
505    /// The dynamic array or queue that `e` names inside an aggregate property
506    /// (`a[1]` of `rand int a[3][]`, `s.q` of a struct): an array of its own
507    /// whose handle sits at a window of the property. `None` when `e` is not
508    /// such a leaf.
509    fn array_leaf(&mut self, e: Expression<'_>) -> Result<Option<ArrayId>, BuildError> {
510        if e.expr_type().and_then(array_shape) != Some(ArrayShape::Collection) {
511            return Ok(None);
512        }
513        let Some(chain) = aggregate_chain(e, &self.forced_roots) else {
514            return Ok(None);
515        };
516        if chain.steps.is_empty() {
517            return Ok(None);
518        }
519        self.array_from_chain(chain).map(Some)
520    }
521
522    /// The array the leaf `chain` names (see [`Self::array_leaf`]).
523    fn array_from_chain(
524        &mut self,
525        chain: super::aggregate::Chain<'_>,
526    ) -> Result<ArrayId, BuildError> {
527        let leaf = match self.resolve_chain(&chain)? {
528            Sel::Leaf(leaf) => leaf,
529            Sel::Pick { .. } => {
530                return Err(unsupported(
531                    "a dynamic array or queue element of a fixed-size array selected at a \
532                     computed index (use a constant index or a `foreach` variable)",
533                    "C14",
534                ));
535            }
536        };
537        let key = leaf.root.derive(&Self::leaf_tag(&leaf.path));
538        if let Some(&id) = self.array_ix.get(&key) {
539            return Ok(id);
540        }
541        if leaf.mode != RandMode::None {
542            return Err(unsupported(
543                format!(
544                    "the random array `{}`, a leaf of an aggregate property that was not \
545                     declared random",
546                    leaf.name
547                ),
548                "C14",
549            ));
550        }
551        let id = self.new_leaf_array(&leaf, false)?;
552        self.array_ix.insert(key, id);
553        Ok(id)
554    }
555
556    fn array_named(&mut self, e: Expression<'_>) -> Result<ArrayId, BuildError> {
557        if let Some(id) = self.array_leaf(e)? {
558            return Ok(id);
559        }
560        let sym = match e.kind() {
561            ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue => e.referenced_symbol(),
562            ExpressionKind::MemberAccess => {
563                let base = Self::operand(e, "a member access")?;
564                let is_this = base.kind() == ExpressionKind::NamedValue
565                    && base.referenced_symbol().is_some_and(|s| s.name() == "this");
566                if !is_this {
567                    return Err(BuildError::ForeignObjectAccess {
568                        what: format!(
569                            "the array `.{}` of another object",
570                            e.member_symbol()
571                                .map_or(String::new(), |m| m.name().to_string())
572                        ),
573                    });
574                }
575                e.member_symbol()
576            }
577            other => {
578                return Err(unsupported(
579                    format!("a `{other:?}` expression as an array operand"),
580                    "C14",
581                ));
582            }
583        };
584        let sym = sym.ok_or_else(|| BuildError::Missing {
585            what: "symbol of an array operand".into(),
586        })?;
587        let uid = sym.uid();
588        if let Some(&id) = self.array_ix.get(&uid) {
589            if self.arrays[id as usize].assoc {
590                return Err(Self::assoc_operand(&self.arrays[id as usize].name));
591            }
592            return Ok(id);
593        }
594        let vt = e.expr_type().ok_or_else(|| BuildError::Missing {
595            what: format!("type of the array `{}`", sym.name()),
596        })?;
597        let info = self.array_info(uid.clone(), sym.name().to_string(), vt, false)?;
598        let id = self.arrays.len() as u32;
599        self.arrays.push(info);
600        self.array_ix.insert(uid, id);
601        Ok(id)
602    }
603
604    /// `A.size` / `A.size()` (and a queue's `num`): `Some(array operand)` for
605    /// such a call.
606    pub(super) fn size_call_array<'d>(e: Expression<'d>) -> Option<Expression<'d>> {
607        let sub = e.system_subroutine()?;
608        if !matches!(
609            sub.known_name_id(),
610            KnownSystemName::Size | KnownSystemName::ArraySize | KnownSystemName::Num
611        ) {
612            return None;
613        }
614        let arr = *e.operands().first()?;
615        expr_shape(arr).map(|_| arr)
616    }
617
618    /// The array-level IR of `A.size`.
619    pub(super) fn array_size(&mut self, arr: Expression<'_>) -> Result<Expr, BuildError> {
620        self.arrays_allowed("an array size")?;
621        let (id, rows) = self.array_ref(arr)?;
622        Ok(Self::row_scoped(rows, Expr::Array(ArrayNode::Size(id))))
623    }
624
625    /// `A[sel]` where `base` (type of `A`) is an unpacked array.
626    pub(super) fn array_element(
627        &mut self,
628        e: Expression<'_>,
629        base: Expression<'_>,
630        ty: Ty,
631    ) -> Result<Expr, BuildError> {
632        let mut shape = base.expr_type().and_then(array_shape);
633        // `b[i][j]` of `rand int b[][4]`: the row `b[i]` is a fixed-size array
634        // stored inline, but it is one row of the outer array's template.
635        if shape == Some(ArrayShape::Fixed) {
636            let inner = strip_conversions(base);
637            if inner.kind() == ExpressionKind::ElementSelect
638                && inner
639                    .select_value()
640                    .and_then(|v| v.expr_type())
641                    .and_then(array_shape)
642                    == Some(ArrayShape::Collection)
643            {
644                shape = Some(ArrayShape::Collection);
645            }
646        }
647        let sel = e.selector().ok_or_else(|| BuildError::Missing {
648            what: "index of an element select".into(),
649        })?;
650        match shape {
651            Some(ArrayShape::Collection) => {
652                self.arrays_allowed("an array element")?;
653                let (id, rows) = self.array_ref(base)?;
654                let index = self.expr(sel)?;
655                let mut has_var = false;
656                index.for_each_var(&mut |_, _| has_var = true);
657                if has_var {
658                    return Err(bad_array(format!(
659                        "a random variable in the index of the array `{}`: an index is a \
660                         constant, a state variable, a loop variable or the size (IEEE \
661                         1800-2023 18.5.7.1)",
662                        self.arrays[id as usize].name
663                    )));
664                }
665                let elem_ty = self.arrays[id as usize].elem_ty;
666                if self.arrays[id as usize].nested {
667                    return Err(unsupported(
668                        format!(
669                            "a row `{}[..]` of an array of arrays as a value (name an element \
670                             `a[i][j]`)",
671                            self.arrays[id as usize].name
672                        ),
673                        "C14",
674                    ));
675                }
676                let el = Self::row_scoped(
677                    rows,
678                    Expr::Array(ArrayNode::Elem {
679                        array: id,
680                        index: Box::new(index),
681                        ty: elem_ty,
682                    }),
683                );
684                Ok(if elem_ty == ty {
685                    el
686                } else {
687                    Expr::Resize(Box::new(el), ty)
688                })
689            }
690            Some(ArrayShape::Assoc) => Err(bad_array(
691                "an element of an associative array: its indices are not randomizable and it \
692                 has no constraint form (IEEE 1800-2023 18.4)",
693            )),
694            Some(ArrayShape::Fixed) => self.fixed_element(e),
695            None => Err(BuildError::Missing {
696                what: "array type of an element select base".into(),
697            }),
698        }
699    }
700
701    /// An element of a fixed-size unpacked array that is not reached through
702    /// a property of this class (`aggregate_chain` resolves every property and
703    /// struct-member path before this is asked): a local, a hierarchical
704    /// reference, or an array read through an object handle.
705    fn fixed_element(&mut self, e: Expression<'_>) -> Result<Expr, BuildError> {
706        let what = e
707            .select_value()
708            .map(strip_conversions)
709            .and_then(|b| b.referenced_symbol().or_else(|| b.member_symbol()))
710            .map_or_else(String::new, |s| s.name().to_string());
711        Err(unsupported(
712            format!(
713                "an element of the unpacked array `{what}`, which is not a property (or a \
714                 member of a struct property) of this class"
715            ),
716            "C14",
717        ))
718    }
719
720    /// The names of a `foreach` constraint's loop variables, from its syntax
721    /// (the semantic node does not keep them).
722    fn foreach_vars(&self, c: Constraint<'_>) -> Result<Vec<String>, BuildError> {
723        for tree in self.trees {
724            if let Some(node) = c.syntax(tree) {
725                let l = LoopConstraintSyntax::cast(node).ok_or_else(|| BuildError::Missing {
726                    what: "`foreach` syntax for a Foreach constraint".into(),
727                })?;
728                return Ok(l
729                    .loop_list()
730                    .loop_variables()
731                    .iter()
732                    .map(|v| v.syntax().text().trim().to_string())
733                    .collect());
734            }
735        }
736        Err(BuildError::Missing {
737            what: "syntax node for a constraint (is its syntax tree passed to the builder?)".into(),
738        })
739    }
740
741    pub(super) fn fresh_loop(&mut self) -> LoopId {
742        let id = self.next_loop;
743        self.next_loop += 1;
744        id
745    }
746
747    /// Builds `body` with `scope` visible, returning its conjuncts.
748    fn in_scope(
749        &mut self,
750        scope: LoopScope,
751        body: Constraint<'_>,
752    ) -> Result<Vec<Expr>, BuildError> {
753        self.loops.push(scope);
754        let r = self.constraint(body);
755        self.loops.pop();
756        r
757    }
758
759    /// `foreach (A[i]) body` (IEEE 1800-2023 18.5.7.1).
760    pub(super) fn foreach(&mut self, c: Constraint<'_>) -> Result<Vec<Expr>, BuildError> {
761        let names = self.foreach_vars(c)?;
762        let kids = c.children();
763        let arr =
764            kids.iter()
765                .find_map(|n| n.as_expression())
766                .ok_or_else(|| BuildError::Missing {
767                    what: "array expression of a `foreach` constraint".into(),
768                })?;
769        let body =
770            kids.iter()
771                .find_map(|n| n.as_constraint())
772                .ok_or_else(|| BuildError::Missing {
773                    what: "body of a `foreach` constraint".into(),
774                })?;
775        let at = arr.expr_type().ok_or_else(|| BuildError::Missing {
776            what: "type of the `foreach` array".into(),
777        })?;
778        let shape = array_shape(at);
779        // LRM 12.7.3 / 18.5.7.1: `foreach (m[i, j])` loops each listed dimension, the first
780        // variable outermost; an empty slot (`m[i, , k]`) skips its dimension. Every looped
781        // dimension must have a static extent (a fixed unpacked or a packed one), so the
782        // body unrolls; a dynamic, queue or associative dimension stays refused.
783        let nested_fixed = shape == Some(ArrayShape::Fixed)
784            && at
785                .canonical()
786                .element_type()
787                .is_some_and(|elem| array_shape(elem) == Some(ArrayShape::Fixed));
788        let nested_dyn = shape == Some(ArrayShape::Collection)
789            && at.canonical().element_type().is_some_and(|elem| {
790                matches!(
791                    array_shape(elem),
792                    Some(ArrayShape::Collection | ArrayShape::Fixed)
793                )
794            });
795        if nested_dyn {
796            return self.foreach_nested(arr, &names, body);
797        }
798        // `rand int a[3][]`: the outer dimension is static (unrolled), each row
799        // is a dynamic array of its own.
800        let fixed_of_dyn = shape == Some(ArrayShape::Fixed)
801            && at
802                .canonical()
803                .element_type()
804                .is_some_and(|elem| array_shape(elem) == Some(ArrayShape::Collection));
805        if fixed_of_dyn {
806            return self.foreach_fixed_outer(arr, at, &names, body);
807        }
808        if names.len() > 1 || nested_fixed {
809            let dims = self.static_dims(at, &names)?;
810            return self.unroll_nd(&dims, body);
811        }
812        let [name] = names.as_slice() else {
813            return Err(BuildError::Missing {
814                what: "a loop variable of a `foreach` constraint".into(),
815            });
816        };
817        if shape.is_some()
818            && at
819                .canonical()
820                .element_type()
821                .is_some_and(|elem| array_shape(elem).is_some())
822        {
823            return Err(unsupported("a `foreach` over an array of arrays", "C11"));
824        }
825        let int = Ty::signed(32);
826        match shape {
827            Some(ArrayShape::Collection) => {
828                self.arrays_allowed("a `foreach`")?;
829                let array = self.array_of(arr)?;
830                let var = self.fresh_loop();
831                // A `dist` is fine here: it is built as a template and instanced per
832                // element at expansion (`dist::Dist::subject`).
833                let (r0, b0, rb0) = (
834                    self.real_conds.len() + self.real_dists.len(),
835                    self.before.len(),
836                    self.real_before.len(),
837                );
838                let parts = self.in_scope(
839                    LoopScope {
840                        name: name.clone(),
841                        value: LoopBind::Runtime(var, int),
842                        index: None,
843                    },
844                    body,
845                )?;
846                if self.real_conds.len() + self.real_dists.len() != r0
847                    || self.before.len() != b0
848                    || self.real_before.len() != rb0
849                {
850                    return Err(unsupported(
851                        "a real `dist` or condition, or a `solve ... before` constraint, inside a \
852                         `foreach` over a dynamic array or queue (it would have to hold once per \
853                         element)",
854                        "C11",
855                    ));
856                }
857                Ok(vec![Expr::Array(ArrayNode::Foreach {
858                    array,
859                    var,
860                    body: Box::new(and_all(parts)),
861                })])
862            }
863            Some(ArrayShape::Assoc) => Err(bad_array(
864                "a `foreach` over an associative array: its indices are not randomizable and \
865                 it has no constraint form (IEEE 1800-2023 18.4)",
866            )),
867            Some(ArrayShape::Fixed) => {
868                let r = at.canonical().fixed_range();
869                self.unroll(name, i64::from(r.left), i64::from(r.right), body)
870            }
871            None => {
872                // A packed vector or packed array: its extent is static.
873                let width = u32::try_from(at.bit_width()).map_err(|_| BuildError::Missing {
874                    what: "representable width for a `foreach` array".into(),
875                })?;
876                let g = Geom::of(at, width, "the `foreach` array")?;
877                self.unroll(name, g.left, g.right, body)
878            }
879        }
880    }
881
882    /// `foreach (a[i])` / `foreach (a[i, j])` over a fixed-size array of
883    /// dynamic arrays or queues (`rand int a[3][]`): `i` is unrolled over the
884    /// static dimension, `j` loops row `a[i]` at run time.
885    fn foreach_fixed_outer(
886        &mut self,
887        arr: Expression<'_>,
888        at: Type<'_>,
889        names: &[String],
890        body: Constraint<'_>,
891    ) -> Result<Vec<Expr>, BuildError> {
892        if names.is_empty() {
893            return Err(unsupported(
894                "a `foreach` with no loop variable over a fixed-size array of dynamic arrays",
895                "C11",
896            ));
897        }
898        let chain = aggregate_chain(arr, &self.forced_roots).ok_or_else(|| {
899            unsupported(
900                "a `foreach` over a fixed-size array of dynamic arrays that is not a property \
901                 of this class",
902                "C11",
903            )
904        })?;
905        let r = at.canonical().fixed_range();
906        let (left, right) = (i64::from(r.left), i64::from(r.right));
907        let idxs: Vec<i64> = if left >= right {
908            (right..=left).rev().collect()
909        } else {
910            (left..=right).collect()
911        };
912        let int = Ty::signed(32);
913        let mut out = Vec::new();
914        for k in idxs {
915            let outer_scope = (!names[0].is_empty()).then(|| LoopScope {
916                name: names[0].clone(),
917                value: LoopBind::Const(k),
918                index: None,
919            });
920            let depth = self.loops.len();
921            self.loops.extend(outer_scope);
922            let parts = self.fixed_outer_row(&chain, k, &names[1..], int, body);
923            self.loops.truncate(depth);
924            out.extend(parts?);
925        }
926        Ok(out)
927    }
928
929    /// The body of a `foreach` over a fixed outer dimension at index `k`: the
930    /// body itself for a one-variable loop, else a run-time loop over row `k`.
931    fn fixed_outer_row(
932        &mut self,
933        chain: &super::aggregate::Chain<'_>,
934        k: i64,
935        rest: &[String],
936        int: Ty,
937        body: Constraint<'_>,
938    ) -> Result<Vec<Expr>, BuildError> {
939        if rest.is_empty() {
940            return self.constraint(body);
941        }
942        self.arrays_allowed("a `foreach`")?;
943        let mut row_chain = chain.clone();
944        row_chain.steps.push(RawStep::Const(k));
945        let row = self.array_from_chain(row_chain)?;
946        if self.arrays[row as usize].nested {
947            // `a[k]` is itself an array of arrays (`rand int a[2][][]`): its own
948            // dimensions loop as a `foreach` over that row (IEEE 1800-2023 18.5.7.1).
949            return self.foreach_rows(row, rest, body);
950        }
951        if rest.len() > 1 {
952            return Err(unsupported(
953                format!(
954                    "a `foreach` with {} loop variables over a fixed-size array of dynamic \
955                     arrays (at most two dimensions)",
956                    rest.len() + 1
957                ),
958                "C11",
959            ));
960        }
961        let inner = &rest[0];
962        let vj = self.fresh_loop();
963        let (r0, b0, rb0) = (
964            self.real_conds.len() + self.real_dists.len(),
965            self.before.len(),
966            self.real_before.len(),
967        );
968        let depth = self.loops.len();
969        if !inner.is_empty() {
970            self.loops.push(LoopScope {
971                name: inner.clone(),
972                value: LoopBind::Runtime(vj, int),
973                index: None,
974            });
975        }
976        let parts = self.constraint(body);
977        self.loops.truncate(depth);
978        let parts = parts?;
979        if self.real_conds.len() + self.real_dists.len() != r0
980            || self.before.len() != b0
981            || self.real_before.len() != rb0
982        {
983            return Err(unsupported(
984                "a real `dist` or condition, or a `solve ... before` constraint, inside a \
985                 `foreach` over a dynamic array row (it would have to hold once per element)",
986                "C11",
987            ));
988        }
989        Ok(vec![Expr::Array(ArrayNode::Foreach {
990            array: row,
991            var: vj,
992            body: Box::new(and_all(parts)),
993        })])
994    }
995
996    /// `foreach (a[i])` / `foreach (a[i, j])` over an array of dynamic arrays
997    /// or queues (IEEE 1800-2023 18.5.7.1, 12.7.3): `i` loops the outer array,
998    /// `j` the row `a[i]` ([`ArrayNode::RowScope`]). An omitted variable
999    /// (`a[, j]`) still loops its dimension.
1000    fn foreach_nested(
1001        &mut self,
1002        arr: Expression<'_>,
1003        names: &[String],
1004        body: Constraint<'_>,
1005    ) -> Result<Vec<Expr>, BuildError> {
1006        self.arrays_allowed("a `foreach`")?;
1007        let (outer, rows) = self.array_ref(arr)?;
1008        if !rows.is_empty() || !self.arrays[outer as usize].nested {
1009            return Err(unsupported(
1010                "a `foreach` over an array whose elements are not dynamic arrays",
1011                "C11",
1012            ));
1013        }
1014        self.foreach_rows(outer, names, body)
1015    }
1016
1017    /// The loops of [`Self::foreach_nested`] over the array of arrays `outer`: a
1018    /// property, or a row `a[k]` of a fixed outer dimension.
1019    fn foreach_rows(
1020        &mut self,
1021        outer: ArrayId,
1022        names: &[String],
1023        body: Constraint<'_>,
1024    ) -> Result<Vec<Expr>, BuildError> {
1025        let dims = self.arrays[outer as usize].row_depth as usize + 1;
1026        if names.is_empty() || names.len() > dims {
1027            return Err(unsupported(
1028                format!(
1029                    "a `foreach` with {} loop variables over an array of dynamic arrays with {} \
1030                     dimensions",
1031                    names.len(),
1032                    dims
1033                ),
1034                "C11",
1035            ));
1036        }
1037        let int = Ty::signed(32);
1038        // `loops[k]` loops dimension `k`; `arrays[k]` is the array it loops (the
1039        // array itself, then the row template of each level).
1040        let loops: Vec<LoopId> = names.iter().map(|_| self.fresh_loop()).collect();
1041        let mut arrays = vec![outer];
1042        for _ in 1..names.len() {
1043            let t = self.row_template(*arrays.last().expect("one array"));
1044            arrays.push(t);
1045        }
1046        let scopes: Vec<LoopScope> = names
1047            .iter()
1048            .zip(&loops)
1049            .filter(|(n, _)| !n.is_empty())
1050            .map(|(n, &v)| LoopScope {
1051                name: n.clone(),
1052                value: LoopBind::Runtime(v, int),
1053                index: None,
1054            })
1055            .collect();
1056        let (r0, b0, rb0) = (
1057            self.real_conds.len() + self.real_dists.len(),
1058            self.before.len(),
1059            self.real_before.len(),
1060        );
1061        let depth = self.loops.len();
1062        self.loops.extend(scopes);
1063        let parts = self.constraint(body);
1064        self.loops.truncate(depth);
1065        let parts = parts?;
1066        if self.real_conds.len() + self.real_dists.len() != r0
1067            || self.before.len() != b0
1068            || self.real_before.len() != rb0
1069        {
1070            return Err(unsupported(
1071                "a real `dist` or condition, or a `solve ... before` constraint, inside a \
1072                 `foreach` over an array of dynamic arrays (it would have to hold once per \
1073                 element)",
1074                "C11",
1075            ));
1076        }
1077        // Innermost dimension first: each deeper one loops the row template of
1078        // the dimension above, bound to that dimension's loop variable.
1079        let mut inner = and_all(parts);
1080        for k in (0..names.len()).rev() {
1081            inner = Expr::Array(ArrayNode::Foreach {
1082                array: arrays[k],
1083                var: loops[k],
1084                body: Box::new(inner),
1085            });
1086            if k > 0 {
1087                inner = Expr::Array(ArrayNode::RowScope {
1088                    template: arrays[k],
1089                    row: Box::new(Expr::Array(ArrayNode::Loop(loops[k - 1], int))),
1090                    body: Box::new(inner),
1091                });
1092            }
1093        }
1094        Ok(vec![inner])
1095    }
1096
1097    /// The `(name, left, right)` of each dimension a multi-variable `foreach` loops over
1098    /// (`None` extent for an omitted loop variable), walking `names.len()` dimensions of `ty`.
1099    fn static_dims(
1100        &self,
1101        ty: sv_lang::Type<'_>,
1102        names: &[String],
1103    ) -> Result<Vec<(String, i64, i64)>, BuildError> {
1104        let mut dims = Vec::new();
1105        let mut cur = ty;
1106        for (k, name) in names.iter().enumerate() {
1107            let c = cur.canonical();
1108            let (left, right) = match array_shape(cur) {
1109                Some(ArrayShape::Fixed) => {
1110                    let r = c.fixed_range();
1111                    (i64::from(r.left), i64::from(r.right))
1112                }
1113                None if c.is_integral() => {
1114                    let width = u32::try_from(c.bit_width()).map_err(|_| BuildError::Missing {
1115                        what: "representable width for a `foreach` array".into(),
1116                    })?;
1117                    let g = Geom::of(cur, width, "the `foreach` array")?;
1118                    (g.left, g.right)
1119                }
1120                _ => {
1121                    return Err(unsupported(
1122                        format!(
1123                            "a multi-dimensional `foreach`: dimension {} is a dynamic array,                              queue or associative array (only fixed-size dimensions unroll)",
1124                            k + 1
1125                        ),
1126                        "C11",
1127                    ));
1128                }
1129            };
1130            dims.push((name.clone(), left, right));
1131            if k + 1 < names.len() {
1132                cur = c.element_type().ok_or_else(|| {
1133                    unsupported(
1134                        format!(
1135                            "a `foreach` with {} loop variables over an array with fewer \
1136                             dimensions",
1137                            names.len()
1138                        ),
1139                        "C11",
1140                    )
1141                })?;
1142            }
1143        }
1144        Ok(dims)
1145    }
1146
1147    /// The body of a multi-variable `foreach`, once per index tuple (first variable outermost,
1148    /// each from its left bound toward its right; LRM 12.7.3).
1149    fn unroll_nd(
1150        &mut self,
1151        dims: &[(String, i64, i64)],
1152        body: Constraint<'_>,
1153    ) -> Result<Vec<Expr>, BuildError> {
1154        let Some((name, left, right)) = dims.first() else {
1155            return self.constraint(body);
1156        };
1157        if name.is_empty() {
1158            return self.unroll_nd(&dims[1..], body);
1159        }
1160        let idxs: Vec<i64> = if left >= right {
1161            (*right..=*left).rev().collect()
1162        } else {
1163            (*left..=*right).collect()
1164        };
1165        let mut out = Vec::new();
1166        for i in idxs {
1167            self.loops.push(LoopScope {
1168                name: name.clone(),
1169                value: LoopBind::Const(i),
1170                index: None,
1171            });
1172            let r = self.unroll_nd(&dims[1..], body);
1173            self.loops.pop();
1174            out.extend(r?);
1175        }
1176        Ok(out)
1177    }
1178
1179    /// The body of a `foreach` over a statically known extent, once per index
1180    /// from the left bound toward the right (LRM 12.7.3).
1181    fn unroll(
1182        &mut self,
1183        name: &str,
1184        left: i64,
1185        right: i64,
1186        body: Constraint<'_>,
1187    ) -> Result<Vec<Expr>, BuildError> {
1188        let idxs: Vec<i64> = if left >= right {
1189            (right..=left).rev().collect()
1190        } else {
1191            (left..=right).collect()
1192        };
1193        let mut out = Vec::new();
1194        for i in idxs {
1195            out.extend(self.in_scope(
1196                LoopScope {
1197                    name: name.to_string(),
1198                    value: LoopBind::Const(i),
1199                    index: None,
1200                },
1201                body,
1202            )?);
1203        }
1204        Ok(out)
1205    }
1206
1207    /// Whether the constraint being built is inside a `foreach` over a
1208    /// dynamic array or queue (its body holds once per run-time element); a
1209    /// `foreach` over a static extent is unrolled and is not.
1210    pub(super) fn in_runtime_loop(&self) -> bool {
1211        self.loops
1212            .iter()
1213            .any(|s| matches!(s.value, LoopBind::Runtime(..)))
1214    }
1215
1216    fn scope_of(&self, name: &str) -> Option<&LoopScope> {
1217        self.loops.iter().rev().find(|s| s.name == name)
1218    }
1219
1220    fn bind_expr(&self, b: LoopBind, ty: Ty) -> Result<Expr, BuildError> {
1221        match b {
1222            LoopBind::Runtime(id, lt) => {
1223                let l = Expr::Array(ArrayNode::Loop(id, lt));
1224                Ok(if lt == ty {
1225                    l
1226                } else {
1227                    Expr::Resize(Box::new(l), ty)
1228                })
1229            }
1230            LoopBind::Const(i) => Ok(iconst(ty, i)),
1231            LoopBind::Elem(x) => Ok(if x.ty() == ty {
1232                x
1233            } else {
1234                Expr::Resize(Box::new(x), ty)
1235            }),
1236        }
1237    }
1238
1239    /// A reference to a `foreach` loop variable or a reduction's `item`.
1240    pub(super) fn loop_ref(&mut self, sym: Symbol<'_>, ty: Ty) -> Result<Expr, BuildError> {
1241        debug_assert_eq!(sym.kind(), SymbolKind::Iterator);
1242        let name = sym.name();
1243        let b = self
1244            .scope_of(name)
1245            .ok_or_else(|| BuildError::Missing {
1246                what: format!("an enclosing `foreach` or array method binding `{name}`"),
1247            })?
1248            .value
1249            .clone();
1250        if matches!(b, LoopBind::Runtime(..)) {
1251            self.arrays_allowed("a loop variable")?;
1252        }
1253        self.bind_expr(b, ty)
1254    }
1255
1256    /// `item.index` inside a reduction's `with` clause.
1257    pub(super) fn iterator_index(&mut self, e: Expression<'_>, ty: Ty) -> Result<Expr, BuildError> {
1258        let this = *e.operands().first().ok_or_else(|| BuildError::Missing {
1259            what: "iterator of `index`".into(),
1260        })?;
1261        let name = this
1262            .referenced_symbol()
1263            .map(|s| s.name().to_string())
1264            .ok_or_else(|| BuildError::Missing {
1265                what: "symbol of the iterator of `index`".into(),
1266            })?;
1267        let b = self
1268            .scope_of(&name)
1269            .and_then(|s| s.index.clone())
1270            .ok_or_else(|| BuildError::Missing {
1271                what: format!("an array method iterator `{name}` for `.index`"),
1272            })?;
1273        self.arrays_allowed("an iterator index")?;
1274        self.bind_expr(b, ty)
1275    }
1276
1277    /// `A.sum()`, `.product()`, `.and()`, `.or()`, `.xor()` with an optional
1278    /// `with (expr)` (IEEE 1800-2023 18.5.7.2, 7.12.3).
1279    pub(super) fn array_reduction(
1280        &mut self,
1281        e: Expression<'_>,
1282        op: ReduceOp,
1283        ty: Ty,
1284    ) -> Result<Expr, BuildError> {
1285        let arr = e
1286            .operands()
1287            .into_iter()
1288            .find(|o| expr_shape(*o).is_some())
1289            .ok_or_else(|| BuildError::Missing {
1290                what: "array operand of a reduction method".into(),
1291            })?;
1292        if expr_shape(arr) == Some(ArrayShape::Fixed) {
1293            return self.fixed_reduction(e, arr, op, ty);
1294        }
1295        self.arrays_allowed("an array reduction")?;
1296        let (array, rows) = self.array_ref(arr)?;
1297        self.reject_nested(array, "a reduction method")?;
1298        let elem_ty = self.arrays[array as usize].elem_ty;
1299        let var = self.fresh_loop();
1300        let index_var = self.fresh_loop();
1301        let body = match e.iterator_expr() {
1302            Some(w) => {
1303                let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
1304                    what: "iterator variable of a reduction's `with` clause".into(),
1305                })?;
1306                self.loops.push(LoopScope {
1307                    name: iter.name().to_string(),
1308                    value: LoopBind::Runtime(var, elem_ty),
1309                    index: Some(LoopBind::Runtime(index_var, Ty::signed(32))),
1310                });
1311                let b = self.expr(w);
1312                self.loops.pop();
1313                b?
1314            }
1315            None => Expr::Array(ArrayNode::Loop(var, elem_ty)),
1316        };
1317        let body = if body.ty() == ty {
1318            body
1319        } else {
1320            coerce(body, ty)
1321        };
1322        Ok(Self::row_scoped(
1323            rows,
1324            Expr::Array(ArrayNode::Reduce {
1325                op,
1326                array,
1327                var,
1328                index_var,
1329                body: Box::new(body),
1330            }),
1331        ))
1332    }
1333
1334    /// A reduction method over a fixed-size unpacked array (a property of
1335    /// this class, or a struct member of one): its extent is known now, so it
1336    /// unrolls into the fold of its elements, each bound to the iterator
1337    /// (IEEE 1800-2023 7.12.3, 18.5.7.2).
1338    fn fixed_reduction(
1339        &mut self,
1340        e: Expression<'_>,
1341        arr: Expression<'_>,
1342        op: ReduceOp,
1343        ty: Ty,
1344    ) -> Result<Expr, BuildError> {
1345        let at = arr
1346            .expr_type()
1347            .ok_or_else(|| BuildError::Missing {
1348                what: "type of the reduced array".into(),
1349            })?
1350            .canonical();
1351        let r = at.fixed_unpacked_array_range();
1352        let (lo, hi) = (
1353            i64::from(r.left).min(i64::from(r.right)),
1354            i64::from(r.left).max(i64::from(r.right)),
1355        );
1356        let et = at.element_type().ok_or_else(|| BuildError::Missing {
1357            what: "element type of the reduced array".into(),
1358        })?;
1359        let elem_ty = ty_of(Some(et), "an element of the reduced array")?;
1360        let mut acc: Option<Expr> = None;
1361        for i in lo..=hi {
1362            let item = self.aggregate_element(arr, i, elem_ty)?;
1363            let body = match e.iterator_expr() {
1364                Some(w) => {
1365                    let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
1366                        what: "iterator variable of a reduction's `with` clause".into(),
1367                    })?;
1368                    self.loops.push(LoopScope {
1369                        name: iter.name().to_string(),
1370                        value: LoopBind::Elem(item),
1371                        index: Some(LoopBind::Const(i)),
1372                    });
1373                    let b = self.expr(w);
1374                    self.loops.pop();
1375                    b?
1376                }
1377                None => item,
1378            };
1379            let body = if body.ty() == ty {
1380                body
1381            } else {
1382                coerce(body, ty)
1383            };
1384            acc = Some(match acc {
1385                None => body,
1386                Some(a) => {
1387                    let bop = match op {
1388                        ReduceOp::Sum => BinOp::Add,
1389                        ReduceOp::Product => BinOp::Mul,
1390                        ReduceOp::And => BinOp::And,
1391                        ReduceOp::Or => BinOp::Or,
1392                        ReduceOp::Xor => BinOp::Xor,
1393                    };
1394                    bin(bop, a, body, ty)
1395                }
1396            });
1397        }
1398        acc.ok_or_else(|| BuildError::Missing {
1399            what: "elements of the reduced array".into(),
1400        })
1401    }
1402
1403    /// `left inside {A}` for an array `A` in the set: true when `left` equals
1404    /// some element of `A` (IEEE 1800-2023 11.4.13, 18.5.3: the array is
1405    /// traversed and each element is a member of the set). A fixed-size array
1406    /// unrolls into a disjunction of equalities; a dynamic array or queue is
1407    /// an `or`-reduction over its run-time elements, so an empty one admits
1408    /// nothing.
1409    pub(super) fn inside_array(
1410        &mut self,
1411        left: &Expr,
1412        arr: Expression<'_>,
1413    ) -> Result<Expr, BuildError> {
1414        let eq = |left: &Expr, x: Expr| {
1415            let t = common_ty(left.ty(), x.ty());
1416            bin(BinOp::Eq, coerce(left.clone(), t), coerce(x, t), Ty::BIT)
1417        };
1418        let at = arr.expr_type().ok_or_else(|| BuildError::Missing {
1419            what: "type of an `inside` array".into(),
1420        })?;
1421        match expr_shape(arr) {
1422            Some(ArrayShape::Fixed) => {
1423                let c = at.canonical();
1424                let r = c.fixed_unpacked_array_range();
1425                let (lo, hi) = (
1426                    i64::from(r.left).min(i64::from(r.right)),
1427                    i64::from(r.left).max(i64::from(r.right)),
1428                );
1429                let et = c.element_type().ok_or_else(|| BuildError::Missing {
1430                    what: "element type of an `inside` array".into(),
1431                })?;
1432                let elem_ty = ty_of(Some(et), "an element of an `inside` array")?;
1433                let mut acc: Option<Expr> = None;
1434                for i in lo..=hi {
1435                    let item = self.aggregate_element(arr, i, elem_ty)?;
1436                    let term = eq(left, item);
1437                    acc = Some(match acc {
1438                        None => term,
1439                        Some(prev) => bin(BinOp::LogOr, prev, term, Ty::BIT),
1440                    });
1441                }
1442                acc.ok_or_else(|| BuildError::Missing {
1443                    what: "elements of an `inside` array".into(),
1444                })
1445            }
1446            Some(ArrayShape::Collection) => {
1447                self.arrays_allowed("an `inside` over an array")?;
1448                let (array, rows) = self.array_ref(arr)?;
1449                self.reject_nested(array, "an `inside` set")?;
1450                let elem_ty = self.arrays[array as usize].elem_ty;
1451                let var = self.fresh_loop();
1452                let index_var = self.fresh_loop();
1453                let body = eq(left, Expr::Array(ArrayNode::Loop(var, elem_ty)));
1454                Ok(Self::row_scoped(
1455                    rows,
1456                    Expr::Array(ArrayNode::Reduce {
1457                        op: ReduceOp::Or,
1458                        array,
1459                        var,
1460                        index_var,
1461                        body: Box::new(body),
1462                    }),
1463                ))
1464            }
1465            Some(ArrayShape::Assoc) | None => Err(bad_array(
1466                "an associative array in an `inside` set: its elements are not constraint \
1467                 operands (IEEE 1800-2023 18.4)",
1468            )),
1469        }
1470    }
1471
1472    /// The array-level value of a call to a system method on an array, or
1473    /// `None` for any other call.
1474    pub(super) fn array_call(
1475        &mut self,
1476        e: Expression<'_>,
1477        ty: Ty,
1478    ) -> Option<Result<Expr, BuildError>> {
1479        let sub = e.system_subroutine()?;
1480        let op = match sub.known_name_id() {
1481            KnownSystemName::Size | KnownSystemName::ArraySize | KnownSystemName::Num => {
1482                let arr = Self::size_call_array(e)?;
1483                return Some(self.array_size(arr));
1484            }
1485            KnownSystemName::Index => return Some(self.iterator_index(e, ty)),
1486            KnownSystemName::Sum => ReduceOp::Sum,
1487            KnownSystemName::Product => ReduceOp::Product,
1488            KnownSystemName::And => ReduceOp::And,
1489            KnownSystemName::Or => ReduceOp::Or,
1490            KnownSystemName::XOr => ReduceOp::Xor,
1491            _ => return None,
1492        };
1493        Some(self.array_reduction(e, op, ty))
1494    }
1495}