Skip to main content

miden_air/lookup/debug/
validation.rs

1//! Single AIR self-validation entry point, backed by one unified walker.
2//!
3//! Exposes one free function, [`validate`], and one extension trait,
4//! [`ValidateLookupAir`], so any qualifying [`LookupAir`] can be checked with
5//! `air.validate(layout)`. One short-circuit [`Result<(), ValidationError>`] covers:
6//!
7//! - `num_columns` declared vs observed (the walker counts `next_column` calls).
8//! - Per-group and per-column `Deg { n, d }` declared vs observed (via
9//!   [`SymbolicExpression::degree_multiple`] on the running `(V, U)`).
10//! - Cached-encoding canonical vs encoded `(V, U)` equivalence, checked by evaluating the symbolic
11//!   difference `U_c·V_e − U_e·V_c` at a random row.
12//! - Simple-group scope: no illegal `insert_encoded` outside the `encoded` closure.
13//!
14//! The global max-degree budget is **not** checked here — the STARK prover's
15//! quotient validation already enforces it and duplicating that check muddies
16//! this module's purpose.
17
18use alloc::vec::Vec;
19use core::{fmt, marker::PhantomData};
20
21use miden_core::field::{PrimeCharacteristicRing, QuadFelt};
22use miden_crypto::{
23    rand::random_felt,
24    stark::air::{
25        AirBuilder, PeriodicAirBuilder, PermutationAirBuilder,
26        symbolic::{
27            BaseEntry, BaseLeaf, ExtEntry, ExtLeaf, SymbolicAirBuilder, SymbolicExpr,
28            SymbolicExpression, SymbolicExpressionExt, SymbolicVariable, SymbolicVariableExt,
29        },
30    },
31};
32
33use super::super::{
34    Challenges, Deg, LookupAir, LookupBatch, LookupBuilder, LookupColumn, LookupGroup,
35    LookupMessage,
36};
37use crate::Felt;
38
39type Inner = SymbolicAirBuilder<Felt, QuadFelt>;
40type Expr = SymbolicExpression<Felt>;
41type ExprEF = SymbolicExpressionExt<Felt, QuadFelt>;
42
43// VALIDATION ERROR
44// ================================================================================================
45
46/// First problem [`validate`] observed. See the module docstring for the per-check
47/// semantics; each variant corresponds to one of the checks.
48#[derive(Clone, Debug)]
49pub enum ValidationError {
50    /// [`LookupAir::num_columns`] disagreed with the number of `next_column` calls
51    /// issued by `eval`.
52    NumColumnsMismatch { declared: usize, observed: usize },
53    /// A column's declared `Deg` differs from the observed symbolic degree of
54    /// its accumulated `(V, U)`. Declared degrees are authoritative and must
55    /// match exactly — loose upper bounds are rejected.
56    ColumnDegreeMismatch {
57        column_idx: usize,
58        declared: Deg,
59        observed: Deg,
60    },
61    /// A group's declared `Deg` differs from the observed symbolic degree of
62    /// the group's `(V, U)` fold. Declared degrees are authoritative and must
63    /// match exactly — loose upper bounds are rejected.
64    GroupDegreeMismatch {
65        column_idx: usize,
66        group_idx: usize,
67        name: &'static str,
68        declared: Deg,
69        observed: Deg,
70    },
71    /// A cached-encoding group's canonical and encoded closures produced different
72    /// `(V, U)` pairs: the symbolic difference `V_c·U_e − V_e·U_c` evaluated to a
73    /// non-zero `QuadFelt` at the sampled random row.
74    EncodingMismatch {
75        column_idx: usize,
76        group_idx: usize,
77        name: &'static str,
78        diff: QuadFelt,
79    },
80    /// A simple-mode group called `insert_encoded`, which is only legal inside the
81    /// `encoded` closure of `group_with_cached_encoding`.
82    ScopeViolation {
83        column_idx: usize,
84        group_idx: usize,
85        name: &'static str,
86    },
87}
88
89impl fmt::Display for ValidationError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::NumColumnsMismatch { declared, observed } => {
93                write!(f, "num_columns mismatch: declared {declared}, observed {observed}")
94            },
95            Self::ColumnDegreeMismatch { column_idx, declared, observed } => write!(
96                f,
97                "column[{column_idx}] degree mismatch: declared (v={}, u={}), observed (v={}, u={})",
98                declared.v, declared.u, observed.v, observed.u,
99            ),
100            Self::GroupDegreeMismatch {
101                column_idx,
102                group_idx,
103                name,
104                declared,
105                observed,
106            } => write!(
107                f,
108                "column[{column_idx}] group[{group_idx}] {name:?} degree mismatch: declared (v={}, u={}), observed (v={}, u={})",
109                declared.v, declared.u, observed.v, observed.u,
110            ),
111            Self::EncodingMismatch { column_idx, group_idx, name, diff } => write!(
112                f,
113                "column[{column_idx}] group[{group_idx}] {name:?} cached-encoding mismatch: V_c·U_e − V_e·U_c = {diff:?}",
114            ),
115            Self::ScopeViolation { column_idx, group_idx, name } => write!(
116                f,
117                "column[{column_idx}] group[{group_idx}] {name:?} simple group called insert_encoded",
118            ),
119        }
120    }
121}
122
123// LAYOUT
124// ================================================================================================
125
126/// Subset of the full `AirLayout` struct that [`validate`] actually consumes. Kept
127/// local so callers don't need to thread prover-only fields (permutation width,
128/// committed final count) through just to run the self-check.
129#[derive(Clone, Copy, Debug)]
130pub struct ValidateLayout {
131    pub trace_width: usize,
132    pub num_public_values: usize,
133    pub num_periodic_columns: usize,
134    pub permutation_width: usize,
135    pub num_permutation_challenges: usize,
136    pub num_permutation_values: usize,
137}
138
139impl ValidateLayout {
140    fn to_symbolic(self) -> miden_crypto::stark::air::symbolic::AirLayout {
141        miden_crypto::stark::air::symbolic::AirLayout {
142            preprocessed_width: 0,
143            main_width: self.trace_width,
144            num_public_values: self.num_public_values,
145            permutation_width: self.permutation_width,
146            num_permutation_challenges: self.num_permutation_challenges,
147            num_permutation_values: self.num_permutation_values,
148            num_periodic_columns: self.num_periodic_columns,
149        }
150    }
151}
152
153// VALIDATE
154// ================================================================================================
155
156/// Run every AIR self-check in one walk.
157///
158/// Short-circuits on the first problem. See [`ValidationError`] for the variants.
159pub fn validate<A>(air: &A, layout: ValidateLayout) -> Result<(), ValidationError>
160where
161    for<'ab, 'r> A: LookupAir<ValidationBuilder<'ab, 'r>>,
162{
163    // Sample a single random row valuation shared by the symbolic and concrete
164    // sides. `alpha`/`beta` are instantiated twice: once as symbolic `Challenge`
165    // leaves inside `SymbolicAirBuilder::permutation_randomness`, and once as
166    // concrete `QuadFelt`s in `row_valuation`. The evaluator below maps
167    // `ExtEntry::Challenge { index: 0/1 }` back to these concrete values.
168    let current: Vec<Felt> = (0..layout.trace_width).map(|_| random_felt()).collect();
169    let next: Vec<Felt> = (0..layout.trace_width).map(|_| random_felt()).collect();
170    let periodic: Vec<Felt> = (0..layout.num_periodic_columns).map(|_| random_felt()).collect();
171    let alpha = QuadFelt::new([random_felt(), random_felt()]);
172    let beta = QuadFelt::new([random_felt(), random_felt()]);
173
174    let mut sym = SymbolicAirBuilder::<Felt, QuadFelt>::new(layout.to_symbolic());
175    let row_valuation = RowValuation {
176        current: &current,
177        next: &next,
178        periodic: &periodic,
179        alpha,
180        beta,
181    };
182    let mut builder = ValidationBuilder::new(&mut sym, air, row_valuation);
183    air.eval(&mut builder);
184    match builder.take_error() {
185        Some(err) => Err(err),
186        None => Ok(()),
187    }
188}
189
190// EXTENSION TRAIT
191// ================================================================================================
192
193/// Extension trait that adapts [`validate`] into a method on any qualifying
194/// [`LookupAir`]. Call sites write `MyLookupAir.validate(layout)` instead of
195/// `validate(&MyLookupAir, layout)`.
196pub trait ValidateLookupAir {
197    fn validate(&self, layout: ValidateLayout) -> Result<(), ValidationError>;
198}
199
200impl<A> ValidateLookupAir for A
201where
202    for<'ab, 'r> A: LookupAir<ValidationBuilder<'ab, 'r>>,
203{
204    fn validate(&self, layout: ValidateLayout) -> Result<(), ValidationError> {
205        validate(self, layout)
206    }
207}
208
209// ROW VALUATION
210// ================================================================================================
211
212/// Concrete valuation used to evaluate symbolic `(V, U)` trees when the walker
213/// needs a numeric answer (cached-encoding equivalence).
214#[derive(Clone, Copy)]
215struct RowValuation<'r> {
216    current: &'r [Felt],
217    next: &'r [Felt],
218    periodic: &'r [Felt],
219    /// `Challenge[0]` in any `SymbolicExpressionExt` tree.
220    alpha: QuadFelt,
221    /// `Challenge[1]` in any `SymbolicExpressionExt` tree.
222    beta: QuadFelt,
223}
224
225impl<'r> RowValuation<'r> {
226    fn eval_base(&self, expr: &Expr) -> Felt {
227        match expr {
228            SymbolicExpr::Leaf(leaf) => self.eval_base_leaf(leaf),
229            SymbolicExpr::Add { x, y, .. } => self.eval_base(x) + self.eval_base(y),
230            SymbolicExpr::Sub { x, y, .. } => self.eval_base(x) - self.eval_base(y),
231            SymbolicExpr::Neg { x, .. } => -self.eval_base(x),
232            SymbolicExpr::Mul { x, y, .. } => self.eval_base(x) * self.eval_base(y),
233        }
234    }
235
236    fn eval_base_leaf(&self, leaf: &BaseLeaf<Felt>) -> Felt {
237        match leaf {
238            BaseLeaf::Constant(c) => *c,
239            BaseLeaf::Variable(SymbolicVariable { entry, index, .. }) => match entry {
240                BaseEntry::Main { offset: 0 } => self.current[*index],
241                BaseEntry::Main { offset: 1 } => self.next[*index],
242                BaseEntry::Periodic => self.periodic[*index],
243                BaseEntry::Main { offset } => {
244                    panic!("unexpected main offset {offset} in LookupAir::eval")
245                },
246                // LookupBuilder doesn't expose preprocessed or public values, and
247                // LookupAir::eval can't construct these leaves.
248                BaseEntry::Preprocessed { .. } | BaseEntry::Public => {
249                    panic!("unexpected {entry:?} leaf in LookupAir::eval")
250                },
251            },
252            // Selector leaves are only produced by `AirBuilder::is_first_row` / etc.,
253            // which LookupBuilder does not expose.
254            BaseLeaf::IsFirstRow | BaseLeaf::IsLastRow | BaseLeaf::IsTransition => {
255                panic!("selector leaf {leaf:?} unexpected in LookupAir::eval")
256            },
257        }
258    }
259
260    fn eval_ext(&self, expr: &ExprEF) -> QuadFelt {
261        match expr {
262            SymbolicExpr::Leaf(leaf) => self.eval_ext_leaf(leaf),
263            SymbolicExpr::Add { x, y, .. } => self.eval_ext(x) + self.eval_ext(y),
264            SymbolicExpr::Sub { x, y, .. } => self.eval_ext(x) - self.eval_ext(y),
265            SymbolicExpr::Neg { x, .. } => -self.eval_ext(x),
266            SymbolicExpr::Mul { x, y, .. } => self.eval_ext(x) * self.eval_ext(y),
267        }
268    }
269
270    fn eval_ext_leaf(&self, leaf: &ExtLeaf<Felt, QuadFelt>) -> QuadFelt {
271        match leaf {
272            ExtLeaf::Base(inner) => self.eval_base(inner).into(),
273            ExtLeaf::ExtConstant(c) => *c,
274            ExtLeaf::ExtVariable(SymbolicVariableExt { entry, index, .. }) => match entry {
275                ExtEntry::Challenge => match *index {
276                    0 => self.alpha,
277                    1 => self.beta,
278                    i => panic!("unexpected challenge index {i} in LookupAir::eval"),
279                },
280                // LookupBuilder doesn't expose permutation columns or permutation
281                // values — the prover-side builder is the only one that touches them.
282                ExtEntry::Permutation { .. } | ExtEntry::PermutationValue => {
283                    panic!("unexpected {entry:?} leaf in LookupAir::eval")
284                },
285            },
286        }
287    }
288}
289
290// BUILDER
291// ================================================================================================
292
293/// Unified walker that cross-checks `(V, U)` degrees, cached-encoding equivalence,
294/// and simple-group scope in a single pass over [`LookupAir::eval`]. The first
295/// error observed is preserved in an internal slot; any later problem is ignored
296/// so [`validate`] can short-circuit cleanly once `eval` returns.
297pub struct ValidationBuilder<'ab, 'r> {
298    ab: &'ab mut Inner,
299    sym_challenges: Challenges<ExprEF>,
300    row_valuation: RowValuation<'r>,
301    column_idx: usize,
302    declared_columns: usize,
303    error: Option<ValidationError>,
304}
305
306impl<'ab, 'r> ValidationBuilder<'ab, 'r> {
307    fn new<A>(ab: &'ab mut Inner, air: &A, row_valuation: RowValuation<'r>) -> Self
308    where
309        A: LookupAir<Self>,
310    {
311        let (alpha, beta): (ExprEF, ExprEF) = {
312            let r = ab.permutation_randomness();
313            (r[0].into(), r[1].into())
314        };
315        let sym_challenges =
316            Challenges::<ExprEF>::new(alpha, beta, air.max_message_width(), air.num_bus_ids());
317        Self {
318            ab,
319            sym_challenges,
320            row_valuation,
321            column_idx: 0,
322            declared_columns: air.num_columns(),
323            error: None,
324        }
325    }
326
327    fn take_error(mut self) -> Option<ValidationError> {
328        if self.error.is_none() && self.column_idx != self.declared_columns {
329            self.error = Some(ValidationError::NumColumnsMismatch {
330                declared: self.declared_columns,
331                observed: self.column_idx,
332            });
333        }
334        self.error
335    }
336}
337
338impl<'ab, 'r> LookupBuilder for ValidationBuilder<'ab, 'r> {
339    type F = Felt;
340    type Expr = Expr;
341    type Var = SymbolicVariable<Felt>;
342
343    type EF = QuadFelt;
344    type ExprEF = ExprEF;
345    type VarEF = SymbolicVariableExt<Felt, QuadFelt>;
346
347    type PeriodicVar = SymbolicVariable<Felt>;
348
349    type MainWindow = <Inner as AirBuilder>::MainWindow;
350
351    type Column<'c>
352        = ValidationColumn<'c, 'r>
353    where
354        Self: 'c;
355
356    fn main(&self) -> Self::MainWindow {
357        self.ab.main()
358    }
359
360    fn periodic_values(&self) -> &[Self::PeriodicVar] {
361        self.ab.periodic_values()
362    }
363
364    fn next_column<'c, R>(&'c mut self, f: impl FnOnce(&mut Self::Column<'c>) -> R, deg: Deg) -> R {
365        let column_idx = self.column_idx;
366        self.column_idx += 1;
367
368        let already_errored = self.error.is_some();
369        let mut col = ValidationColumn {
370            challenges: &self.sym_challenges,
371            row_valuation: self.row_valuation,
372            u: ExprEF::ONE,
373            v: ExprEF::ZERO,
374            column_idx,
375            next_group_idx: 0,
376            error: None,
377            _phantom: PhantomData,
378        };
379        let result = f(&mut col);
380
381        if !already_errored {
382            if let Some(err) = col.error.take() {
383                self.error = Some(err);
384            } else {
385                let observed = Deg {
386                    v: col.v.degree_multiple(),
387                    u: col.u.degree_multiple(),
388                };
389                if observed != deg {
390                    self.error = Some(ValidationError::ColumnDegreeMismatch {
391                        column_idx,
392                        declared: deg,
393                        observed,
394                    });
395                }
396            }
397        }
398
399        result
400    }
401}
402
403// COLUMN
404// ================================================================================================
405
406pub struct ValidationColumn<'c, 'r> {
407    challenges: &'c Challenges<ExprEF>,
408    row_valuation: RowValuation<'r>,
409    u: ExprEF,
410    v: ExprEF,
411    column_idx: usize,
412    next_group_idx: usize,
413    /// First group-level error observed while walking this column, drained by
414    /// [`ValidationBuilder::next_column`] after the closure returns.
415    error: Option<ValidationError>,
416    _phantom: PhantomData<&'c ()>,
417}
418
419impl<'c, 'r> ValidationColumn<'c, 'r> {
420    fn fold_group(&mut self, u_g: ExprEF, v_g: ExprEF) {
421        self.v = self.v.clone() * u_g.clone() + v_g * self.u.clone();
422        self.u = self.u.clone() * u_g;
423    }
424
425    fn check_group_degree(
426        &mut self,
427        name: &'static str,
428        group_idx: usize,
429        declared: Deg,
430        u: &ExprEF,
431        v: &ExprEF,
432    ) {
433        if self.error.is_some() {
434            return;
435        }
436        let observed = Deg {
437            v: v.degree_multiple(),
438            u: u.degree_multiple(),
439        };
440        if observed != declared {
441            self.error = Some(ValidationError::GroupDegreeMismatch {
442                column_idx: self.column_idx,
443                group_idx,
444                name,
445                declared,
446                observed,
447            });
448        }
449    }
450}
451
452/// Build a fresh group scoped to `challenges`. Taken as a free function (not a
453/// method) so calling it doesn't borrow the containing `ValidationColumn` —
454/// the caller can still mutate `self.error` while the group is alive.
455fn fresh_group<'g>(
456    challenges: &'g Challenges<ExprEF>,
457    inside_encoded_closure: bool,
458) -> ValidationGroup<'g> {
459    ValidationGroup {
460        challenges,
461        u: ExprEF::ONE,
462        v: ExprEF::ZERO,
463        inside_encoded_closure,
464        used_insert_encoded: false,
465    }
466}
467
468impl<'c, 'r> LookupColumn for ValidationColumn<'c, 'r> {
469    type Expr = Expr;
470    type ExprEF = ExprEF;
471
472    type Group<'g>
473        = ValidationGroup<'g>
474    where
475        Self: 'g;
476
477    fn group<'g>(&'g mut self, name: &'static str, f: impl FnOnce(&mut Self::Group<'g>), deg: Deg) {
478        let group_idx = self.next_group_idx;
479        self.next_group_idx += 1;
480
481        let mut group = fresh_group(self.challenges, false);
482        f(&mut group);
483        let ValidationGroup { u, v, used_insert_encoded, .. } = group;
484
485        if self.error.is_none() && used_insert_encoded {
486            self.error = Some(ValidationError::ScopeViolation {
487                column_idx: self.column_idx,
488                group_idx,
489                name,
490            });
491        }
492        self.check_group_degree(name, group_idx, deg, &u, &v);
493        self.fold_group(u, v);
494    }
495
496    fn group_with_cached_encoding<'g>(
497        &'g mut self,
498        name: &'static str,
499        canonical: impl FnOnce(&mut Self::Group<'g>),
500        encoded: impl FnOnce(&mut Self::Group<'g>),
501        deg: Deg,
502    ) {
503        let group_idx = self.next_group_idx;
504        self.next_group_idx += 1;
505
506        let mut canon = fresh_group(self.challenges, false);
507        canonical(&mut canon);
508
509        let mut enc = fresh_group(self.challenges, true);
510        encoded(&mut enc);
511
512        // Cached-encoding equivalence: the two closures must agree on `(V, U)`
513        // up to cross-multiplication, i.e. `V_c·U_e − V_e·U_c == 0`. We don't
514        // rely on symbolic simplification to zero — we evaluate the difference
515        // at the shared random row.
516        if self.error.is_none() {
517            let diff_expr = canon.v.clone() * enc.u.clone() - enc.v.clone() * canon.u;
518            let diff = self.row_valuation.eval_ext(&diff_expr);
519            if diff != QuadFelt::ZERO {
520                self.error = Some(ValidationError::EncodingMismatch {
521                    column_idx: self.column_idx,
522                    group_idx,
523                    name,
524                    diff,
525                });
526            }
527        }
528
529        // Degree check and column fold use the `encoded` half, consistent with
530        // the production constraint path which only emits the encoded form.
531        let ValidationGroup { u, v, .. } = enc;
532        self.check_group_degree(name, group_idx, deg, &u, &v);
533        self.fold_group(u, v);
534    }
535}
536
537// GROUP
538// ================================================================================================
539
540pub struct ValidationGroup<'g> {
541    challenges: &'g Challenges<ExprEF>,
542    u: ExprEF,
543    v: ExprEF,
544    /// Set when this group was opened via the `encoded` closure of
545    /// `group_with_cached_encoding`; toggles the legal use of `insert_encoded`.
546    inside_encoded_closure: bool,
547    /// `true` if `insert_encoded` was called outside its legal scope. The column
548    /// inspects this flag at close time and raises `ScopeViolation` if the group
549    /// was simple.
550    used_insert_encoded: bool,
551}
552
553impl<'g> LookupGroup for ValidationGroup<'g> {
554    type Expr = Expr;
555    type ExprEF = ExprEF;
556
557    type Batch<'b>
558        = ValidationBatch<'b>
559    where
560        Self: 'b;
561
562    fn add<M>(&mut self, _name: &'static str, flag: Expr, msg: impl FnOnce() -> M, _deg: Deg)
563    where
564        M: LookupMessage<Expr, ExprEF>,
565    {
566        let v_msg = msg().encode(self.challenges);
567        self.u += (v_msg - ExprEF::ONE) * flag.clone();
568        self.v += flag;
569    }
570
571    fn remove<M>(&mut self, _name: &'static str, flag: Expr, msg: impl FnOnce() -> M, _deg: Deg)
572    where
573        M: LookupMessage<Expr, ExprEF>,
574    {
575        let v_msg = msg().encode(self.challenges);
576        self.u += (v_msg - ExprEF::ONE) * flag.clone();
577        self.v -= flag;
578    }
579
580    fn insert<M>(
581        &mut self,
582        _name: &'static str,
583        flag: Expr,
584        multiplicity: Expr,
585        msg: impl FnOnce() -> M,
586        _deg: Deg,
587    ) where
588        M: LookupMessage<Expr, ExprEF>,
589    {
590        let v_msg = msg().encode(self.challenges);
591        self.u += (v_msg - ExprEF::ONE) * flag.clone();
592        self.v += flag * multiplicity;
593    }
594
595    fn batch<'b>(
596        &'b mut self,
597        _name: &'static str,
598        flag: Expr,
599        build: impl FnOnce(&mut Self::Batch<'b>),
600        _deg: Deg,
601    ) {
602        let mut batch = ValidationBatch {
603            challenges: self.challenges,
604            n: ExprEF::ZERO,
605            d: ExprEF::ONE,
606        };
607        build(&mut batch);
608        let ValidationBatch { n, d, .. } = batch;
609        self.u += (d - ExprEF::ONE) * flag.clone();
610        self.v += n * flag;
611    }
612
613    fn beta_powers(&self) -> &[ExprEF] {
614        &self.challenges.beta_powers[..]
615    }
616
617    fn bus_prefix(&self, bus_id: usize) -> ExprEF {
618        self.challenges.bus_prefix[bus_id].clone()
619    }
620
621    fn insert_encoded(
622        &mut self,
623        _name: &'static str,
624        flag: Expr,
625        multiplicity: Expr,
626        encoded: impl FnOnce() -> ExprEF,
627        _deg: Deg,
628    ) {
629        if !self.inside_encoded_closure {
630            self.used_insert_encoded = true;
631        }
632        let v_msg = encoded();
633        self.u += (v_msg - ExprEF::ONE) * flag.clone();
634        self.v += flag * multiplicity;
635    }
636}
637
638// BATCH
639// ================================================================================================
640
641pub struct ValidationBatch<'b> {
642    challenges: &'b Challenges<ExprEF>,
643    n: ExprEF,
644    d: ExprEF,
645}
646
647impl<'b> LookupBatch for ValidationBatch<'b> {
648    type Expr = Expr;
649    type ExprEF = ExprEF;
650
651    fn insert<M>(&mut self, _name: &'static str, multiplicity: Expr, msg: M, _deg: Deg)
652    where
653        M: LookupMessage<Expr, ExprEF>,
654    {
655        let v_msg = msg.encode(self.challenges);
656        let d_prev = self.d.clone();
657        self.n = self.n.clone() * v_msg.clone() + d_prev * multiplicity;
658        self.d = self.d.clone() * v_msg;
659    }
660
661    fn insert_encoded(
662        &mut self,
663        _name: &'static str,
664        multiplicity: Expr,
665        encoded: impl FnOnce() -> ExprEF,
666        _deg: Deg,
667    ) {
668        let v_msg = encoded();
669        let d_prev = self.d.clone();
670        self.n = self.n.clone() * v_msg.clone() + d_prev * multiplicity;
671        self.d = self.d.clone() * v_msg;
672    }
673}