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