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}