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() || names.len() > 2 {
893            return Err(unsupported(
894                format!(
895                    "a `foreach` with {} loop variables over a fixed-size array of dynamic \
896                     arrays (at most two dimensions)",
897                    names.len()
898                ),
899                "C11",
900            ));
901        }
902        if at
903            .canonical()
904            .element_type()
905            .and_then(|e| e.canonical().element_type())
906            .is_some_and(|r| array_shape(r).is_some())
907        {
908            return Err(unsupported(
909                "a `foreach` over an array nested more than two deep",
910                "C11",
911            ));
912        }
913        let chain = aggregate_chain(arr, &self.forced_roots).ok_or_else(|| {
914            unsupported(
915                "a `foreach` over a fixed-size array of dynamic arrays that is not a property \
916                 of this class",
917                "C11",
918            )
919        })?;
920        let r = at.canonical().fixed_range();
921        let (left, right) = (i64::from(r.left), i64::from(r.right));
922        let idxs: Vec<i64> = if left >= right {
923            (right..=left).rev().collect()
924        } else {
925            (left..=right).collect()
926        };
927        let int = Ty::signed(32);
928        let mut out = Vec::new();
929        for k in idxs {
930            let outer_scope = (!names[0].is_empty()).then(|| LoopScope {
931                name: names[0].clone(),
932                value: LoopBind::Const(k),
933                index: None,
934            });
935            let depth = self.loops.len();
936            self.loops.extend(outer_scope);
937            let parts = self.fixed_outer_row(&chain, k, names.get(1), int, body);
938            self.loops.truncate(depth);
939            out.extend(parts?);
940        }
941        Ok(out)
942    }
943
944    /// The body of a `foreach` over a fixed outer dimension at index `k`: the
945    /// body itself for a one-variable loop, else a run-time loop over row `k`.
946    fn fixed_outer_row(
947        &mut self,
948        chain: &super::aggregate::Chain<'_>,
949        k: i64,
950        inner: Option<&String>,
951        int: Ty,
952        body: Constraint<'_>,
953    ) -> Result<Vec<Expr>, BuildError> {
954        let Some(inner) = inner else {
955            return self.constraint(body);
956        };
957        self.arrays_allowed("a `foreach`")?;
958        let mut row_chain = chain.clone();
959        row_chain.steps.push(RawStep::Const(k));
960        let row = self.array_from_chain(row_chain)?;
961        let vj = self.fresh_loop();
962        let (r0, b0, rb0) = (
963            self.real_conds.len() + self.real_dists.len(),
964            self.before.len(),
965            self.real_before.len(),
966        );
967        let depth = self.loops.len();
968        if !inner.is_empty() {
969            self.loops.push(LoopScope {
970                name: inner.clone(),
971                value: LoopBind::Runtime(vj, int),
972                index: None,
973            });
974        }
975        let parts = self.constraint(body);
976        self.loops.truncate(depth);
977        let parts = parts?;
978        if self.real_conds.len() + self.real_dists.len() != r0
979            || self.before.len() != b0
980            || self.real_before.len() != rb0
981        {
982            return Err(unsupported(
983                "a real `dist` or condition, or a `solve ... before` constraint, inside a \
984                 `foreach` over a dynamic array row (it would have to hold once per element)",
985                "C11",
986            ));
987        }
988        Ok(vec![Expr::Array(ArrayNode::Foreach {
989            array: row,
990            var: vj,
991            body: Box::new(and_all(parts)),
992        })])
993    }
994
995    /// `foreach (a[i])` / `foreach (a[i, j])` over an array of dynamic arrays
996    /// or queues (IEEE 1800-2023 18.5.7.1, 12.7.3): `i` loops the outer array,
997    /// `j` the row `a[i]` ([`ArrayNode::RowScope`]). An omitted variable
998    /// (`a[, j]`) still loops its dimension.
999    fn foreach_nested(
1000        &mut self,
1001        arr: Expression<'_>,
1002        names: &[String],
1003        body: Constraint<'_>,
1004    ) -> Result<Vec<Expr>, BuildError> {
1005        self.arrays_allowed("a `foreach`")?;
1006        let (outer, rows) = self.array_ref(arr)?;
1007        if !rows.is_empty() || !self.arrays[outer as usize].nested {
1008            return Err(unsupported(
1009                "a `foreach` over an array whose elements are not dynamic arrays",
1010                "C11",
1011            ));
1012        }
1013        let dims = self.arrays[outer as usize].row_depth as usize + 1;
1014        if names.is_empty() || names.len() > dims {
1015            return Err(unsupported(
1016                format!(
1017                    "a `foreach` with {} loop variables over an array of dynamic arrays with {} \
1018                     dimensions",
1019                    names.len(),
1020                    dims
1021                ),
1022                "C11",
1023            ));
1024        }
1025        let int = Ty::signed(32);
1026        // `loops[k]` loops dimension `k`; `arrays[k]` is the array it loops (the
1027        // array itself, then the row template of each level).
1028        let loops: Vec<LoopId> = names.iter().map(|_| self.fresh_loop()).collect();
1029        let mut arrays = vec![outer];
1030        for _ in 1..names.len() {
1031            let t = self.row_template(*arrays.last().expect("one array"));
1032            arrays.push(t);
1033        }
1034        let scopes: Vec<LoopScope> = names
1035            .iter()
1036            .zip(&loops)
1037            .filter(|(n, _)| !n.is_empty())
1038            .map(|(n, &v)| LoopScope {
1039                name: n.clone(),
1040                value: LoopBind::Runtime(v, int),
1041                index: None,
1042            })
1043            .collect();
1044        let (r0, b0, rb0) = (
1045            self.real_conds.len() + self.real_dists.len(),
1046            self.before.len(),
1047            self.real_before.len(),
1048        );
1049        let depth = self.loops.len();
1050        self.loops.extend(scopes);
1051        let parts = self.constraint(body);
1052        self.loops.truncate(depth);
1053        let parts = parts?;
1054        if self.real_conds.len() + self.real_dists.len() != r0
1055            || self.before.len() != b0
1056            || self.real_before.len() != rb0
1057        {
1058            return Err(unsupported(
1059                "a real `dist` or condition, or a `solve ... before` constraint, inside a \
1060                 `foreach` over an array of dynamic arrays (it would have to hold once per \
1061                 element)",
1062                "C11",
1063            ));
1064        }
1065        // Innermost dimension first: each deeper one loops the row template of
1066        // the dimension above, bound to that dimension's loop variable.
1067        let mut inner = and_all(parts);
1068        for k in (0..names.len()).rev() {
1069            inner = Expr::Array(ArrayNode::Foreach {
1070                array: arrays[k],
1071                var: loops[k],
1072                body: Box::new(inner),
1073            });
1074            if k > 0 {
1075                inner = Expr::Array(ArrayNode::RowScope {
1076                    template: arrays[k],
1077                    row: Box::new(Expr::Array(ArrayNode::Loop(loops[k - 1], int))),
1078                    body: Box::new(inner),
1079                });
1080            }
1081        }
1082        Ok(vec![inner])
1083    }
1084
1085    /// The `(name, left, right)` of each dimension a multi-variable `foreach` loops over
1086    /// (`None` extent for an omitted loop variable), walking `names.len()` dimensions of `ty`.
1087    fn static_dims(
1088        &self,
1089        ty: sv_lang::Type<'_>,
1090        names: &[String],
1091    ) -> Result<Vec<(String, i64, i64)>, BuildError> {
1092        let mut dims = Vec::new();
1093        let mut cur = ty;
1094        for (k, name) in names.iter().enumerate() {
1095            let c = cur.canonical();
1096            let (left, right) = match array_shape(cur) {
1097                Some(ArrayShape::Fixed) => {
1098                    let r = c.fixed_range();
1099                    (i64::from(r.left), i64::from(r.right))
1100                }
1101                None if c.is_integral() => {
1102                    let width = u32::try_from(c.bit_width()).map_err(|_| BuildError::Missing {
1103                        what: "representable width for a `foreach` array".into(),
1104                    })?;
1105                    let g = Geom::of(cur, width, "the `foreach` array")?;
1106                    (g.left, g.right)
1107                }
1108                _ => {
1109                    return Err(unsupported(
1110                        format!(
1111                            "a multi-dimensional `foreach`: dimension {} is a dynamic array,                              queue or associative array (only fixed-size dimensions unroll)",
1112                            k + 1
1113                        ),
1114                        "C11",
1115                    ));
1116                }
1117            };
1118            dims.push((name.clone(), left, right));
1119            if k + 1 < names.len() {
1120                cur = c.element_type().ok_or_else(|| {
1121                    unsupported(
1122                        format!(
1123                            "a `foreach` with {} loop variables over an array with fewer \
1124                             dimensions",
1125                            names.len()
1126                        ),
1127                        "C11",
1128                    )
1129                })?;
1130            }
1131        }
1132        Ok(dims)
1133    }
1134
1135    /// The body of a multi-variable `foreach`, once per index tuple (first variable outermost,
1136    /// each from its left bound toward its right; LRM 12.7.3).
1137    fn unroll_nd(
1138        &mut self,
1139        dims: &[(String, i64, i64)],
1140        body: Constraint<'_>,
1141    ) -> Result<Vec<Expr>, BuildError> {
1142        let Some((name, left, right)) = dims.first() else {
1143            return self.constraint(body);
1144        };
1145        if name.is_empty() {
1146            return self.unroll_nd(&dims[1..], body);
1147        }
1148        let idxs: Vec<i64> = if left >= right {
1149            (*right..=*left).rev().collect()
1150        } else {
1151            (*left..=*right).collect()
1152        };
1153        let mut out = Vec::new();
1154        for i in idxs {
1155            self.loops.push(LoopScope {
1156                name: name.clone(),
1157                value: LoopBind::Const(i),
1158                index: None,
1159            });
1160            let r = self.unroll_nd(&dims[1..], body);
1161            self.loops.pop();
1162            out.extend(r?);
1163        }
1164        Ok(out)
1165    }
1166
1167    /// The body of a `foreach` over a statically known extent, once per index
1168    /// from the left bound toward the right (LRM 12.7.3).
1169    fn unroll(
1170        &mut self,
1171        name: &str,
1172        left: i64,
1173        right: i64,
1174        body: Constraint<'_>,
1175    ) -> Result<Vec<Expr>, BuildError> {
1176        let idxs: Vec<i64> = if left >= right {
1177            (right..=left).rev().collect()
1178        } else {
1179            (left..=right).collect()
1180        };
1181        let mut out = Vec::new();
1182        for i in idxs {
1183            out.extend(self.in_scope(
1184                LoopScope {
1185                    name: name.to_string(),
1186                    value: LoopBind::Const(i),
1187                    index: None,
1188                },
1189                body,
1190            )?);
1191        }
1192        Ok(out)
1193    }
1194
1195    /// Whether the constraint being built is inside a `foreach` over a
1196    /// dynamic array or queue (its body holds once per run-time element); a
1197    /// `foreach` over a static extent is unrolled and is not.
1198    pub(super) fn in_runtime_loop(&self) -> bool {
1199        self.loops
1200            .iter()
1201            .any(|s| matches!(s.value, LoopBind::Runtime(..)))
1202    }
1203
1204    fn scope_of(&self, name: &str) -> Option<&LoopScope> {
1205        self.loops.iter().rev().find(|s| s.name == name)
1206    }
1207
1208    fn bind_expr(&self, b: LoopBind, ty: Ty) -> Result<Expr, BuildError> {
1209        match b {
1210            LoopBind::Runtime(id, lt) => {
1211                let l = Expr::Array(ArrayNode::Loop(id, lt));
1212                Ok(if lt == ty {
1213                    l
1214                } else {
1215                    Expr::Resize(Box::new(l), ty)
1216                })
1217            }
1218            LoopBind::Const(i) => Ok(iconst(ty, i)),
1219            LoopBind::Elem(x) => Ok(if x.ty() == ty {
1220                x
1221            } else {
1222                Expr::Resize(Box::new(x), ty)
1223            }),
1224        }
1225    }
1226
1227    /// A reference to a `foreach` loop variable or a reduction's `item`.
1228    pub(super) fn loop_ref(&mut self, sym: Symbol<'_>, ty: Ty) -> Result<Expr, BuildError> {
1229        debug_assert_eq!(sym.kind(), SymbolKind::Iterator);
1230        let name = sym.name();
1231        let b = self
1232            .scope_of(name)
1233            .ok_or_else(|| BuildError::Missing {
1234                what: format!("an enclosing `foreach` or array method binding `{name}`"),
1235            })?
1236            .value
1237            .clone();
1238        if matches!(b, LoopBind::Runtime(..)) {
1239            self.arrays_allowed("a loop variable")?;
1240        }
1241        self.bind_expr(b, ty)
1242    }
1243
1244    /// `item.index` inside a reduction's `with` clause.
1245    pub(super) fn iterator_index(&mut self, e: Expression<'_>, ty: Ty) -> Result<Expr, BuildError> {
1246        let this = *e.operands().first().ok_or_else(|| BuildError::Missing {
1247            what: "iterator of `index`".into(),
1248        })?;
1249        let name = this
1250            .referenced_symbol()
1251            .map(|s| s.name().to_string())
1252            .ok_or_else(|| BuildError::Missing {
1253                what: "symbol of the iterator of `index`".into(),
1254            })?;
1255        let b = self
1256            .scope_of(&name)
1257            .and_then(|s| s.index.clone())
1258            .ok_or_else(|| BuildError::Missing {
1259                what: format!("an array method iterator `{name}` for `.index`"),
1260            })?;
1261        self.arrays_allowed("an iterator index")?;
1262        self.bind_expr(b, ty)
1263    }
1264
1265    /// `A.sum()`, `.product()`, `.and()`, `.or()`, `.xor()` with an optional
1266    /// `with (expr)` (IEEE 1800-2023 18.5.7.2, 7.12.3).
1267    pub(super) fn array_reduction(
1268        &mut self,
1269        e: Expression<'_>,
1270        op: ReduceOp,
1271        ty: Ty,
1272    ) -> Result<Expr, BuildError> {
1273        let arr = e
1274            .operands()
1275            .into_iter()
1276            .find(|o| expr_shape(*o).is_some())
1277            .ok_or_else(|| BuildError::Missing {
1278                what: "array operand of a reduction method".into(),
1279            })?;
1280        if expr_shape(arr) == Some(ArrayShape::Fixed) {
1281            return self.fixed_reduction(e, arr, op, ty);
1282        }
1283        self.arrays_allowed("an array reduction")?;
1284        let (array, rows) = self.array_ref(arr)?;
1285        self.reject_nested(array, "a reduction method")?;
1286        let elem_ty = self.arrays[array as usize].elem_ty;
1287        let var = self.fresh_loop();
1288        let index_var = self.fresh_loop();
1289        let body = match e.iterator_expr() {
1290            Some(w) => {
1291                let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
1292                    what: "iterator variable of a reduction's `with` clause".into(),
1293                })?;
1294                self.loops.push(LoopScope {
1295                    name: iter.name().to_string(),
1296                    value: LoopBind::Runtime(var, elem_ty),
1297                    index: Some(LoopBind::Runtime(index_var, Ty::signed(32))),
1298                });
1299                let b = self.expr(w);
1300                self.loops.pop();
1301                b?
1302            }
1303            None => Expr::Array(ArrayNode::Loop(var, elem_ty)),
1304        };
1305        let body = if body.ty() == ty {
1306            body
1307        } else {
1308            coerce(body, ty)
1309        };
1310        Ok(Self::row_scoped(
1311            rows,
1312            Expr::Array(ArrayNode::Reduce {
1313                op,
1314                array,
1315                var,
1316                index_var,
1317                body: Box::new(body),
1318            }),
1319        ))
1320    }
1321
1322    /// A reduction method over a fixed-size unpacked array (a property of
1323    /// this class, or a struct member of one): its extent is known now, so it
1324    /// unrolls into the fold of its elements, each bound to the iterator
1325    /// (IEEE 1800-2023 7.12.3, 18.5.7.2).
1326    fn fixed_reduction(
1327        &mut self,
1328        e: Expression<'_>,
1329        arr: Expression<'_>,
1330        op: ReduceOp,
1331        ty: Ty,
1332    ) -> Result<Expr, BuildError> {
1333        let at = arr
1334            .expr_type()
1335            .ok_or_else(|| BuildError::Missing {
1336                what: "type of the reduced array".into(),
1337            })?
1338            .canonical();
1339        let r = at.fixed_unpacked_array_range();
1340        let (lo, hi) = (
1341            i64::from(r.left).min(i64::from(r.right)),
1342            i64::from(r.left).max(i64::from(r.right)),
1343        );
1344        let et = at.element_type().ok_or_else(|| BuildError::Missing {
1345            what: "element type of the reduced array".into(),
1346        })?;
1347        let elem_ty = ty_of(Some(et), "an element of the reduced array")?;
1348        let mut acc: Option<Expr> = None;
1349        for i in lo..=hi {
1350            let item = self.aggregate_element(arr, i, elem_ty)?;
1351            let body = match e.iterator_expr() {
1352                Some(w) => {
1353                    let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
1354                        what: "iterator variable of a reduction's `with` clause".into(),
1355                    })?;
1356                    self.loops.push(LoopScope {
1357                        name: iter.name().to_string(),
1358                        value: LoopBind::Elem(item),
1359                        index: Some(LoopBind::Const(i)),
1360                    });
1361                    let b = self.expr(w);
1362                    self.loops.pop();
1363                    b?
1364                }
1365                None => item,
1366            };
1367            let body = if body.ty() == ty {
1368                body
1369            } else {
1370                coerce(body, ty)
1371            };
1372            acc = Some(match acc {
1373                None => body,
1374                Some(a) => {
1375                    let bop = match op {
1376                        ReduceOp::Sum => BinOp::Add,
1377                        ReduceOp::Product => BinOp::Mul,
1378                        ReduceOp::And => BinOp::And,
1379                        ReduceOp::Or => BinOp::Or,
1380                        ReduceOp::Xor => BinOp::Xor,
1381                    };
1382                    bin(bop, a, body, ty)
1383                }
1384            });
1385        }
1386        acc.ok_or_else(|| BuildError::Missing {
1387            what: "elements of the reduced array".into(),
1388        })
1389    }
1390
1391    /// `left inside {A}` for an array `A` in the set: true when `left` equals
1392    /// some element of `A` (IEEE 1800-2023 11.4.13, 18.5.3: the array is
1393    /// traversed and each element is a member of the set). A fixed-size array
1394    /// unrolls into a disjunction of equalities; a dynamic array or queue is
1395    /// an `or`-reduction over its run-time elements, so an empty one admits
1396    /// nothing.
1397    pub(super) fn inside_array(
1398        &mut self,
1399        left: &Expr,
1400        arr: Expression<'_>,
1401    ) -> Result<Expr, BuildError> {
1402        let eq = |left: &Expr, x: Expr| {
1403            let t = common_ty(left.ty(), x.ty());
1404            bin(BinOp::Eq, coerce(left.clone(), t), coerce(x, t), Ty::BIT)
1405        };
1406        let at = arr.expr_type().ok_or_else(|| BuildError::Missing {
1407            what: "type of an `inside` array".into(),
1408        })?;
1409        match expr_shape(arr) {
1410            Some(ArrayShape::Fixed) => {
1411                let c = at.canonical();
1412                let r = c.fixed_unpacked_array_range();
1413                let (lo, hi) = (
1414                    i64::from(r.left).min(i64::from(r.right)),
1415                    i64::from(r.left).max(i64::from(r.right)),
1416                );
1417                let et = c.element_type().ok_or_else(|| BuildError::Missing {
1418                    what: "element type of an `inside` array".into(),
1419                })?;
1420                let elem_ty = ty_of(Some(et), "an element of an `inside` array")?;
1421                let mut acc: Option<Expr> = None;
1422                for i in lo..=hi {
1423                    let item = self.aggregate_element(arr, i, elem_ty)?;
1424                    let term = eq(left, item);
1425                    acc = Some(match acc {
1426                        None => term,
1427                        Some(prev) => bin(BinOp::LogOr, prev, term, Ty::BIT),
1428                    });
1429                }
1430                acc.ok_or_else(|| BuildError::Missing {
1431                    what: "elements of an `inside` array".into(),
1432                })
1433            }
1434            Some(ArrayShape::Collection) => {
1435                self.arrays_allowed("an `inside` over an array")?;
1436                let (array, rows) = self.array_ref(arr)?;
1437                self.reject_nested(array, "an `inside` set")?;
1438                let elem_ty = self.arrays[array as usize].elem_ty;
1439                let var = self.fresh_loop();
1440                let index_var = self.fresh_loop();
1441                let body = eq(left, Expr::Array(ArrayNode::Loop(var, elem_ty)));
1442                Ok(Self::row_scoped(
1443                    rows,
1444                    Expr::Array(ArrayNode::Reduce {
1445                        op: ReduceOp::Or,
1446                        array,
1447                        var,
1448                        index_var,
1449                        body: Box::new(body),
1450                    }),
1451                ))
1452            }
1453            Some(ArrayShape::Assoc) | None => Err(bad_array(
1454                "an associative array in an `inside` set: its elements are not constraint \
1455                 operands (IEEE 1800-2023 18.4)",
1456            )),
1457        }
1458    }
1459
1460    /// The array-level value of a call to a system method on an array, or
1461    /// `None` for any other call.
1462    pub(super) fn array_call(
1463        &mut self,
1464        e: Expression<'_>,
1465        ty: Ty,
1466    ) -> Option<Result<Expr, BuildError>> {
1467        let sub = e.system_subroutine()?;
1468        let op = match sub.known_name_id() {
1469            KnownSystemName::Size | KnownSystemName::ArraySize | KnownSystemName::Num => {
1470                let arr = Self::size_call_array(e)?;
1471                return Some(self.array_size(arr));
1472            }
1473            KnownSystemName::Index => return Some(self.iterator_index(e, ty)),
1474            KnownSystemName::Sum => ReduceOp::Sum,
1475            KnownSystemName::Product => ReduceOp::Product,
1476            KnownSystemName::And => ReduceOp::And,
1477            KnownSystemName::Or => ReduceOp::Or,
1478            KnownSystemName::XOr => ReduceOp::Xor,
1479            _ => return None,
1480        };
1481        Some(self.array_reduction(e, op, ty))
1482    }
1483}