Skip to main content

midnight_circuits/parsing/scanner/
mod.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) 2025 Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! A chip combining lookup-based parsing techniques. The chip supports:
15//!
16//! - **Static automaton parsing** (`ScannerChip::parse`): verifies that a byte
17//!   sequence matches a regular expression, using a fixed lookup table
18//!   pre-loaded with transitions from a library of automata ([`StdLibParser`]),
19//!   and/or dynamically-provided regular expressions. See the `automaton_chip`
20//!   module for details.
21//!
22//! - **Substring checks** (`ScannerChip::check_bytes`): verifies that a
23//!   sub-sequence appears at a given position inside a larger sequence, using a
24//!   dynamic lookup argument. Calls are deferred and batched at the end of
25//!   circuit synthesis for efficiency. See the `substring` module for details.
26
27pub(crate) mod automaton;
28mod automaton_chip;
29/// A module to specify languages as regular expressions and convert them into
30/// finite automata.
31pub mod regex;
32mod serialization;
33pub mod static_specs;
34mod substring;
35mod substring_varlen;
36pub(crate) mod varlen;
37
38use std::{
39    cell::RefCell,
40    collections::{BTreeMap, BTreeSet},
41    rc::Rc,
42};
43
44use automaton::Automaton;
45use midnight_proofs::{
46    circuit::{Chip, Layouter},
47    plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, Selector, TableColumn},
48    poly::Rotation,
49};
50use regex::Regex;
51use rustc_hash::FxHashMap;
52pub use static_specs::StdLibParser;
53
54/// A test vector pairing an input with expected marker-grouped outputs.
55#[cfg(test)]
56pub(crate) type MarkerTestVector<'a> = (&'a [u8], &'a [(usize, &'a [u8])]);
57
58/// A test vector pairing an input with the expected raw output sequence.
59#[cfg(test)]
60#[allow(dead_code)]
61pub(crate) type OutputTestVector<'a> = (&'a [u8], &'a [usize]);
62#[cfg(test)]
63use {
64    crate::field::decomposition::chip::P2RDecompositionConfig,
65    crate::field::decomposition::pow2range::Pow2RangeChip, crate::field::native::NB_ARITH_COLS,
66    crate::testing_utils::FromScratch, midnight_proofs::plonk::Instance, regex::RegexInstructions,
67};
68
69use crate::{
70    field::{
71        decomposition::chip::P2RDecompositionChip, native::AssignedBit, AssignedNative, NativeChip,
72        NativeGadget,
73    },
74    utils::ComposableChip,
75    vec::vector_gadget::VectorGadget,
76    CircuitField,
77};
78
79/// Maximal size of the alphabet of an automaton/regex (input bytes are in
80/// `[0, 255]`). Also used to encode final states in the transition table as
81/// dummy transitions labelled with `ALPHABET_MAX_SIZE` (see the
82/// `automaton_chip` module), and as the packing shift for substring checks
83/// (see the `substring` module).
84const ALPHABET_MAX_SIZE: usize = 256;
85
86/// Number of advice columns used per automaton lookup (source, letter, output).
87const NB_AUTOMATON_COLS: usize = 3;
88/// Number of advice columns used per substring lookup argument (packed
89/// sequence+index, packed sub+index).
90const NB_SUBSTRING_COLS: usize = 2;
91
92/// Number of parallel lookups performed by automata based parsers.
93const AUTOMATON_PARALLELISM: usize = 2;
94/// Number of parallel query lookups for substring checks. The total advice
95/// columns is `(1 + SUBSTRING_PARALLELISM) * NB_SUBSTRING_COLS`.
96const SUBSTRING_PARALLELISM: usize = 3;
97
98/// Maximum bit-length for the longer sequence length in substring checks. This
99/// value must be chosen lower or equal than `F::CAPACITY - 9`.
100const PARSING_MAX_LEN_BITS: u32 = 64;
101
102/// Number of advice columns for the scanner chip.
103pub const NB_SCANNER_ADVICE_COLS: usize = {
104    let automaton = NB_AUTOMATON_COLS * AUTOMATON_PARALLELISM;
105    let substring = SUBSTRING_PARALLELISM * NB_SUBSTRING_COLS;
106    if automaton > substring {
107        automaton
108    } else {
109        substring
110    }
111};
112/// Number of shared fixed columns necessary for the scanner chip.
113pub const NB_SCANNER_FIXED_COLS: usize = 1;
114
115type NG<F> = NativeGadget<F, P2RDecompositionChip<F>, NativeChip<F>>;
116
117/// A simple map from the automaton structure to handle field elements, and thus
118/// precompute all transition operations on the prover code.
119#[derive(Clone, Debug)]
120pub struct NativeAutomaton<F> {
121    /// The number of states of the automaton.
122    pub nb_states: usize,
123    /// The initial state of the automaton.
124    pub initial_state: F,
125    /// The final states of the automaton.
126    pub final_states: BTreeSet<F>,
127    /// When `transitions[source_state][letter] = (target_state, output)`, it
128    /// means that in state `source_state`, upon reading the byte `letter`, the
129    /// automaton run moves to state `target_state` and tags `letter` with
130    /// `output`. If the entry is undefined, the automaton run gets stuck.
131    pub transitions: BTreeMap<F, BTreeMap<F, (F, F)>>,
132}
133
134impl<F> From<&Automaton> for NativeAutomaton<F>
135where
136    F: CircuitField + Ord,
137{
138    fn from(value: &Automaton) -> Self {
139        let mut transitions = BTreeMap::new();
140        for (&source, inner) in &value.transitions {
141            let native_inner: BTreeMap<F, (F, F)> = inner
142                .iter()
143                .map(|(&letter, &(target, output))| {
144                    (
145                        F::from(letter as u64),
146                        (F::from(target as u64), F::from(output as u64)),
147                    )
148                })
149                .collect();
150            transitions.insert(F::from(source as u64), native_inner);
151        }
152
153        let initial_state = F::from(value.initial_state as u64);
154        let final_states: BTreeSet<F> =
155            (value.final_states.iter()).map(|s| F::from(*s as u64)).collect();
156
157        // Self-loop transitions on the filler letter (ALPHABET_MAX_SIZE) for
158        // the initial and final states. These allow `parse_varlen` to skip
159        // filler elements in [`ScannerVec`] buffers, and are also loaded into
160        // the fixed lookup table unconditionally.
161        let filler = F::from(ALPHABET_MAX_SIZE as u64);
162        transitions
163            .entry(initial_state)
164            .or_default()
165            .insert(filler, (initial_state, F::ZERO));
166        for &state in &final_states {
167            transitions.entry(state).or_default().insert(filler, (state, F::ZERO));
168        }
169
170        NativeAutomaton {
171            nb_states: value.nb_states,
172            initial_state,
173            final_states,
174            transitions,
175        }
176    }
177}
178
179impl<F> From<Automaton> for NativeAutomaton<F>
180where
181    F: CircuitField + Ord,
182{
183    fn from(value: Automaton) -> Self {
184        (&value).into()
185    }
186}
187
188impl<F> NativeAutomaton<F>
189where
190    F: CircuitField + Ord,
191{
192    /// Looks up the transition from `state` reading `letter`.
193    fn get_transition(&self, state: &F, letter: &F) -> Option<(F, F)> {
194        self.transitions.get(state).and_then(|inner| inner.get(letter)).copied()
195    }
196}
197
198#[derive(Clone, Debug, PartialEq, Eq, Hash)]
199/// A reference for parsing methods for the function `parse`. Either an entry of
200/// the static automaton library (more efficient, but limited library), or a
201/// dynamic regular expression (more costly, but supports arbitrary regexes).
202pub enum AutomatonParser {
203    /// Static automaton library, as defined in `parsing::static_specs` (see the
204    /// documentation of each object of type `StdLibParser` to get the exact
205    /// regular expression they check). The off-circuit conversion
206    /// Regex->Automaton has been pre-computed and is serialised.
207    Static(StdLibParser),
208    /// Parses an arbitrary regular expression. Induces the same circuit logic
209    /// and performances as `Static`, but the conversion Regex->Automaton will
210    /// be performed by the prover (off-circuit).
211    Dynamic(Regex),
212}
213
214impl From<&StdLibParser> for AutomatonParser {
215    fn from(value: &StdLibParser) -> Self {
216        AutomatonParser::Static(*value)
217    }
218}
219
220impl From<StdLibParser> for AutomatonParser {
221    fn from(value: StdLibParser) -> Self {
222        AutomatonParser::from(&value)
223    }
224}
225
226impl From<Regex> for AutomatonParser {
227    fn from(value: Regex) -> Self {
228        AutomatonParser::Dynamic(value)
229    }
230}
231
232/// A static library of serialised automata for parsing common regexes. The
233/// automaton states start from 0 and may overlap one with each other.
234type ParsingLibrary = FxHashMap<StdLibParser, (Regex, Automaton)>;
235/// Set of automata (with offset states) called by [`ScannerChip::parse`] or
236/// [`ScannerChip::parse_varlen`].
237type AutomatonCache<F> = FxHashMap<AutomatonParser, NativeAutomaton<F>>;
238/// A sequence of assigned elements.
239type Sequence<F> = Vec<AssignedNative<F>>;
240
241/// Optional per-element padding flags for variable-length subs. When present,
242/// filler positions (flag = true) are masked to zero during packing.
243type PaddingFlags<F> = Option<Vec<AssignedBit<F>>>;
244
245/// A cached sub entry: (idx, index offsets, sub content, padding flags).
246/// Index offsets are additional `(coefficient, value)` terms folded into the
247/// packing linear combination alongside `idx`, saving a dedicated row per sub.
248type CachedSub<F> = (
249    AssignedNative<F>,
250    Vec<(F, AssignedNative<F>)>,
251    Sequence<F>,
252    PaddingFlags<F>,
253);
254
255/// Cache of assigned sequences passed as arguments to `check_subsequence` or
256/// `check_subsequence_varlen`. Each sequence (keyed by its cells) is mapped to
257/// the list of sub entries and the cumulative sub length.
258///
259/// **Cell-identity assumption**: the cache key is based on
260/// [`AssignedCell`](`midnight_proofs::circuit::AssignedCell`) identity (column
261/// and row), not on the cell's value. This means two sequences holding the same
262/// byte values but assigned at different circuit positions are distinct cache
263/// entries. Callers that introduce new ways to build sequences must ensure
264/// fresh cells are produced if a distinct cache entry is intended.
265type SequenceCache<F> = FxHashMap<Sequence<F>, (Vec<CachedSub<F>>, usize)>;
266
267/// Scanner gate configuration.
268#[derive(Clone, Debug)]
269pub struct ScannerConfig {
270    // Shared advice columns used by scanner operations.
271    advice_cols: [Column<Advice>; NB_SCANNER_ADVICE_COLS],
272
273    /// Pre-computed library of automata. If some are not used in the circuit,
274    /// their table will not be loaded wastingly.
275    static_library: ParsingLibrary,
276
277    // Automaton circuit resources.
278    q_automaton: Selector,
279    t_source: TableColumn,
280    t_letter: TableColumn,
281    t_target: TableColumn,
282    t_output: TableColumn,
283
284    // Substring resources. The tag column is used for domain separation and cannot be shared.
285    q_substring: Selector,
286    index_col: Column<Fixed>,
287    tag_col: Column<Fixed>,
288}
289
290/// Chip for scanning: automaton parsing and substring verification.
291#[derive(Clone, Debug)]
292pub struct ScannerChip<F>
293where
294    F: CircuitField,
295{
296    config: ScannerConfig,
297    native_gadget: NG<F>,
298    vector_gadget: VectorGadget<F>,
299
300    /// Unified cache of all resolved automata (both static library and dynamic
301    /// regexes), with their states already offset. Populated on demand by
302    /// `resolve_automaton` when `parse` is called for the first time with a
303    /// given `AutomatonParser`.
304    automaton_cache: Rc<RefCell<AutomatonCache<F>>>,
305    /// Tracks the next available state offset. Starts at 1 (state 0 is
306    /// reserved as the dummy state for soundness).
307    max_state: Rc<RefCell<usize>>,
308
309    /// Cache mapping a sequence of cells to the list of `(idx, sub)` pairs
310    /// it was called with, so that repeated `check_bytes` calls with the same
311    /// `sequence` argument share the table cost. Tags are assigned later
312    /// during finalisation.
313    sequence_cache: Rc<RefCell<SequenceCache<F>>>,
314}
315
316impl<F> ScannerChip<F>
317where
318    F: CircuitField,
319{
320    /// Gets the regex associated to a `StdLibParser`, as stored in the static
321    /// library of `self`.
322    pub fn specs_regex(&self, parser: &StdLibParser) -> &Regex {
323        let (regex, _) = self
324            .config
325            .static_library
326            .get(parser)
327            .unwrap_or_else(|| panic!("parser {:?} not found", parser));
328        regex
329    }
330}
331
332impl<F> Chip<F> for ScannerChip<F>
333where
334    F: CircuitField,
335{
336    type Config = ScannerConfig;
337    type Loaded = ();
338    fn config(&self) -> &Self::Config {
339        &self.config
340    }
341    fn loaded(&self) -> &Self::Loaded {
342        &()
343    }
344}
345
346impl<F> ComposableChip<F> for ScannerChip<F>
347where
348    F: CircuitField + Ord,
349{
350    type InstructionDeps = NG<F>;
351
352    type SharedResources = (
353        [Column<Advice>; NB_SCANNER_ADVICE_COLS],
354        Column<Fixed>,
355        FxHashMap<StdLibParser, (Regex, Automaton)>,
356    );
357
358    fn new(config: &ScannerConfig, deps: &Self::InstructionDeps) -> Self {
359        Self {
360            config: config.clone(),
361            vector_gadget: VectorGadget::new(deps),
362            native_gadget: deps.clone(),
363            automaton_cache: Rc::new(RefCell::new(FxHashMap::default())),
364            max_state: Rc::new(RefCell::new(1)),
365            sequence_cache: Rc::new(RefCell::new(FxHashMap::default())),
366        }
367    }
368
369    fn configure(
370        meta: &mut ConstraintSystem<F>,
371        shared_res: &Self::SharedResources,
372    ) -> ScannerConfig {
373        #[allow(clippy::assertions_on_constants)]
374        {
375            assert!(
376                AUTOMATON_PARALLELISM > 0 && SUBSTRING_PARALLELISM > 0,
377                "at least 1 lookup required for automata and substring checks"
378            );
379            assert!(
380                PARSING_MAX_LEN_BITS <= F::CAPACITY - (u8::BITS + 1),
381                "check_subsequence batching exceeds field capacity ({} / {})",
382                PARSING_MAX_LEN_BITS + u8::BITS + 1,
383                F::CAPACITY
384            )
385        }
386
387        let (advice_cols, index_col, automata) = shared_res;
388
389        // Enable equality on all advice columns.
390        for &col in advice_cols {
391            meta.enable_equality(col);
392        }
393
394        // Automaton resources (shared fixed lookup table).
395        let q_automaton = meta.complex_selector();
396        let t_source = meta.lookup_table_column();
397        let t_letter = meta.lookup_table_column();
398        let t_target = meta.lookup_table_column();
399        let t_output = meta.lookup_table_column();
400
401        // Automaton lookup by batch: AUTOMATON_PARALLELISM transitions per row.
402        for batch in 0..AUTOMATON_PARALLELISM {
403            meta.lookup(
404                format!("automaton transition check (batch {batch})"),
405                |meta| {
406                    let q = meta.query_selector(q_automaton);
407                    let base = NB_AUTOMATON_COLS * batch;
408                    let [source, letter, output] = core::array::from_fn(|i| {
409                        meta.query_advice(advice_cols[base + i], Rotation::cur())
410                    });
411                    let target = if batch + 1 < AUTOMATON_PARALLELISM {
412                        meta.query_advice(advice_cols[base + NB_AUTOMATON_COLS], Rotation::cur())
413                    } else {
414                        meta.query_advice(advice_cols[0], Rotation::next())
415                    };
416                    vec![
417                        (q.clone() * source, t_source),
418                        (q.clone() * letter, t_letter),
419                        (q.clone() * target, t_target),
420                        (q * output, t_output),
421                    ]
422                },
423            );
424        }
425
426        // Substring resources.
427        let tag_col = meta.fixed_column();
428        let q_substring = meta.complex_selector();
429
430        // Substring lookup arguments (see the `substring` module for full details).
431        //
432        // There are `SUBSTRING_PARALLELISM` independent lookup arguments, each
433        // operating on 2 advice columns: `advice_cols[2*batch]` (table byte)
434        // and `advice_cols[2*batch + 1]` (packed query), plus 2 shared fixed
435        // columns: index and tag.
436        //
437        // Example: checking `wor` appears in `hello world` at index 6 (batch 0):
438        //
439        //    fixed          advice
440        //    tag | index    cols[2*i] | cols[2*i+1]
441        //    -----------    -----------------------
442        //     1  |  0          'h'    | 257*6 + 'w'    <- query for sub[0]
443        //     1  |  1          'e'    | 257*7 + 'o'    <- query for sub[1]
444        //     1  |  2          'l'    | 257*8 + 'r'    <- query for sub[2]
445        //     1  |  3          'l'    | (padding)
446        //     1  |  4          'o'    | (padding)
447        //     1  |  5          ' '    | (padding)
448        //     1  |  6          'w'    | (padding)
449        //     1  |  7          'o'    | (padding)
450        //     1  |  8          'r'    | (padding)
451        //     1  |  9          'l'    | (padding)
452        //     1  | 10          'd'    | (padding)
453        //
454        // The packed query `257 * (idx + i) + sub[i]` is pre-computed in
455        // circuit (see `index_and_pack_sequence` in `substring`). The table
456        // packing is done in the expression below:
457        //
458        //     table_packed = 257 * index + table_byte
459        //
460        // The lookup checks: (tag, packed_query) ∈ {(tag, table_packed)}.
461        //
462        // When sel=OFF, both sides reduce to (tag, query), i.e., a tautology, so rows
463        // not used by substring checks are unconstrained.
464        //
465        // Invariant: the tag column is 0 on every row that is not part of a substring
466        // check region, and non-zero (a unique positive integer) inside each region.
467        // This isolates independent substring checks from each other and from
468        // unrelated rows: a query tagged T can only match table entries with the
469        // same tag T, and rows with tag 0 never participate in any lookup.
470        for batch in 0..SUBSTRING_PARALLELISM {
471            meta.lookup_any(format!("substring lookup (batch {batch})"), |meta| {
472                let sel = meta.query_selector(q_substring);
473                let not_sel = Expression::Constant(F::ONE) - sel.clone();
474                let index = meta.query_fixed(*index_col, Rotation::cur());
475                let tag = meta.query_fixed(tag_col, Rotation::cur());
476                let shift = Expression::Constant(F::from(ALPHABET_MAX_SIZE as u64 + 1));
477
478                let base = NB_SUBSTRING_COLS * batch;
479                let table = meta.query_advice(advice_cols[base], Rotation::cur());
480                let query = meta.query_advice(advice_cols[base + 1], Rotation::cur());
481
482                vec![
483                    (tag.clone(), sel.clone() * tag.clone()),
484                    (
485                        query.clone(),
486                        sel * (index * shift + table) + not_sel * query,
487                    ),
488                ]
489            });
490        }
491
492        ScannerConfig {
493            advice_cols: *advice_cols,
494            static_library: automata.clone(),
495            q_automaton,
496            t_source,
497            t_letter,
498            t_target,
499            t_output,
500            q_substring,
501            index_col: *index_col,
502            tag_col,
503        }
504    }
505
506    /// Loads the automaton transition table and finalises all deferred
507    /// substring checks. Must be called at the end of circuit synthesis.
508    fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
509        self.load_automata_table(layouter)?;
510        self.finalise_substring_checks(layouter)
511    }
512}
513
514#[cfg(test)]
515impl Regex {
516    // "hello hello [...] hello \( world , world , [...] , world \) !!!!!" with
517    // 1. arbitrary spaces whenever there is one
518    // 2. at least one "hello" and one "world"
519    // 3. an arbitrary sequence of characters different from '!' at the end of the
520    //    string.
521    // The definition of the regex purposely performs some non succinct operations
522    // to test several constructions of the library.
523    fn hard_coded_example0() -> Self {
524        let hellos = Regex::word("hello").separated_non_empty_list(Regex::blanks_strict());
525        let worlds = Regex::word("world").separated_non_empty_list(Regex::cat([
526            Regex::blanks(),
527            ",".into(),
528            Regex::blanks(),
529        ]));
530        let marks5 = Regex::word("!").repeat(5);
531        let trail = Regex::any_byte().minus("!".into()).list();
532        Regex::separated_cat(
533            [
534                hellos.terminated(Regex::one_blank()),
535                worlds
536                    .delimited(Regex::blanks(), Regex::blanks())
537                    .delimited("(".into(), ")".into()),
538                marks5,
539                trail,
540            ],
541            Regex::blanks(),
542        )
543    }
544
545    fn hard_coded_example1() -> Self {
546        // `output_regex` accepts any character, marking 'l' as 2, and
547        // any other non-blank character different from 'h' as 1.
548        let output_regex = Regex::any_byte()
549            .output(&|b| {
550                if b == b'l' {
551                    Some(2)
552                } else if !b"h\n\t ".contains(&b) {
553                    Some(1)
554                } else {
555                    None
556                }
557            })
558            .list();
559        let holy = Regex::word("holy").terminated(Regex::word("y").list());
560        let hell = Regex::word("hell");
561        let marks = Regex::word("!").non_empty_list();
562        let sentence = Regex::separated_cat([holy, hell, marks], Regex::blanks_strict());
563        sentence.and(output_regex)
564    }
565}
566
567#[cfg(test)]
568impl<F> FromScratch<F> for ScannerChip<F>
569where
570    F: CircuitField + Ord,
571{
572    type Config = (P2RDecompositionConfig, ScannerConfig);
573
574    fn new_from_scratch(config: &Self::Config) -> Self {
575        let max_bit_len = 8;
576        let native_chip = NativeChip::new(&config.0.native_config, &());
577        let core_decomposition_chip = P2RDecompositionChip::new(&config.0, &max_bit_len);
578        let native_gadget = NG::<F>::new(core_decomposition_chip, native_chip);
579        <ScannerChip<F> as ComposableChip<F>>::new(&config.1, &native_gadget)
580    }
581
582    fn configure_from_scratch(
583        meta: &mut ConstraintSystem<F>,
584        advice_columns: &mut Vec<Column<Advice>>,
585        fixed_columns: &mut Vec<Column<Fixed>>,
586        instance_columns: &[Column<Instance>; 2],
587    ) -> Self::Config {
588        let nb_advice_needed = std::cmp::max(NB_SCANNER_ADVICE_COLS, NB_ARITH_COLS);
589        let nb_fixed_needed = NB_ARITH_COLS + 4;
590        while advice_columns.len() < nb_advice_needed {
591            advice_columns.push(meta.advice_column());
592        }
593        while fixed_columns.len() < nb_fixed_needed {
594            fixed_columns.push(meta.fixed_column());
595        }
596        let advice_cols = &advice_columns[..nb_advice_needed];
597        let fixed_cols = &fixed_columns[..nb_fixed_needed];
598
599        let native_config = NativeChip::configure(
600            meta,
601            &(
602                advice_cols[..NB_ARITH_COLS].try_into().unwrap(),
603                fixed_cols[..NB_ARITH_COLS + 4].try_into().unwrap(),
604                *instance_columns,
605            ),
606        );
607
608        let scanner_config = ScannerChip::configure(
609            meta,
610            &(
611                advice_cols[..NB_SCANNER_ADVICE_COLS].try_into().unwrap(),
612                fixed_cols[0],
613                FxHashMap::default(),
614            ),
615        );
616
617        let pow2range_config = Pow2RangeChip::configure(meta, &advice_cols[1..=4]);
618
619        let native_gadget_config = P2RDecompositionConfig {
620            native_config,
621            pow2range_config,
622        };
623
624        (native_gadget_config, scanner_config)
625    }
626
627    fn load_from_scratch(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
628        self.native_gadget.load_from_scratch(layouter)?;
629        self.load(layouter)
630    }
631}