Skip to main content

miden_air/lookup/
builder.rs

1//! Closure-based builder traits for the LogUp lookup-argument API.
2//!
3//! The lookup-air refactor introduces the trait stack that sits on top of
4//! `LookupAir`:
5//!
6//! - [`LookupBuilder`] — the top-level handle mirroring the subset of `LiftedAirBuilder` a lookup
7//!   author actually needs (trace access plus per-column scoping). It hides `assert_*` / `when_*` /
8//!   permutation plumbing and does not expose the verifier challenges.
9//! - [`LookupColumn`] — per-column handle returned by [`LookupBuilder::next_column`]. It owns the
10//!   boundary between groups; its only job is to open a group (either the simple path or the
11//!   cached-encoding dual path).
12//! - [`LookupGroup`] — the simple, challenge-free interaction API used by bus authors. Every method
13//!   here takes a `LookupMessage`; the enclosing adapter is responsible for encoding it under α + Σ
14//!   βⁱ · payload.
15//! - [`LookupBatch`] — a short-lived handle returned inside [`LookupGroup::batch`]. Represents a
16//!   set of simultaneous interactions that share the outer group's flag.
17//! - [`LookupGroup`] also exposes optional encoding primitives (`bus_prefix`, `beta_powers`,
18//!   `insert_encoded`) for the cached-encoding path. Default implementations panic; only the
19//!   constraint-path adapter overrides them with real bodies.
20//!
21//! No adapter impls live in this file; the bounds here are chosen so that both the
22//! constraint-path adapter (which forwards to an inner `LiftedAirBuilder`, carrying
23//! symbolic `AB::Expr` / `AB::ExprEF` associated types) and the prover-path adapter
24//! (instantiated with the concrete `F` / `EF` field types) can satisfy them.
25
26use miden_core::field::{Algebra, ExtensionField, Field, PrimeCharacteristicRing};
27use miden_crypto::stark::air::WindowAccess;
28
29use super::message::LookupMessage;
30
31// DEGREE ANNOTATION
32// ================================================================================================
33
34/// Expected post-flag `(V, U)` contribution for one interaction or scope.
35///
36/// Every builder method takes a `Deg` as its last argument so authors can
37/// declare the expected degrees inline. Production adapters ignore the value
38/// (it is `Copy` and dead-code-eliminated after inlining). A debug adapter
39/// can compare the declared degrees against the symbolic expression it just
40/// accumulated and panic with the interaction's `name` if they disagree.
41///
42/// - `v`: degree of the numerator (`V`) contribution after multiplying by the surrounding flag.
43/// - `u`: degree of the denominator (`U`) contribution after multiplying by the surrounding flag.
44///
45/// Field order mirrors the `(V, U)` tuple convention used throughout the
46/// adapter code: numerator first, denominator second.
47///
48/// ## Semantics by call site
49///
50/// - **Single interactions** ([`LookupGroup::add`], `remove`, `insert`, `insert_encoded`, and the
51///   corresponding [`LookupBatch`] methods): `(v, u) = (deg(m · D) + deg(f), deg(D) + deg(f))`
52///   where `m` is the interaction's signed multiplicity, `D` is its denominator polynomial, and `f`
53///   is the gating flag (the enclosing batch flag for `LookupBatch` methods). The standalone
54///   post-flag contribution this interaction would make if folded directly into the enclosing
55///   group's `(V_g, U_g)`.
56///
57/// - **Batch outer** ([`LookupGroup::batch`]): the post-flag contribution the *whole* batch makes
58///   to the enclosing group's `(V_g, U_g)` — `(deg(N) + deg(f), deg(D) + deg(f))` where `(N, D)` is
59///   the running pair the inner-loop body accumulates and `f` is the batch flag. The pre-flag `(N,
60///   D)` is mechanically derivable from the inner-loop body — `((k − 1) · d_v, k · d_v)` for `k`
61///   interactions of inner denominator degree `d_v` — so it is documented inline at each batch site
62///   rather than carried in the struct.
63///
64/// - **Group / column scope** ([`LookupColumn::group`], `group_with_cached_encoding`,
65///   [`LookupBuilder::next_column`]): the total post-flag `(V, U)` contribution of the group /
66///   column to the surrounding accumulator. Useful as a budget audit number when the author wants
67///   to assert what the scope as a whole contributes.
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
69pub struct Deg {
70    pub v: usize,
71    pub u: usize,
72}
73
74// LOOKUP BUILDER
75// ================================================================================================
76
77/// The trace-reading handle handed to a [`super::LookupAir`] implementation.
78///
79/// `LookupBuilder` deliberately mirrors the subset of `LiftedAirBuilder`'s
80/// associated types needed to read `main` and `periodic_values`. It is
81/// **not** a sub-trait of `AirBuilder`: the constraint
82/// emission surface (`assert_zero` / `when_first_row` / …) and the
83/// permutation column plumbing stay hidden, which keeps the simple lookup
84/// path free of challenge access.
85///
86/// Implementors must not shortcut the per-column scoping: a [`super::LookupAir`]
87/// author that opens `n` columns must issue exactly `n` calls to
88/// [`LookupBuilder::next_column`], matching [`super::LookupAir::num_columns`].
89///
90/// ## Associated-type layout
91///
92/// The base-field stack (`F`, `Expr`, `Var`) and extension-field stack
93/// (`EF`, `ExprEF`, `VarEF`) mirror the upstream `AirBuilder` /
94/// `ExtensionBuilder` split one-for-one; `Algebra<Var>` on `Expr` lets the
95/// lookup author multiply main-trace variables with arbitrary expressions
96/// without crossing trait boundaries. `PeriodicVar` / `MainWindow` come
97/// from `PeriodicAirBuilder` / `AirBuilder` respectively and are passed
98/// through the adapter unchanged.
99///
100/// The per-column handle is a generic associated type
101/// ([`Self::Column`](Self::Column)) so that each `column(...)` call can
102/// borrow from `self` without outliving the closure. Its bound pins the
103/// expression and extension-variable types to keep them in sync with the
104/// outer builder.
105pub trait LookupBuilder: Sized {
106    // --- base field stack (copied from AirBuilder) ---
107
108    /// Underlying base field. Lookups only pin `Field` here (not the wider
109    /// `PrimeCharacteristicRing`) because the extension-field associated
110    /// types below require an `ExtensionField<Self::F>` relationship, and
111    /// `ExtensionField` itself bounds on `Field`.
112    type F: Field;
113
114    /// Expression type over base-field elements. Must be an algebra over
115    /// both `Self::F` (for constants) and `Self::Var` (for trace
116    /// variables), matching upstream `AirBuilder::Expr`.
117    type Expr: Algebra<Self::F> + Algebra<Self::Var>;
118
119    /// Variable type over base-field trace cells. Held by value, so bound
120    /// only by `Into<Self::Expr> + Copy + Send + Sync`; the full arithmetic
121    /// bound soup from `AirBuilder::Var` is not required here because the
122    /// `Algebra<Self::Var>` bound on `Expr` lets callers convert before
123    /// composing.
124    type Var: Into<Self::Expr> + Copy + Send + Sync;
125
126    // --- extension field stack (copied from ExtensionBuilder) ---
127
128    /// Extension field used by the auxiliary trace and the LogUp
129    /// accumulators.
130    type EF: ExtensionField<Self::F>;
131
132    /// Expression type over extension-field elements; must be an algebra
133    /// over both `Self::Expr` (to lift base expressions) and `Self::EF`
134    /// (for extension-field constants).
135    type ExprEF: Algebra<Self::Expr> + Algebra<Self::EF>;
136
137    /// Variable type over extension-field trace cells (permutation
138    /// columns and the α/β challenges).
139    type VarEF: Into<Self::ExprEF> + Copy + Send + Sync;
140
141    // --- auxiliary trace access types ---
142
143    /// Periodic column value at the current row (copied from
144    /// `PeriodicAirBuilder::PeriodicVar`).
145    type PeriodicVar: Into<Self::Expr> + Copy;
146
147    /// Two-row window over the main trace, returned as-is from the
148    /// underlying builder. Pinned to [`WindowAccess`] + `Clone` so a
149    /// lookup author can split it into `current_slice()` / `next_slice()`
150    /// and pass either to `borrow`-based view types without re-reading
151    /// the handle.
152    type MainWindow: WindowAccess<Self::Var> + Clone;
153
154    /// Per-column handle opened by [`Self::next_column`]. Holds the adapter's per-column
155    /// state (running `(V, U)` on the constraint path, fraction collector on the prover
156    /// path) for the column's closure.
157    type Column<'a>: LookupColumn<Expr = Self::Expr, ExprEF = Self::ExprEF>
158    where
159        Self: 'a;
160
161    // ---- trace access ----
162
163    /// Two-row main trace window. Pass-through to the wrapped builder.
164    fn main(&self) -> Self::MainWindow;
165
166    /// Periodic column values at the current row.
167    fn periodic_values(&self) -> &[Self::PeriodicVar];
168
169    // ---- per-column scoping ----
170
171    /// Open a fresh permutation column and evaluate `f` inside it.
172    ///
173    /// The implementation is responsible for:
174    ///
175    /// 1. Wiring the column handle to the adapter's internal state (current `acc` / `acc_next` for
176    ///    the constraint path; the per-column fraction buffer slot for the prover path).
177    /// 2. Running the closure, which must describe at least one group via [`LookupColumn::group`]
178    ///    or [`LookupColumn::group_with_cached_encoding`].
179    /// 3. Finalizing the column on close (emitting boundary + transition constraints, or draining
180    ///    the column's fraction pair).
181    /// 4. Advancing to the next permutation column index so the next call targets a fresh
182    ///    accumulator.
183    ///
184    /// The closure's return value `R` is forwarded unchanged.
185    fn next_column<'a, R>(&'a mut self, f: impl FnOnce(&mut Self::Column<'a>) -> R, deg: Deg) -> R;
186}
187
188// LOOKUP COLUMN
189// ================================================================================================
190
191/// Per-column handle returned by [`LookupBuilder::next_column`].
192///
193/// The only decision a column makes is how to open a group: either the
194/// simple path via [`group`](Self::group) or the dual cached-encoding path
195/// via [`group_with_cached_encoding`](Self::group_with_cached_encoding).
196///
197/// Multiple groups may be opened per column; the adapter is responsible
198/// for composing them according to the column accumulator algebra
199/// (`V ← V·U_g + V_g·U`, `U ← U·U_g`). Groups opened inside the same
200/// column are assumed *product-closed*, not mutually exclusive.
201pub trait LookupColumn {
202    /// Expression type over base-field elements. Pinned to
203    /// [`LookupBuilder::Expr`] through [`LookupBuilder::Column`].
204    type Expr: PrimeCharacteristicRing + Clone;
205
206    /// Expression type over extension-field elements. Pinned to
207    /// [`LookupBuilder::ExprEF`] through [`LookupBuilder::Column`]. The
208    /// [`Algebra<Self::Expr>`] bound lets [`LookupMessage::encode`]
209    /// multiply an `Expr`-typed payload slot by an `ExprEF`-typed
210    /// β-power without manually lifting.
211    type ExprEF: PrimeCharacteristicRing + Clone + Algebra<Self::Expr>;
212
213    /// Per-group handle used for the simple (challenge-free) path.
214    type Group<'a>: LookupGroup<Expr = Self::Expr, ExprEF = Self::ExprEF>
215    where
216        Self: 'a;
217
218    /// Open a group using the simple, challenge-free API.
219    ///
220    /// Every interaction added inside the closure is folded into this
221    /// group's `(V_g, U_g)` pair; on close, the column composes the pair
222    /// into its running accumulator.
223    fn group<'a>(&'a mut self, name: &'static str, f: impl FnOnce(&mut Self::Group<'a>), deg: Deg);
224
225    /// Open a group with two sibling descriptions for the same
226    /// interaction set.
227    ///
228    /// - `canonical` runs on the prover path. It sees the simple [`LookupGroup`] surface — no
229    ///   challenges, no `insert_encoded`. Zero-valued flag closures are skipped by the backing
230    ///   fraction collector.
231    /// - `encoded` runs on the constraint path. It sees the same [`LookupGroup`] surface, plus the
232    ///   encoding primitives `beta_powers()`, `bus_prefix()`, and `insert_encoded()`. Authors use
233    ///   this to precompute shared encoding fragments (e.g. a common `α + β·addr` prefix) and reuse
234    ///   them across mutually-exclusive variants.
235    ///
236    /// Both closures must produce mathematically identical `(V, U)`
237    /// pairs; the split is purely an optimization for expensive
238    /// extension-field arithmetic on the symbolic path. Adapters are
239    /// free to drop whichever closure they do not use.
240    fn group_with_cached_encoding<'a>(
241        &'a mut self,
242        name: &'static str,
243        canonical: impl FnOnce(&mut Self::Group<'a>),
244        encoded: impl FnOnce(&mut Self::Group<'a>),
245        deg: Deg,
246    );
247}
248
249// LOOKUP GROUP
250// ================================================================================================
251
252/// Simple, challenge-free interaction API opened inside a
253/// [`LookupColumn`].
254///
255/// Authors call `add` / `remove` / `insert` to describe one flag-gated
256/// interaction at a time, or `batch` to describe several simultaneous
257/// interactions that share a single outer flag.
258///
259/// All methods take the message through an `impl FnOnce() -> M` closure
260/// so the prover-path adapter can skip the construction (and any
261/// expensive derivation) when `flag == 0`.
262pub trait LookupGroup {
263    /// Expression type over base-field elements. Pinned to
264    /// [`LookupBuilder::Expr`] through the column. The
265    /// `PrimeCharacteristicRing` bound keeps [`LookupMessage`] happy when
266    /// authors pass messages through `add` / `remove` / `insert`.
267    type Expr: PrimeCharacteristicRing + Clone;
268
269    /// Expression type over extension-field elements. Pinned to
270    /// [`LookupBuilder::ExprEF`] through the column. The
271    /// [`Algebra<Self::Expr>`] bound mirrors [`LookupColumn::ExprEF`]
272    /// and lets [`LookupMessage::encode`] use `ExprEF × Expr` products.
273    type ExprEF: PrimeCharacteristicRing + Clone + Algebra<Self::Expr>;
274
275    /// Transient handle returned by [`batch`](Self::batch). GAT so the
276    /// batch can borrow from `self` (and therefore from the column and
277    /// the outer builder) for the duration of the closure.
278    type Batch<'b>: LookupBatch<Expr = Self::Expr, ExprEF = Self::ExprEF>
279    where
280        Self: 'b;
281
282    /// Add a single interaction with multiplicity `+1`, gated by `flag`.
283    ///
284    /// `msg` is deferred so the adapter can skip both the construction
285    /// and the encoding when `flag == 0` on the prover path.
286    ///
287    /// The default delegates to [`insert`](Self::insert) with multiplicity `ONE`.
288    /// Adapters may override for optimization (e.g. the constraint path avoids
289    /// the redundant `flag * ONE` symbolic node).
290    fn add<M>(&mut self, name: &'static str, flag: Self::Expr, msg: impl FnOnce() -> M, deg: Deg)
291    where
292        M: LookupMessage<Self::Expr, Self::ExprEF>,
293    {
294        self.insert(name, flag, Self::Expr::ONE, msg, deg);
295    }
296
297    /// Add a single interaction with multiplicity `-1`, gated by `flag`.
298    ///
299    /// The default delegates to [`insert`](Self::insert) with multiplicity `NEG_ONE`.
300    fn remove<M>(&mut self, name: &'static str, flag: Self::Expr, msg: impl FnOnce() -> M, deg: Deg)
301    where
302        M: LookupMessage<Self::Expr, Self::ExprEF>,
303    {
304        self.insert(name, flag, Self::Expr::NEG_ONE, msg, deg);
305    }
306
307    /// Add a single interaction with explicit signed multiplicity, gated
308    /// by `flag`.
309    ///
310    /// `multiplicity` is a base-field expression so callers can mix
311    /// trace columns, constants, and boolean selectors freely.
312    fn insert<M>(
313        &mut self,
314        name: &'static str,
315        flag: Self::Expr,
316        multiplicity: Self::Expr,
317        msg: impl FnOnce() -> M,
318        deg: Deg,
319    ) where
320        M: LookupMessage<Self::Expr, Self::ExprEF>;
321
322    /// Open a batch of simultaneous interactions that all share the
323    /// single outer flag `flag`.
324    ///
325    /// Inside the closure, messages are passed by value (see
326    /// [`LookupBatch`]): the flag-zero skip is handled once at the batch
327    /// level, so per-interaction closures are redundant.
328    ///
329    /// Multiple batches inside the same [`LookupGroup`] are **not**
330    /// checked for mutual exclusion; adapters assume the author upholds
331    /// this invariant (matching the existing `RationalSet` contract).
332    fn batch<'a>(
333        &'a mut self,
334        name: &'static str,
335        flag: Self::Expr,
336        build: impl FnOnce(&mut Self::Batch<'a>),
337        deg: Deg,
338    );
339
340    // ---- encoding primitives (cached-encoding path only) ----
341
342    /// Precomputed powers `[β⁰, β¹, …, β^(W-1)]`, where
343    /// `W = max_message_width` from the enclosing
344    /// [`LookupAir`](super::LookupAir).
345    ///
346    /// The slice length is exactly `W` — there is **no** trailing `β^W`
347    /// entry, because that power is the per-bus step baked into every
348    /// [`Challenges::bus_prefix`](super::Challenges) entry
349    /// at builder-construction time. Authors that want to build their
350    /// own encoded denominator loop should iterate over `beta_powers()`
351    /// directly and slice to their own message width.
352    ///
353    /// Returned as extension-field expressions; the adapter materializes
354    /// the powers once at construction time (as `AB::ExprEF` on the
355    /// constraint path) and serves them back by reference.
356    ///
357    /// # Panics
358    ///
359    /// Default implementation panics — only valid inside the `encoded`
360    /// closure of [`LookupColumn::group_with_cached_encoding`].
361    fn beta_powers(&self) -> &[Self::ExprEF] {
362        panic!(
363            "beta_powers() is only available inside the `encoded` closure of group_with_cached_encoding"
364        )
365    }
366
367    /// Look up the precomputed bus prefix
368    /// `bus_prefix[bus_id] = α + (bus_id + 1) · β^W` for the given
369    /// coarse bus ID.
370    ///
371    /// Returns an owned [`Self::ExprEF`] by cloning the entry — the
372    /// underlying storage is a `Box<[ExprEF]>` on the adapter and
373    /// `ExprEF` is typically a ring element, so cloning is cheap.
374    ///
375    /// # Panics
376    ///
377    /// Default implementation panics — only valid inside the `encoded`
378    /// closure of [`LookupColumn::group_with_cached_encoding`].
379    /// Also panics if `bus_id` is out of bounds of the adapter's
380    /// `num_bus_ids`.
381    fn bus_prefix(&self, bus_id: usize) -> Self::ExprEF {
382        let _ = bus_id;
383        panic!(
384            "bus_prefix() is only available inside the `encoded` closure of group_with_cached_encoding"
385        )
386    }
387
388    /// Add a flag-gated interaction whose denominator is already an
389    /// extension-field expression.
390    ///
391    /// - `flag`: base-field selector. Zero flags are skipped by the prover-path adapter
392    ///   (constraint-path evaluates unconditionally).
393    /// - `multiplicity`: base-field signed multiplicity.
394    /// - `encoded`: closure producing the final denominator. Run once on the constraint path. On
395    ///   the prover path the adapter may skip the call entirely when `flag == 0`.
396    ///
397    /// # Panics
398    ///
399    /// Default implementation panics — only valid inside the `encoded`
400    /// closure of [`LookupColumn::group_with_cached_encoding`].
401    fn insert_encoded(
402        &mut self,
403        _name: &'static str,
404        _flag: Self::Expr,
405        _multiplicity: Self::Expr,
406        _encoded: impl FnOnce() -> Self::ExprEF,
407        _deg: Deg,
408    ) {
409        panic!(
410            "insert_encoded() is only available inside the `encoded` closure of group_with_cached_encoding"
411        )
412    }
413}
414
415// LOOKUP BATCH
416// ================================================================================================
417
418/// Transient handle exposed inside [`LookupGroup::batch`].
419///
420/// A batch groups several simultaneously-active interactions under a
421/// single outer flag, emitted by the enclosing group. The flag-zero skip
422/// is performed once by the group when the batch opens, so within the
423/// batch the message can be built unconditionally and is taken by value
424/// (not through a closure).
425///
426/// Kept as a separate trait rather than a concrete helper struct because
427/// the constraint-path and prover-path adapters need different backing
428/// storage (`RationalSet` vs `FractionCollector`) and expressing that
429/// split through a GAT on [`LookupGroup::Batch`] is cleaner than bolting
430/// a second generic parameter onto a shared struct.
431pub trait LookupBatch {
432    /// Expression type over base-field elements. Must match the
433    /// enclosing group's `Expr`. `PrimeCharacteristicRing` is required
434    /// by [`LookupMessage`] (passed by value into the `add` / `remove` /
435    /// `insert` methods below).
436    type Expr: PrimeCharacteristicRing + Clone;
437
438    /// Expression type over extension-field elements. Must match the
439    /// enclosing group's `ExprEF` — [`LookupMessage::encode`] returns an
440    /// extension-field value and the batch's underlying algebra operates
441    /// on that type. The [`Algebra<Self::Expr>`] bound mirrors the
442    /// enclosing group's `ExprEF` bound.
443    type ExprEF: PrimeCharacteristicRing + Clone + Algebra<Self::Expr>;
444
445    /// Absorb an interaction with multiplicity `+1`.
446    ///
447    /// The default delegates to [`insert`](Self::insert) with multiplicity `ONE`.
448    fn add<M>(&mut self, name: &'static str, msg: M, deg: Deg)
449    where
450        M: LookupMessage<Self::Expr, Self::ExprEF>,
451    {
452        self.insert(name, Self::Expr::ONE, msg, deg);
453    }
454
455    /// Absorb an interaction with multiplicity `-1`.
456    ///
457    /// The default delegates to [`insert`](Self::insert) with multiplicity `NEG_ONE`.
458    fn remove<M>(&mut self, name: &'static str, msg: M, deg: Deg)
459    where
460        M: LookupMessage<Self::Expr, Self::ExprEF>,
461    {
462        self.insert(name, Self::Expr::NEG_ONE, msg, deg);
463    }
464
465    /// Absorb an interaction with arbitrary signed multiplicity.
466    fn insert<M>(&mut self, name: &'static str, multiplicity: Self::Expr, msg: M, deg: Deg)
467    where
468        M: LookupMessage<Self::Expr, Self::ExprEF>;
469
470    /// Absorb an interaction with an already-encoded denominator.
471    fn insert_encoded(
472        &mut self,
473        name: &'static str,
474        multiplicity: Self::Expr,
475        encoded: impl FnOnce() -> Self::ExprEF,
476        deg: Deg,
477    );
478}
479
480// BOUNDARY BUILDER
481// ================================================================================================
482
483/// Handle for emitting **once-per-proof** "outer" interactions — contributions to the
484/// LogUp sum that are not tied to any main-trace row.
485///
486/// Typical sources are committed-final boundary terminals (kernel ROM init, block hash
487/// seed, log-precompile terminals, public-input bus seeds). Each emission contributes
488/// one signed fraction to the overall balance; no column / row / group scoping, no
489/// flag gating, no `Deg` (boundary terms are plain field elements, not polynomials).
490///
491/// Used by [`super::LookupAir::eval_boundary`]. Default implementations on the trait
492/// are a no-op, so AIRs with no boundary contributions don't need to override it.
493pub trait BoundaryBuilder {
494    /// Base field for boundary-interaction multiplicities and encoded message slots.
495    type F: Field;
496
497    /// Extension field used by [`LookupMessage::encode`] — matches the enclosing
498    /// `LookupAir`'s `LB::EF`.
499    type EF: ExtensionField<Self::F>;
500
501    /// Public values passed to the proof (the `public_values` slice threaded through
502    /// `prove_stark`).
503    fn public_values(&self) -> &[Self::F];
504
505    /// Variable-length public inputs (e.g. kernel felts). Matches the layout the
506    /// prover hands to `miden_crypto::stark::prover::prove_single`.
507    fn var_len_public_inputs(&self) -> &[&[Self::F]];
508
509    /// Emit a boundary interaction with multiplicity `+1`.
510    ///
511    /// The default delegates to [`insert`](Self::insert) with multiplicity `ONE`.
512    fn add<M>(&mut self, name: &'static str, msg: M)
513    where
514        M: LookupMessage<Self::F, Self::EF>,
515    {
516        self.insert(name, Self::F::ONE, msg);
517    }
518
519    /// Emit a boundary interaction with multiplicity `-1`.
520    ///
521    /// The default delegates to [`insert`](Self::insert) with multiplicity `NEG_ONE`.
522    fn remove<M>(&mut self, name: &'static str, msg: M)
523    where
524        M: LookupMessage<Self::F, Self::EF>,
525    {
526        self.insert(name, Self::F::NEG_ONE, msg);
527    }
528
529    /// Emit a boundary interaction with an arbitrary signed multiplicity.
530    fn insert<M>(&mut self, name: &'static str, multiplicity: Self::F, msg: M)
531    where
532        M: LookupMessage<Self::F, Self::EF>;
533}