From b6be6e7ebbaad0b055a6373841c5bd58f79ff981 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Tue, 1 Sep 2026 20:01:28 -0500 Subject: [PATCH 1/2] compile: eliminate quadratic environment rebuilds in Scope::get Every variable read in codegen re-folded all live bindings into the input pattern, re-converted it, and re-walked it to find the variable: O(n) work per read and O(n^2) per program, even though the generated program is only linear in size. A 4096-binding program spent ~3.9s in codegen. Scope now maintains its bindings incrementally: a flat list of binding patterns, an identifier -> binding-index map, and per-scope start offsets. Reading an identifier (get_identifier) computes its selector directly from the binding's position: drop past newer bindings, take into the target binding (skipped for the oldest binding, the right tip of the input spine), then select within that binding's own pattern via the new BasePattern::get_from. The selectors are identical to the old fold-and-translate result, which the tests keep as a reference oracle, so the compiled program is byte-for-byte unchanged. - pattern.rs: factor BasePattern::get into get_from, which continues an accumulated selection instead of starting from an empty one - compile/mod.rs: rework Scope's representation as above; add a randomized property test comparing get_identifier against the reference, covering main and child scopes, shadowing and compound patterns Validation: the workspace test suite passes, including the pinned test-data bytecode regressions, and 28 programs (all examples plus generated stress shapes) produce byte-identical output to master. Local benchmarking (release build): codegen -68% for 256 sequential bindings, -63% for 256 bindings across 32 nested scopes, -35% at 64 bindings; a 4096-binding program goes 3.90s -> 120ms. Programs with only a few bindings regress ~1-3% from the per-let bookkeeping. --- src/compile/mod.rs | 267 +++++++++++++++++++++++++++++++++++++++++---- src/pattern.rs | 14 ++- 2 files changed, 257 insertions(+), 24 deletions(-) diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 02ac4468..0afb2e60 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -2,6 +2,7 @@ mod builtins; +use std::collections::HashMap; use std::sync::Arc; use either::Either; @@ -16,9 +17,10 @@ use crate::ast::{ }; use crate::debug::CallTracker; use crate::error::{Diagnostic, Error, Span, WithSpan}; -use crate::named::{self, CoreExt, PairBuilder}; +use crate::named::{self, CoreExt, PairBuilder, SelectorBuilder}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::pattern::{BasePattern, Pattern}; +use crate::str::Identifier; use crate::template_program::{TemplateProgram, TemplateProgramWitness}; use crate::types::{StructuralType, TypeDeconstructible}; use crate::value::{StructuralValue, Value}; @@ -38,15 +40,20 @@ type ProgNode<'brand> = Arc>; /// Bindings live as long as their scope. #[derive(Debug)] struct Scope<'brand> { - /// For each scope, the set of assigned variables. - /// - /// A stack of scopes. Each scope is a stack of patterns. - /// New patterns are pushed onto the top _(current, innermost)_ scope. + /// The patterns of all live bindings, in insertion order: the oldest + /// binding first, the newest last. /// /// ## Input pattern /// - /// The stack of scopes corresponds to an _input pattern_. + /// The bindings form the _input pattern_, the right-nested product + /// + /// ```text + /// product(binding_n, product(binding_{n-1}, ..., product(binding_1, binding_0))) + /// ``` + /// /// All valid input values match the input pattern. + /// Inner scopes occur higher in the tree than outer scopes. + /// Later assignments occur higher in the tree than earlier assignments. /// /// ## Example /// @@ -59,10 +66,16 @@ struct Scope<'brand> { /// / \ /// p2 p1 /// ``` - /// - /// Inner scopes occur higher in the tree than outer scopes. - /// Later assignments occur higher in the tree than earlier assignments. - variables: Vec>, + bindings: Vec, + /// For every live identifier, the indices into [`Scope::bindings`] of the + /// bindings that bind it, oldest first. The last index is the binding in + /// effect; earlier ones are shadowed and come back into effect when + /// their scope pops. + identifiers: HashMap>, + /// For every scope on the stack, the index in [`Scope::bindings`] where + /// that scope starts. Popping a scope truncates the bindings and restores + /// the shadowed identifiers. + binding_starts: Vec, ctx: simplicity::types::Context<'brand>, /// Tracker of function calls. call_tracker: Arc, @@ -72,6 +85,20 @@ struct Scope<'brand> { jet_hinter: Box, } +/// Register every identifier inside `bindings` in an identifiers map. +fn binding_identifiers(bindings: &[BasePattern]) -> HashMap> { + let mut identifiers: HashMap> = HashMap::new(); + for (index, binding) in bindings.iter().enumerate() { + for identifier in binding.identifiers() { + identifiers + .entry(identifier.clone()) + .or_default() + .push(index); + } + } + identifiers +} + impl<'brand> Scope<'brand> { /// Create the main scope. /// @@ -89,7 +116,9 @@ impl<'brand> Scope<'brand> { jet_hinter: Box, ) -> Self { Self { - variables: vec![vec![Pattern::Ignore]], + bindings: vec![BasePattern::Ignore], + identifiers: HashMap::new(), + binding_starts: vec![0], ctx, call_tracker, arguments, @@ -100,8 +129,11 @@ impl<'brand> Scope<'brand> { /// Create a child scope for a function that takes `input` of the given pattern. pub fn child(&self, input: Pattern) -> Self { + let bindings = vec![BasePattern::from(&input)]; Self { - variables: vec![vec![input]], + identifiers: binding_identifiers(&bindings), + bindings, + binding_starts: vec![0], ctx: self.ctx.shallow_clone(), call_tracker: Arc::clone(&self.call_tracker), arguments: self.arguments.clone(), @@ -112,16 +144,35 @@ impl<'brand> Scope<'brand> { /// Push a new scope onto the stack. pub fn push_scope(&mut self) { - self.variables.push(Vec::new()); + self.binding_starts.push(self.bindings.len()); } /// Pop the current scope from the stack. /// - /// # Panics + /// Bindings from the popped scope are removed and the identifiers that + /// they shadowed come back into effect. + /// + /// ## Panics /// /// The stack is empty. pub fn pop_scope(&mut self) { - self.variables.pop().expect("Empty stack"); + let start = self.binding_starts.pop().expect("Empty stack"); + while self.bindings.len() > start { + let index = self.bindings.len() - 1; + for identifier in self.bindings[index].identifiers() { + match self.identifiers.get_mut(identifier) { + Some(indices) => { + debug_assert_eq!(indices.last(), Some(&index)); + indices.pop(); + if indices.is_empty() { + self.identifiers.remove(identifier); + } + } + None => unreachable!("every binding registers its identifiers"), + } + } + self.bindings.pop(); + } } /// Push an assignment to the current scope. @@ -138,10 +189,15 @@ impl<'brand> Scope<'brand> { /// /// The stack is empty. pub fn insert(&mut self, pattern: Pattern) { - self.variables - .last_mut() - .expect("Empty stack") - .push(pattern); + let index = self.bindings.len(); + let binding = BasePattern::from(&pattern); + for identifier in binding.identifiers() { + self.identifiers + .entry(identifier.clone()) + .or_default() + .push(index); + } + self.bindings.push(binding); } /// Get the input pattern. @@ -151,11 +207,11 @@ impl<'brand> Scope<'brand> { /// ## Panics /// /// The stack is empty. - fn get_input_pattern(&self) -> Pattern { - let mut it = self.variables.iter().flat_map(|scope| scope.iter()); + fn get_input_pattern(&self) -> BasePattern { + let mut it = self.bindings.iter(); let first = it.next().expect("Empty stack"); it.cloned() - .fold(first.clone(), |acc, next| Pattern::product(next, acc)) + .fold(first.clone(), |acc, next| BasePattern::product(next, acc)) } /// Compute a Simplicity expression that takes a valid input value (that matches the input pattern) @@ -186,7 +242,46 @@ impl<'brand> Scope<'brand> { /// /// The expression `drop (IOH & OH)` returns the seeked value. pub fn get(&self, target: &BasePattern) -> Option>> { - BasePattern::from(&self.get_input_pattern()).translate(&self.ctx, target) + match target { + BasePattern::Identifier(identifier) => self.get_identifier(identifier), + // No caller passes anything but an identifier today. + BasePattern::Ignore | BasePattern::Product(..) => { + self.get_input_pattern().translate(&self.ctx, target) + } + } + } + + /// Extract a single identifier from the input value. + /// + /// The input pattern is the right-nested product + /// `product(binding_n, ..., product(binding_1, binding_0))`: every + /// binding but the oldest is the left child of a product; the oldest + /// binding is the right tip of the spine. The selection of the binding + /// at index `i` is therefore `[1; n - 1 - i]` — drop past every newer + /// binding — followed by `[0]` — take into the binding — unless `i == 0`, + /// followed by the path inside the binding's own pattern. + /// + /// That is bit-for-bit the selection that folding the whole input + /// pattern and translating it computes (see the reference in + /// [`scope_tests`]), so the compiled program is unchanged; only the + /// work to produce it shrinks from O(live bindings) per access to + /// O(distance from the binding). + fn get_identifier(&self, identifier: &Identifier) -> Option>> { + let index = *self.identifiers.get(identifier)?.last()?; + let newer_bindings = self.bindings.len() - 1 - index; + + let mut selector = SelectorBuilder::>::default(); + for _ in 0..newer_bindings { + selector = selector.i(); + } + let selector = match index { + // The oldest binding is the seed of the spine fold: the right + // tip of the product, reached without a final `take`. + 0 => selector, + _ => selector.o(), + }; + let selector = self.bindings[index].get_from(selector, identifier)?; + Some(selector.h(&self.ctx)) } /// Access the Simplicity type inference context. @@ -755,3 +850,129 @@ impl EnumMatch { input.comp(&dispatch).with_span(self) } } + +#[cfg(test)] +mod scope_tests { + use super::*; + use crate::str::Identifier; + + /// Reference implementation for [`Scope::get_identifier`]: fold the whole + /// input pattern and translate, the way every access worked before the + /// direct-selection fast path existed. + fn reference_get<'brand>( + scope: &Scope<'brand>, + target: &BasePattern, + ) -> Option>> { + scope.get_input_pattern().translate(&scope.ctx, target) + } + + /// Deterministic xorshift so failures reproduce. + struct Rng(u64); + + impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn below(&mut self, n: u64) -> u64 { + self.next() % n + } + } + + fn ident(s: &str) -> Identifier { + Identifier::from_str_unchecked(s) + } + + fn random_pattern(rng: &mut Rng, ids: &[Identifier], budget: u32) -> Pattern { + if budget == 0 || rng.below(4) == 0 { + if rng.below(3) == 0 { + return Pattern::Ignore; + } + let index = rng.below(ids.len() as u64) as usize; // len fits in u64 + return Pattern::Identifier(ids[index].clone()); + } + let len = 1 + rng.below(3) as usize; // 1..=3, fits in usize + let elements = (0..len) + .map(|_| random_pattern(rng, ids, budget - 1)) + .collect::>(); + if rng.below(2) == 0 { + Pattern::tuple(elements) + } else { + Pattern::array(elements) + } + } + + /// The fast path of [`Scope::get`] must produce bit-for-bit the same + /// selector as the reference implementation that folds the whole + /// environment, on randomized scopes with shadowing, nesting and + /// compound patterns. + #[test] + fn fast_get_matches_reference() { + let ids: Vec = ["a", "b", "c", "d"].map(ident).to_vec(); + let mut rng = Rng(0x5EED_CAFE_F00D_0001); + + types::Context::with_context(|ctx| { + let root = Scope::new( + ctx.shallow_clone(), + Arc::new(CallTracker::default()), + Arguments::default(), + false, + Box::new(crate::ast::ElementsJetHinter::new()), + ); + for iteration in 0..200u32 { + // Main scopes start with an ignore binding; child scopes + // (function bodies) start with the parameters pattern, whose + // identifiers sit at the oldest binding — the right tip of + // the input spine. + let mut scope = if iteration % 2 == 0 { + Scope::new( + ctx.shallow_clone(), + Arc::new(CallTracker::default()), + Arguments::default(), + false, + Box::new(crate::ast::ElementsJetHinter::new()), + ) + } else { + root.child(random_pattern(&mut rng, &ids, 3)) + }; + let mut depth = 1; + let statements = 5 + iteration % 30; + for _ in 0..statements { + match rng.below(10) { + 0 | 1 if depth < 6 => { + scope.push_scope(); + depth += 1; + } + 2 if depth > 1 => { + scope.pop_scope(); + depth -= 1; + } + _ => scope.insert(random_pattern(&mut rng, &ids, 3)), + } + for id in &ids { + let target = BasePattern::Identifier(id.clone()); + let fast = scope.get(&target); + let slow = reference_get(&scope, &target); + assert_eq!( + fast.is_some(), + slow.is_some(), + "presence mismatch for {id}, iteration {iteration}" + ); + if let (Some(fast), Some(slow)) = (fast, slow) { + assert_eq!( + fast.as_ref().display_expr().to_string(), + slow.as_ref().display_expr().to_string(), + "selector mismatch for {id}, iteration {iteration}" + ); + } + } + } + } + }); + } +} diff --git a/src/pattern.rs b/src/pattern.rs index caf6f608..9a54fafb 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -219,8 +219,20 @@ impl BasePattern { &self, identifier: &Identifier, ) -> Option> { - let mut selector = SelectorBuilder::default(); + self.get_from(SelectorBuilder::default(), identifier) + } + /// Like [`BasePattern::get`], but continue the selection accumulated in + /// `selector` instead of starting from an empty one: walk `self` as if + /// it had been entered after everything selected so far. + /// + /// The caller must know that `self` contains `identifier`; otherwise + /// the partial selection is discarded and `None` is returned. + pub(crate) fn get_from<'brand, P: CoreExt<'brand>>( + &self, + mut selector: SelectorBuilder

, + identifier: &Identifier, + ) -> Option> { for data in self.verbose_pre_order_iter() { match data.node { BasePattern::Identifier(id) if id == identifier => return Some(selector), From 733a93647e0d3dc1af53d1c03b50bf7096c51bf2 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Tue, 1 Sep 2026 20:01:28 -0500 Subject: [PATCH 2/2] compile: encapsulate Scope's binding bookkeeping in ScopeBindings The three fields added by the previous commit (binding list, identifier -> binding-index map, per-scope start offsets) form a referential-integrity triple: the index fields are meaningless without the binding list, and pop_scope repairs all three together. No invariant crosses the other Scope fields. ScopeBindings owns the triple behind a small interface (from_root/insert/push_scope/pop_scope/position_of/as_slice), so the invariants hold at method boundaries and pop_scope's transient half-updated states stay unobservable. The type has no generic parameters; all selector and type-inference logic stays in Scope. A property test checks position_of against a naive newest-first rescan of the bindings after every mutation. Also drops a stale "## Panics" section from insert, a leftover from a body that could panic; the current one cannot. Pure refactor: tests pass and example and stress programs produce byte-identical output. --- src/compile/mod.rs | 292 +++++++++++++++++++++++++++++++-------------- 1 file changed, 200 insertions(+), 92 deletions(-) diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 0afb2e60..3d23ca97 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -40,42 +40,8 @@ type ProgNode<'brand> = Arc>; /// Bindings live as long as their scope. #[derive(Debug)] struct Scope<'brand> { - /// The patterns of all live bindings, in insertion order: the oldest - /// binding first, the newest last. - /// - /// ## Input pattern - /// - /// The bindings form the _input pattern_, the right-nested product - /// - /// ```text - /// product(binding_n, product(binding_{n-1}, ..., product(binding_1, binding_0))) - /// ``` - /// - /// All valid input values match the input pattern. - /// Inner scopes occur higher in the tree than outer scopes. - /// Later assignments occur higher in the tree than earlier assignments. - /// - /// ## Example - /// - /// The stack `[[p1], [p2, p3]]` corresponds to a nested product pattern: - /// - /// ```text - /// . - /// / \ - /// p3 . - /// / \ - /// p2 p1 - /// ``` - bindings: Vec, - /// For every live identifier, the indices into [`Scope::bindings`] of the - /// bindings that bind it, oldest first. The last index is the binding in - /// effect; earlier ones are shadowed and come back into effect when - /// their scope pops. - identifiers: HashMap>, - /// For every scope on the stack, the index in [`Scope::bindings`] where - /// that scope starts. Popping a scope truncates the bindings and restores - /// the shadowed identifiers. - binding_starts: Vec, + /// The live bindings: their patterns, name index and scope boundaries. + bindings: ScopeBindings, ctx: simplicity::types::Context<'brand>, /// Tracker of function calls. call_tracker: Arc, @@ -85,20 +51,6 @@ struct Scope<'brand> { jet_hinter: Box, } -/// Register every identifier inside `bindings` in an identifiers map. -fn binding_identifiers(bindings: &[BasePattern]) -> HashMap> { - let mut identifiers: HashMap> = HashMap::new(); - for (index, binding) in bindings.iter().enumerate() { - for identifier in binding.identifiers() { - identifiers - .entry(identifier.clone()) - .or_default() - .push(index); - } - } - identifiers -} - impl<'brand> Scope<'brand> { /// Create the main scope. /// @@ -116,9 +68,7 @@ impl<'brand> Scope<'brand> { jet_hinter: Box, ) -> Self { Self { - bindings: vec![BasePattern::Ignore], - identifiers: HashMap::new(), - binding_starts: vec![0], + bindings: ScopeBindings::from_root(BasePattern::Ignore), ctx, call_tracker, arguments, @@ -129,11 +79,8 @@ impl<'brand> Scope<'brand> { /// Create a child scope for a function that takes `input` of the given pattern. pub fn child(&self, input: Pattern) -> Self { - let bindings = vec![BasePattern::from(&input)]; Self { - identifiers: binding_identifiers(&bindings), - bindings, - binding_starts: vec![0], + bindings: ScopeBindings::from_root(BasePattern::from(&input)), ctx: self.ctx.shallow_clone(), call_tracker: Arc::clone(&self.call_tracker), arguments: self.arguments.clone(), @@ -144,7 +91,7 @@ impl<'brand> Scope<'brand> { /// Push a new scope onto the stack. pub fn push_scope(&mut self) { - self.binding_starts.push(self.bindings.len()); + self.bindings.push_scope(); } /// Pop the current scope from the stack. @@ -156,23 +103,7 @@ impl<'brand> Scope<'brand> { /// /// The stack is empty. pub fn pop_scope(&mut self) { - let start = self.binding_starts.pop().expect("Empty stack"); - while self.bindings.len() > start { - let index = self.bindings.len() - 1; - for identifier in self.bindings[index].identifiers() { - match self.identifiers.get_mut(identifier) { - Some(indices) => { - debug_assert_eq!(indices.last(), Some(&index)); - indices.pop(); - if indices.is_empty() { - self.identifiers.remove(identifier); - } - } - None => unreachable!("every binding registers its identifiers"), - } - } - self.bindings.pop(); - } + self.bindings.pop_scope(); } /// Push an assignment to the current scope. @@ -184,20 +115,8 @@ impl<'brand> Scope<'brand> { /// / \ /// p previous /// ``` - /// - /// ## Panics - /// - /// The stack is empty. pub fn insert(&mut self, pattern: Pattern) { - let index = self.bindings.len(); - let binding = BasePattern::from(&pattern); - for identifier in binding.identifiers() { - self.identifiers - .entry(identifier.clone()) - .or_default() - .push(index); - } - self.bindings.push(binding); + self.bindings.insert(BasePattern::from(&pattern)); } /// Get the input pattern. @@ -208,7 +127,7 @@ impl<'brand> Scope<'brand> { /// /// The stack is empty. fn get_input_pattern(&self) -> BasePattern { - let mut it = self.bindings.iter(); + let mut it = self.bindings.as_slice().iter(); let first = it.next().expect("Empty stack"); it.cloned() .fold(first.clone(), |acc, next| BasePattern::product(next, acc)) @@ -267,8 +186,9 @@ impl<'brand> Scope<'brand> { /// work to produce it shrinks from O(live bindings) per access to /// O(distance from the binding). fn get_identifier(&self, identifier: &Identifier) -> Option>> { - let index = *self.identifiers.get(identifier)?.last()?; - let newer_bindings = self.bindings.len() - 1 - index; + let bindings = self.bindings.as_slice(); + let index = self.bindings.position_of(identifier)?; + let newer_bindings = bindings.len() - 1 - index; let mut selector = SelectorBuilder::>::default(); for _ in 0..newer_bindings { @@ -280,7 +200,7 @@ impl<'brand> Scope<'brand> { 0 => selector, _ => selector.o(), }; - let selector = self.bindings[index].get_from(selector, identifier)?; + let selector = bindings[index].get_from(selector, identifier)?; Some(selector.h(&self.ctx)) } @@ -318,6 +238,145 @@ impl<'brand> Scope<'brand> { } } +/// The live bindings of a scope stack, in the shape that makes identifier +/// extraction O(distance) instead of O(live bindings). +/// +/// ## Input pattern +/// +/// The bindings form the _input pattern_, the right-nested product +/// +/// ```text +/// product(binding_n, product(binding_{n-1}, ..., product(binding_1, binding_0))) +/// ``` +/// +/// All valid input values match the input pattern. +/// Inner scopes occur higher in the tree than outer scopes. +/// Later assignments occur higher in the tree than earlier assignments. +/// +/// ## Example +/// +/// The stack `[[p1], [p2, p3]]` corresponds to a nested product pattern: +/// +/// ```text +/// . +/// / \ +/// p3 . +/// / \ +/// p2 p1 +/// ``` +/// +/// ## Invariants +/// +/// - `binding_starts` is nonempty and non-decreasing; its last entry is the +/// index where the current (innermost) scope's bindings begin. +/// - Every index stored in `identifiers` is in range of `bindings`, and +/// every identifier's indices are ascending: the last one is the binding +/// in effect, earlier ones are shadowed by it. +/// - All mutations go through [`insert`](Self::insert), +/// [`push_scope`](Self::push_scope) and [`pop_scope`](Self::pop_scope), +/// which restore the invariants before returning. +#[derive(Debug)] +struct ScopeBindings { + /// The patterns of all live bindings, in insertion order: the oldest + /// binding first, the newest last. + bindings: Vec, + /// For every live identifier, the indices into `bindings` of the + /// bindings that bind it, oldest first. The last index is the binding + /// in effect; earlier ones are shadowed and come back into effect when + /// their scope pops. + identifiers: HashMap>, + /// For every scope on the stack, the index in `bindings` where that + /// scope starts. Popping a scope truncates the bindings and restores + /// the shadowed identifiers. + binding_starts: Vec, +} + +impl ScopeBindings { + /// Create bindings whose oldest binding — the right tip of the input + /// pattern — is `root`. + fn from_root(root: BasePattern) -> Self { + let mut bindings = Self { + bindings: vec![root.clone()], + identifiers: HashMap::new(), + binding_starts: vec![0], + }; + for identifier in root.identifiers() { + bindings + .identifiers + .entry(identifier.clone()) + .or_default() + .push(0); + } + bindings + } + + /// The live bindings, oldest first. A binding's index in the slice is + /// its position in the input pattern: 0 is the right tip, `len() - 1` + /// the newest, leftmost binding. + fn as_slice(&self) -> &[BasePattern] { + &self.bindings + } + + /// The index of the binding in effect for `identifier`, or `None` if no + /// live binding binds that name. + fn position_of(&self, identifier: &Identifier) -> Option { + self.identifiers + .get(identifier) + .and_then(|indices| indices.last().copied()) + } + + /// Open a new, empty scope. + fn push_scope(&mut self) { + self.binding_starts.push(self.bindings.len()); + } + + /// Close the current scope: its bindings die and the identifiers they + /// shadowed come back into effect. + /// + /// ## Panics + /// + /// The stack is empty. + fn pop_scope(&mut self) { + let start = self.binding_starts.pop().expect("Empty stack"); + while self.bindings.len() > start { + let index = self.bindings.len() - 1; + for identifier in self.bindings[index].identifiers() { + match self.identifiers.get_mut(identifier) { + Some(indices) => { + debug_assert_eq!(indices.last(), Some(&index)); + indices.pop(); + if indices.is_empty() { + self.identifiers.remove(identifier); + } + } + None => unreachable!("every binding registers its identifiers"), + } + } + self.bindings.pop(); + } + } + + /// Append `binding` to the current scope and register its identifiers. + /// + /// Update the input pattern accordingly: + /// + /// ```text + /// . + /// / \ + /// p previous + /// ``` + fn insert(&mut self, binding: BasePattern) { + let index = self.bindings.len(); + for identifier in binding.identifiers() { + self.identifiers + .entry(identifier.clone()) + .or_default() + .push(index); + } + self.bindings.push(binding); + } +} + fn compile_blk<'brand>( stmts: &[Statement], scope: &mut Scope<'brand>, @@ -975,4 +1034,53 @@ mod scope_tests { } }); } + + /// [`ScopeBindings::position_of`] must agree with a naive newest-first + /// rescan of the bindings after every mutation, so that the referential + /// integrity between the three fields has its own oracle instead of + /// being checked only indirectly through selector equality. + #[test] + fn position_of_matches_rescan() { + let ids: Vec = ["a", "b", "c", "d"].map(ident).to_vec(); + let mut rng = Rng(0xD1CE_C0FF_EE0D_0002); + + for iteration in 0..200u32 { + // Roots with and without identifiers: an ignore root mirrors + // the main scope, a pattern root mirrors a function's parameters. + let root = if iteration % 2 == 0 { + BasePattern::Ignore + } else { + BasePattern::from(&random_pattern(&mut rng, &ids, 2)) + }; + let mut bindings = ScopeBindings::from_root(root); + let mut depth = 1; + for _ in 0..(5 + iteration % 30) { + match rng.below(10) { + 0 | 1 if depth < 6 => { + bindings.push_scope(); + depth += 1; + } + 2 if depth > 1 => { + bindings.pop_scope(); + depth -= 1; + } + _ => bindings.insert(BasePattern::from(&random_pattern(&mut rng, &ids, 3))), + } + for id in &ids { + let naive = bindings + .as_slice() + .iter() + .enumerate() + .rev() + .find(|(_, binding)| binding.contains(id)) + .map(|(index, _)| index); + assert_eq!( + bindings.position_of(id), + naive, + "mismatch for {id}, iteration {iteration}" + ); + } + } + } + } }