Skip to main content

sv_sim_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}
69
70/// What a loop name stands for while its body is built.
71#[derive(Clone, Debug)]
72pub(super) enum LoopBind {
73    /// A run-time loop variable ([`ArrayNode::Loop`]).
74    Runtime(LoopId, Ty),
75    /// A constant index of an unrolled loop.
76    Const(i64),
77    /// The element an unrolled reduction over a fixed-size array is at: the
78    /// expression of that element.
79    Elem(Expr),
80}
81
82/// One name bound by a `foreach` or a reduction's iterator.
83#[derive(Clone, Debug)]
84pub(super) struct LoopScope {
85    name: String,
86    /// What the name itself reads: the index of a `foreach` variable, the
87    /// element of a reduction's `item`.
88    value: LoopBind,
89    /// What `name.index` reads (a reduction iterator only).
90    index: Option<LoopBind>,
91}
92
93/// How an array-typed expression is stored.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub(super) enum ArrayShape {
96    /// A dynamic array or queue: its size is a run-time value.
97    Collection,
98    /// An associative array.
99    Assoc,
100    /// A fixed-size unpacked array.
101    Fixed,
102}
103
104pub(super) fn array_shape(t: Type<'_>) -> Option<ArrayShape> {
105    let c = t.canonical();
106    if c.is_associative_array() {
107        Some(ArrayShape::Assoc)
108    } else if c.is_dynamically_sized_array() || c.is_queue() {
109        Some(ArrayShape::Collection)
110    } else if c.is_unpacked_array() {
111        Some(ArrayShape::Fixed)
112    } else {
113        None
114    }
115}
116
117fn bad_array(what: impl Into<String>) -> BuildError {
118    BuildError::BadArray { what: what.into() }
119}
120
121/// The value of a constant `Expr` of type `ty` as a signed integer.
122pub(super) fn const_i64(ty: Ty, v: &BigUint) -> Option<i64> {
123    let signed = BigInt::from(v.clone());
124    let signed = if ty.signed && v.bit(u64::from(ty.width) - 1) {
125        signed - (BigInt::one() << ty.width)
126    } else {
127        signed
128    };
129    i64::try_from(signed).ok()
130}
131
132impl Builder<'_> {
133    pub(super) fn arrays_allowed(&self, what: &str) -> Result<(), BuildError> {
134        match self.no_arrays {
135            None => Ok(()),
136            Some(inside) => Err(unsupported(
137                format!("{what} inside {inside}"),
138                "C14 (array-level values have no weight or bound at build time)",
139            )),
140        }
141    }
142
143    /// Registers the `rand` dynamic-array or queue property `sym` as an
144    /// active random array and returns its id (idempotent). Its size and
145    /// every element are random variables at run time (IEEE 1800-2023 18.4).
146    pub fn declare_rand_array(&mut self, sym: Symbol<'_>) -> Result<u32, BuildError> {
147        let uid = sym.uid();
148        let name = sym.name().to_string();
149        match sym.rand_mode() {
150            RandMode::None => return Err(BuildError::NotRand { name }),
151            RandMode::RandC => {
152                return Err(unsupported(
153                    format!("the `randc` array `{name}`: per-element cycling"),
154                    "C14",
155                ));
156            }
157            RandMode::Rand => {}
158        }
159        if let Some(&id) = self.array_ix.get(&uid) {
160            self.arrays[id as usize].rand = true;
161            return Ok(id);
162        }
163        let vt = sym.value_type().ok_or_else(|| BuildError::Missing {
164            what: format!("value type for `{name}`"),
165        })?;
166        let info = self.array_info(uid.clone(), name, vt, true)?;
167        let id = self.arrays.len() as u32;
168        self.arrays.push(info);
169        self.array_ix.insert(uid, id);
170        Ok(id)
171    }
172
173    /// Registers the dynamic-array or queue `sym` as an active random array
174    /// whether or not it is declared `rand` (LRM 18.11, 18.12: an argument of
175    /// `randomize(v)` / `std::randomize(v)` is random for that call). A `randc`
176    /// array is refused as in [`Self::declare_rand_array`]. Idempotent.
177    pub fn declare_array_forced(&mut self, sym: Symbol<'_>) -> Result<u32, BuildError> {
178        let uid = sym.uid();
179        let name = sym.name().to_string();
180        if matches!(sym.rand_mode(), RandMode::RandC) {
181            return Err(unsupported(
182                format!("the `randc` array `{name}`: per-element cycling"),
183                "C14",
184            ));
185        }
186        if let Some(&id) = self.array_ix.get(&uid) {
187            self.arrays[id as usize].rand = true;
188            return Ok(id);
189        }
190        let vt = sym.value_type().ok_or_else(|| BuildError::Missing {
191            what: format!("value type for `{name}`"),
192        })?;
193        let info = self.array_info(uid.clone(), name, vt, true)?;
194        let id = self.arrays.len() as u32;
195        self.arrays.push(info);
196        self.array_ix.insert(uid, id);
197        Ok(id)
198    }
199
200    fn array_info(
201        &self,
202        symbol: SymbolId,
203        name: String,
204        vt: Type<'_>,
205        rand: bool,
206    ) -> Result<ArrayInfo, BuildError> {
207        match array_shape(vt) {
208            Some(ArrayShape::Collection) => {}
209            // Declared `rand` it is registered so its values are redrawn; a
210            // constraint operand is refused where the operand is resolved
211            // ([`Builder::array_of`]).
212            Some(ArrayShape::Assoc) if rand => {}
213            Some(ArrayShape::Assoc) => return Err(Self::assoc_operand(&name)),
214            // A fixed-size array is not one array-level value: its elements
215            // are leaf variables (`super::aggregate`), and the methods that
216            // take the whole array unroll over them.
217            Some(ArrayShape::Fixed) => {
218                return Err(unsupported(
219                    format!(
220                        "the fixed-size unpacked array `{name}` as a whole-array constraint \
221                         operand (name its elements, or use a reduction method or `foreach`)"
222                    ),
223                    "C14",
224                ));
225            }
226            None => {
227                return Err(BuildError::NonIntegral {
228                    what: format!(
229                        "`{name}` of type `{}`, which is not an array",
230                        vt.to_sv_string()
231                    ),
232                });
233            }
234        }
235        let et = vt
236            .canonical()
237            .element_type()
238            .ok_or_else(|| BuildError::Missing {
239                what: format!("element type of the array `{name}`"),
240            })?;
241        let elem_ty = ty_of(Some(et), &format!("an element of the array `{name}`"))?;
242        let elem_domain = Self::enum_values(et, elem_ty)?;
243        Ok(ArrayInfo {
244            symbol,
245            name,
246            elem_ty,
247            rand,
248            elem_domain,
249            assoc: array_shape(vt) == Some(ArrayShape::Assoc),
250        })
251    }
252
253    fn assoc_operand(name: &str) -> BuildError {
254        bad_array(format!(
255            "`{name}` is an associative array: its size and indices are not randomizable and \
256             its elements are not constraint operands (IEEE 1800-2023 18.4)"
257        ))
258    }
259
260    /// The array a constraint operand names: a dynamic array or queue property
261    /// of this class, registered on first use (as state when it is not
262    /// `rand`).
263    pub(super) fn array_of(&mut self, e: Expression<'_>) -> Result<ArrayId, BuildError> {
264        let e = strip_conversions(e);
265        let sym = match e.kind() {
266            ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue => e.referenced_symbol(),
267            ExpressionKind::MemberAccess => {
268                let base = Self::operand(e, "a member access")?;
269                let is_this = base.kind() == ExpressionKind::NamedValue
270                    && base.referenced_symbol().is_some_and(|s| s.name() == "this");
271                if !is_this {
272                    return Err(BuildError::ForeignObjectAccess {
273                        what: format!(
274                            "the array `.{}` of another object",
275                            e.member_symbol()
276                                .map_or(String::new(), |m| m.name().to_string())
277                        ),
278                    });
279                }
280                e.member_symbol()
281            }
282            other => {
283                return Err(unsupported(
284                    format!("a `{other:?}` expression as an array operand"),
285                    "C14",
286                ));
287            }
288        };
289        let sym = sym.ok_or_else(|| BuildError::Missing {
290            what: "symbol of an array operand".into(),
291        })?;
292        let uid = sym.uid();
293        if let Some(&id) = self.array_ix.get(&uid) {
294            if self.arrays[id as usize].assoc {
295                return Err(Self::assoc_operand(&self.arrays[id as usize].name));
296            }
297            return Ok(id);
298        }
299        let vt = e.expr_type().ok_or_else(|| BuildError::Missing {
300            what: format!("type of the array `{}`", sym.name()),
301        })?;
302        let info = self.array_info(uid.clone(), sym.name().to_string(), vt, false)?;
303        let id = self.arrays.len() as u32;
304        self.arrays.push(info);
305        self.array_ix.insert(uid, id);
306        Ok(id)
307    }
308
309    /// `A.size` / `A.size()` (and a queue's `num`): `Some(array operand)` for
310    /// such a call.
311    pub(super) fn size_call_array<'d>(e: Expression<'d>) -> Option<Expression<'d>> {
312        let sub = e.system_subroutine()?;
313        if !matches!(
314            sub.known_name_id(),
315            KnownSystemName::Size | KnownSystemName::ArraySize | KnownSystemName::Num
316        ) {
317            return None;
318        }
319        let arr = *e.operands().first()?;
320        array_shape(arr.expr_type()?).map(|_| arr)
321    }
322
323    /// The array-level IR of `A.size`.
324    pub(super) fn array_size(&mut self, arr: Expression<'_>) -> Result<Expr, BuildError> {
325        self.arrays_allowed("an array size")?;
326        let id = self.array_of(arr)?;
327        Ok(Expr::Array(ArrayNode::Size(id)))
328    }
329
330    /// `A[sel]` where `base` (type of `A`) is an unpacked array.
331    pub(super) fn array_element(
332        &mut self,
333        e: Expression<'_>,
334        base: Expression<'_>,
335        ty: Ty,
336    ) -> Result<Expr, BuildError> {
337        let shape = base.expr_type().and_then(array_shape);
338        let sel = e.selector().ok_or_else(|| BuildError::Missing {
339            what: "index of an element select".into(),
340        })?;
341        match shape {
342            Some(ArrayShape::Collection) => {
343                self.arrays_allowed("an array element")?;
344                let id = self.array_of(base)?;
345                let index = self.expr(sel)?;
346                let mut has_var = false;
347                index.for_each_var(&mut |_, _| has_var = true);
348                if has_var {
349                    return Err(bad_array(format!(
350                        "a random variable in the index of the array `{}`: an index is a \
351                         constant, a state variable, a loop variable or the size (IEEE \
352                         1800-2023 18.5.7.1)",
353                        self.arrays[id as usize].name
354                    )));
355                }
356                let elem_ty = self.arrays[id as usize].elem_ty;
357                let el = Expr::Array(ArrayNode::Elem {
358                    array: id,
359                    index: Box::new(index),
360                    ty: elem_ty,
361                });
362                Ok(if elem_ty == ty {
363                    el
364                } else {
365                    Expr::Resize(Box::new(el), ty)
366                })
367            }
368            Some(ArrayShape::Assoc) => Err(bad_array(
369                "an element of an associative array: its indices are not randomizable and it \
370                 has no constraint form (IEEE 1800-2023 18.4)",
371            )),
372            Some(ArrayShape::Fixed) => self.fixed_element(e),
373            None => Err(BuildError::Missing {
374                what: "array type of an element select base".into(),
375            }),
376        }
377    }
378
379    /// An element of a fixed-size unpacked array that is not reached through
380    /// a property of this class (`aggregate_chain` resolves every property and
381    /// struct-member path before this is asked): a local, a hierarchical
382    /// reference, or an array read through an object handle.
383    fn fixed_element(&mut self, e: Expression<'_>) -> Result<Expr, BuildError> {
384        let what = e
385            .select_value()
386            .map(strip_conversions)
387            .and_then(|b| b.referenced_symbol().or_else(|| b.member_symbol()))
388            .map_or_else(String::new, |s| s.name().to_string());
389        Err(unsupported(
390            format!(
391                "an element of the unpacked array `{what}`, which is not a property (or a \
392                 member of a struct property) of this class"
393            ),
394            "C14",
395        ))
396    }
397
398    /// The names of a `foreach` constraint's loop variables, from its syntax
399    /// (the semantic node does not keep them).
400    fn foreach_vars(&self, c: Constraint<'_>) -> Result<Vec<String>, BuildError> {
401        for tree in self.trees {
402            if let Some(node) = c.syntax(tree) {
403                let l = LoopConstraintSyntax::cast(node).ok_or_else(|| BuildError::Missing {
404                    what: "`foreach` syntax for a Foreach constraint".into(),
405                })?;
406                return Ok(l
407                    .loop_list()
408                    .loop_variables()
409                    .iter()
410                    .map(|v| v.syntax().text().trim().to_string())
411                    .collect());
412            }
413        }
414        Err(BuildError::Missing {
415            what: "syntax node for a constraint (is its syntax tree passed to the builder?)".into(),
416        })
417    }
418
419    fn fresh_loop(&mut self) -> LoopId {
420        let id = self.next_loop;
421        self.next_loop += 1;
422        id
423    }
424
425    /// Builds `body` with `scope` visible, returning its conjuncts.
426    fn in_scope(
427        &mut self,
428        scope: LoopScope,
429        body: Constraint<'_>,
430    ) -> Result<Vec<Expr>, BuildError> {
431        self.loops.push(scope);
432        let r = self.constraint(body);
433        self.loops.pop();
434        r
435    }
436
437    /// `foreach (A[i]) body` (IEEE 1800-2023 18.5.7.1).
438    pub(super) fn foreach(&mut self, c: Constraint<'_>) -> Result<Vec<Expr>, BuildError> {
439        let names = self.foreach_vars(c)?;
440        let kids = c.children();
441        let arr =
442            kids.iter()
443                .find_map(|n| n.as_expression())
444                .ok_or_else(|| BuildError::Missing {
445                    what: "array expression of a `foreach` constraint".into(),
446                })?;
447        let body =
448            kids.iter()
449                .find_map(|n| n.as_constraint())
450                .ok_or_else(|| BuildError::Missing {
451                    what: "body of a `foreach` constraint".into(),
452                })?;
453        let at = arr.expr_type().ok_or_else(|| BuildError::Missing {
454            what: "type of the `foreach` array".into(),
455        })?;
456        let shape = array_shape(at);
457        // LRM 12.7.3 / 18.5.7.1: `foreach (m[i, j])` loops each listed dimension, the first
458        // variable outermost; an empty slot (`m[i, , k]`) skips its dimension. Every looped
459        // dimension must have a static extent (a fixed unpacked or a packed one), so the
460        // body unrolls; a dynamic, queue or associative dimension stays refused.
461        let nested_fixed = shape == Some(ArrayShape::Fixed)
462            && at
463                .canonical()
464                .element_type()
465                .is_some_and(|elem| array_shape(elem) == Some(ArrayShape::Fixed));
466        if names.len() > 1 || nested_fixed {
467            let dims = self.static_dims(at, &names)?;
468            return self.unroll_nd(&dims, body);
469        }
470        let [name] = names.as_slice() else {
471            return Err(BuildError::Missing {
472                what: "a loop variable of a `foreach` constraint".into(),
473            });
474        };
475        if shape.is_some()
476            && at
477                .canonical()
478                .element_type()
479                .is_some_and(|elem| array_shape(elem).is_some())
480        {
481            return Err(unsupported("a `foreach` over an array of arrays", "C11"));
482        }
483        let int = Ty::signed(32);
484        match shape {
485            Some(ArrayShape::Collection) => {
486                self.arrays_allowed("a `foreach`")?;
487                let array = self.array_of(arr)?;
488                let var = self.fresh_loop();
489                // A `dist` is fine here: it is built as a template and instanced per
490                // element at expansion (`dist::Dist::subject`).
491                let (r0, b0, rb0) = (
492                    self.real_conds.len() + self.real_dists.len(),
493                    self.before.len(),
494                    self.real_before.len(),
495                );
496                let parts = self.in_scope(
497                    LoopScope {
498                        name: name.clone(),
499                        value: LoopBind::Runtime(var, int),
500                        index: None,
501                    },
502                    body,
503                )?;
504                if self.real_conds.len() + self.real_dists.len() != r0
505                    || self.before.len() != b0
506                    || self.real_before.len() != rb0
507                {
508                    return Err(unsupported(
509                        "a real `dist` or condition, or a `solve ... before` constraint, inside a \
510                         `foreach` over a dynamic array or queue (it would have to hold once per \
511                         element)",
512                        "C11",
513                    ));
514                }
515                Ok(vec![Expr::Array(ArrayNode::Foreach {
516                    array,
517                    var,
518                    body: Box::new(and_all(parts)),
519                })])
520            }
521            Some(ArrayShape::Assoc) => Err(bad_array(
522                "a `foreach` over an associative array: its indices are not randomizable and \
523                 it has no constraint form (IEEE 1800-2023 18.4)",
524            )),
525            Some(ArrayShape::Fixed) => {
526                let r = at.canonical().fixed_range();
527                self.unroll(name, i64::from(r.left), i64::from(r.right), body)
528            }
529            None => {
530                // A packed vector or packed array: its extent is static.
531                let width = u32::try_from(at.bit_width()).map_err(|_| BuildError::Missing {
532                    what: "representable width for a `foreach` array".into(),
533                })?;
534                let g = Geom::of(at, width, "the `foreach` array")?;
535                self.unroll(name, g.left, g.right, body)
536            }
537        }
538    }
539
540    /// The `(name, left, right)` of each dimension a multi-variable `foreach` loops over
541    /// (`None` extent for an omitted loop variable), walking `names.len()` dimensions of `ty`.
542    fn static_dims(
543        &self,
544        ty: sv_lang::Type<'_>,
545        names: &[String],
546    ) -> Result<Vec<(String, i64, i64)>, BuildError> {
547        let mut dims = Vec::new();
548        let mut cur = ty;
549        for (k, name) in names.iter().enumerate() {
550            let c = cur.canonical();
551            let (left, right) = match array_shape(cur) {
552                Some(ArrayShape::Fixed) => {
553                    let r = c.fixed_range();
554                    (i64::from(r.left), i64::from(r.right))
555                }
556                None if c.is_integral() => {
557                    let width = u32::try_from(c.bit_width()).map_err(|_| BuildError::Missing {
558                        what: "representable width for a `foreach` array".into(),
559                    })?;
560                    let g = Geom::of(cur, width, "the `foreach` array")?;
561                    (g.left, g.right)
562                }
563                _ => {
564                    return Err(unsupported(
565                        format!(
566                            "a multi-dimensional `foreach`: dimension {} is a dynamic array,                              queue or associative array (only fixed-size dimensions unroll)",
567                            k + 1
568                        ),
569                        "C11",
570                    ));
571                }
572            };
573            dims.push((name.clone(), left, right));
574            if k + 1 < names.len() {
575                cur = c.element_type().ok_or_else(|| {
576                    unsupported(
577                        format!(
578                            "a `foreach` with {} loop variables over an array with fewer \
579                             dimensions",
580                            names.len()
581                        ),
582                        "C11",
583                    )
584                })?;
585            }
586        }
587        Ok(dims)
588    }
589
590    /// The body of a multi-variable `foreach`, once per index tuple (first variable outermost,
591    /// each from its left bound toward its right; LRM 12.7.3).
592    fn unroll_nd(
593        &mut self,
594        dims: &[(String, i64, i64)],
595        body: Constraint<'_>,
596    ) -> Result<Vec<Expr>, BuildError> {
597        let Some((name, left, right)) = dims.first() else {
598            return self.constraint(body);
599        };
600        if name.is_empty() {
601            return self.unroll_nd(&dims[1..], body);
602        }
603        let idxs: Vec<i64> = if left >= right {
604            (*right..=*left).rev().collect()
605        } else {
606            (*left..=*right).collect()
607        };
608        let mut out = Vec::new();
609        for i in idxs {
610            self.loops.push(LoopScope {
611                name: name.clone(),
612                value: LoopBind::Const(i),
613                index: None,
614            });
615            let r = self.unroll_nd(&dims[1..], body);
616            self.loops.pop();
617            out.extend(r?);
618        }
619        Ok(out)
620    }
621
622    /// The body of a `foreach` over a statically known extent, once per index
623    /// from the left bound toward the right (LRM 12.7.3).
624    fn unroll(
625        &mut self,
626        name: &str,
627        left: i64,
628        right: i64,
629        body: Constraint<'_>,
630    ) -> Result<Vec<Expr>, BuildError> {
631        let idxs: Vec<i64> = if left >= right {
632            (right..=left).rev().collect()
633        } else {
634            (left..=right).collect()
635        };
636        let mut out = Vec::new();
637        for i in idxs {
638            out.extend(self.in_scope(
639                LoopScope {
640                    name: name.to_string(),
641                    value: LoopBind::Const(i),
642                    index: None,
643                },
644                body,
645            )?);
646        }
647        Ok(out)
648    }
649
650    /// Whether the constraint being built is inside a `foreach` over a
651    /// dynamic array or queue (its body holds once per run-time element); a
652    /// `foreach` over a static extent is unrolled and is not.
653    pub(super) fn in_runtime_loop(&self) -> bool {
654        self.loops
655            .iter()
656            .any(|s| matches!(s.value, LoopBind::Runtime(..)))
657    }
658
659    fn scope_of(&self, name: &str) -> Option<&LoopScope> {
660        self.loops.iter().rev().find(|s| s.name == name)
661    }
662
663    fn bind_expr(&self, b: LoopBind, ty: Ty) -> Result<Expr, BuildError> {
664        match b {
665            LoopBind::Runtime(id, lt) => {
666                let l = Expr::Array(ArrayNode::Loop(id, lt));
667                Ok(if lt == ty {
668                    l
669                } else {
670                    Expr::Resize(Box::new(l), ty)
671                })
672            }
673            LoopBind::Const(i) => Ok(iconst(ty, i)),
674            LoopBind::Elem(x) => Ok(if x.ty() == ty {
675                x
676            } else {
677                Expr::Resize(Box::new(x), ty)
678            }),
679        }
680    }
681
682    /// A reference to a `foreach` loop variable or a reduction's `item`.
683    pub(super) fn loop_ref(&mut self, sym: Symbol<'_>, ty: Ty) -> Result<Expr, BuildError> {
684        debug_assert_eq!(sym.kind(), SymbolKind::Iterator);
685        let name = sym.name();
686        let b = self
687            .scope_of(name)
688            .ok_or_else(|| BuildError::Missing {
689                what: format!("an enclosing `foreach` or array method binding `{name}`"),
690            })?
691            .value
692            .clone();
693        if matches!(b, LoopBind::Runtime(..)) {
694            self.arrays_allowed("a loop variable")?;
695        }
696        self.bind_expr(b, ty)
697    }
698
699    /// `item.index` inside a reduction's `with` clause.
700    pub(super) fn iterator_index(&mut self, e: Expression<'_>, ty: Ty) -> Result<Expr, BuildError> {
701        let this = *e.operands().first().ok_or_else(|| BuildError::Missing {
702            what: "iterator of `index`".into(),
703        })?;
704        let name = this
705            .referenced_symbol()
706            .map(|s| s.name().to_string())
707            .ok_or_else(|| BuildError::Missing {
708                what: "symbol of the iterator of `index`".into(),
709            })?;
710        let b = self
711            .scope_of(&name)
712            .and_then(|s| s.index.clone())
713            .ok_or_else(|| BuildError::Missing {
714                what: format!("an array method iterator `{name}` for `.index`"),
715            })?;
716        self.arrays_allowed("an iterator index")?;
717        self.bind_expr(b, ty)
718    }
719
720    /// `A.sum()`, `.product()`, `.and()`, `.or()`, `.xor()` with an optional
721    /// `with (expr)` (IEEE 1800-2023 18.5.7.2, 7.12.3).
722    pub(super) fn array_reduction(
723        &mut self,
724        e: Expression<'_>,
725        op: ReduceOp,
726        ty: Ty,
727    ) -> Result<Expr, BuildError> {
728        let arr = e
729            .operands()
730            .into_iter()
731            .find(|o| o.expr_type().is_some_and(|t| array_shape(t).is_some()))
732            .ok_or_else(|| BuildError::Missing {
733                what: "array operand of a reduction method".into(),
734            })?;
735        if arr.expr_type().and_then(array_shape) == Some(ArrayShape::Fixed) {
736            return self.fixed_reduction(e, arr, op, ty);
737        }
738        self.arrays_allowed("an array reduction")?;
739        let array = self.array_of(arr)?;
740        let elem_ty = self.arrays[array as usize].elem_ty;
741        let var = self.fresh_loop();
742        let index_var = self.fresh_loop();
743        let body = match e.iterator_expr() {
744            Some(w) => {
745                let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
746                    what: "iterator variable of a reduction's `with` clause".into(),
747                })?;
748                self.loops.push(LoopScope {
749                    name: iter.name().to_string(),
750                    value: LoopBind::Runtime(var, elem_ty),
751                    index: Some(LoopBind::Runtime(index_var, Ty::signed(32))),
752                });
753                let b = self.expr(w);
754                self.loops.pop();
755                b?
756            }
757            None => Expr::Array(ArrayNode::Loop(var, elem_ty)),
758        };
759        let body = if body.ty() == ty {
760            body
761        } else {
762            coerce(body, ty)
763        };
764        Ok(Expr::Array(ArrayNode::Reduce {
765            op,
766            array,
767            var,
768            index_var,
769            body: Box::new(body),
770        }))
771    }
772
773    /// A reduction method over a fixed-size unpacked array (a property of
774    /// this class, or a struct member of one): its extent is known now, so it
775    /// unrolls into the fold of its elements, each bound to the iterator
776    /// (IEEE 1800-2023 7.12.3, 18.5.7.2).
777    fn fixed_reduction(
778        &mut self,
779        e: Expression<'_>,
780        arr: Expression<'_>,
781        op: ReduceOp,
782        ty: Ty,
783    ) -> Result<Expr, BuildError> {
784        let at = arr
785            .expr_type()
786            .ok_or_else(|| BuildError::Missing {
787                what: "type of the reduced array".into(),
788            })?
789            .canonical();
790        let r = at.fixed_unpacked_array_range();
791        let (lo, hi) = (
792            i64::from(r.left).min(i64::from(r.right)),
793            i64::from(r.left).max(i64::from(r.right)),
794        );
795        let et = at.element_type().ok_or_else(|| BuildError::Missing {
796            what: "element type of the reduced array".into(),
797        })?;
798        let elem_ty = ty_of(Some(et), "an element of the reduced array")?;
799        let mut acc: Option<Expr> = None;
800        for i in lo..=hi {
801            let item = self.aggregate_element(arr, i, elem_ty)?;
802            let body = match e.iterator_expr() {
803                Some(w) => {
804                    let iter = e.iterator_var().ok_or_else(|| BuildError::Missing {
805                        what: "iterator variable of a reduction's `with` clause".into(),
806                    })?;
807                    self.loops.push(LoopScope {
808                        name: iter.name().to_string(),
809                        value: LoopBind::Elem(item),
810                        index: Some(LoopBind::Const(i)),
811                    });
812                    let b = self.expr(w);
813                    self.loops.pop();
814                    b?
815                }
816                None => item,
817            };
818            let body = if body.ty() == ty {
819                body
820            } else {
821                coerce(body, ty)
822            };
823            acc = Some(match acc {
824                None => body,
825                Some(a) => {
826                    let bop = match op {
827                        ReduceOp::Sum => BinOp::Add,
828                        ReduceOp::Product => BinOp::Mul,
829                        ReduceOp::And => BinOp::And,
830                        ReduceOp::Or => BinOp::Or,
831                        ReduceOp::Xor => BinOp::Xor,
832                    };
833                    bin(bop, a, body, ty)
834                }
835            });
836        }
837        acc.ok_or_else(|| BuildError::Missing {
838            what: "elements of the reduced array".into(),
839        })
840    }
841
842    /// `left inside {A}` for an array `A` in the set: true when `left` equals
843    /// some element of `A` (IEEE 1800-2023 11.4.13, 18.5.3: the array is
844    /// traversed and each element is a member of the set). A fixed-size array
845    /// unrolls into a disjunction of equalities; a dynamic array or queue is
846    /// an `or`-reduction over its run-time elements, so an empty one admits
847    /// nothing.
848    pub(super) fn inside_array(
849        &mut self,
850        left: &Expr,
851        arr: Expression<'_>,
852    ) -> Result<Expr, BuildError> {
853        let eq = |left: &Expr, x: Expr| {
854            let t = common_ty(left.ty(), x.ty());
855            bin(BinOp::Eq, coerce(left.clone(), t), coerce(x, t), Ty::BIT)
856        };
857        let at = arr.expr_type().ok_or_else(|| BuildError::Missing {
858            what: "type of an `inside` array".into(),
859        })?;
860        match array_shape(at) {
861            Some(ArrayShape::Fixed) => {
862                let c = at.canonical();
863                let r = c.fixed_unpacked_array_range();
864                let (lo, hi) = (
865                    i64::from(r.left).min(i64::from(r.right)),
866                    i64::from(r.left).max(i64::from(r.right)),
867                );
868                let et = c.element_type().ok_or_else(|| BuildError::Missing {
869                    what: "element type of an `inside` array".into(),
870                })?;
871                let elem_ty = ty_of(Some(et), "an element of an `inside` array")?;
872                let mut acc: Option<Expr> = None;
873                for i in lo..=hi {
874                    let item = self.aggregate_element(arr, i, elem_ty)?;
875                    let term = eq(left, item);
876                    acc = Some(match acc {
877                        None => term,
878                        Some(prev) => bin(BinOp::LogOr, prev, term, Ty::BIT),
879                    });
880                }
881                acc.ok_or_else(|| BuildError::Missing {
882                    what: "elements of an `inside` array".into(),
883                })
884            }
885            Some(ArrayShape::Collection) => {
886                self.arrays_allowed("an `inside` over an array")?;
887                let array = self.array_of(arr)?;
888                let elem_ty = self.arrays[array as usize].elem_ty;
889                let var = self.fresh_loop();
890                let index_var = self.fresh_loop();
891                let body = eq(left, Expr::Array(ArrayNode::Loop(var, elem_ty)));
892                Ok(Expr::Array(ArrayNode::Reduce {
893                    op: ReduceOp::Or,
894                    array,
895                    var,
896                    index_var,
897                    body: Box::new(body),
898                }))
899            }
900            Some(ArrayShape::Assoc) | None => Err(bad_array(
901                "an associative array in an `inside` set: its elements are not constraint \
902                 operands (IEEE 1800-2023 18.4)",
903            )),
904        }
905    }
906
907    /// The array-level value of a call to a system method on an array, or
908    /// `None` for any other call.
909    pub(super) fn array_call(
910        &mut self,
911        e: Expression<'_>,
912        ty: Ty,
913    ) -> Option<Result<Expr, BuildError>> {
914        let sub = e.system_subroutine()?;
915        let op = match sub.known_name_id() {
916            KnownSystemName::Size | KnownSystemName::ArraySize | KnownSystemName::Num => {
917                let arr = Self::size_call_array(e)?;
918                return Some(self.array_size(arr));
919            }
920            KnownSystemName::Index => return Some(self.iterator_index(e, ty)),
921            KnownSystemName::Sum => ReduceOp::Sum,
922            KnownSystemName::Product => ReduceOp::Product,
923            KnownSystemName::And => ReduceOp::And,
924            KnownSystemName::Or => ReduceOp::Or,
925            KnownSystemName::XOr => ReduceOp::Xor,
926            _ => return None,
927        };
928        Some(self.array_reduction(e, op, ty))
929    }
930}