From 041e6ab9f021475ad4b1f326bbbdef775c5c1e30 Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 25 Jul 2026 00:01:26 +0200 Subject: [PATCH 1/2] perf: avoid re-running the trait-solver fast path on unchanged obligations `FulfillmentCtxt::try_evaluate_obligations` re-processes every pending ambiguous obligation on every call. For obligations resolved by `compute_goal_fast_path`, the `Certainty::Maybe` branch discarded the obligation's `GoalStalledOn` (re-registering with `None`), which meant every subsequent call fully re-ran the fast path's predicate resolution (`shallow_resolve` + `is_trivially_wf`) instead of being able to skip it, even though the real solver's own internal stall-check exists precisely to avoid this. Reconstruct a `GoalStalledOn` for these obligations by walking the goal's predicate for the inference variables it depends on (`fast_path_stalled_on`), and check it up front with the same primitives the solver's internal fast path already uses (`is_still_stalled`), so an unchanged obligation is skipped before touching the solver at all. This is a constant-factor fix, not asymptotic (the per-call scan over all pending obligations remains O(n)). Measured: prefill 7.79s -> 4.35s (1.79x), full self-analysis inference 24.4s -> ~17-18s (~1.4x), up to 3.9x per-obligation on a synthetic worst case. Full-repo differential (unknown type / type mismatches / pattern unknown type / pattern type mismatches / mir failed bodies / failed const evals) identical before/after; `cargo test -p hir-ty` 1015/0 both ways; clippy clean. --- crates/hir-ty/src/next_solver/fulfill.rs | 102 +++++++++++++++++- .../next_solver/infer/opaque_types/table.rs | 9 ++ 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/crates/hir-ty/src/next_solver/fulfill.rs b/crates/hir-ty/src/next_solver/fulfill.rs index e422f75a0207..57fa4ade6012 100644 --- a/crates/hir-ty/src/next_solver/fulfill.rs +++ b/crates/hir-ty/src/next_solver/fulfill.rs @@ -8,15 +8,17 @@ use rustc_next_trait_solver::{ solve::{GoalEvaluation, GoalStalledOn, HasChanged, SolverDelegateEvalExt}, }; use rustc_type_ir::{ - Interner, TypeSuperVisitable, TypeVisitable, TypeVisitableExt, TypeVisitor, - inherent::IntoKind, - solve::{Certainty, NoSolution}, + InferConst, InferCtxtLike, InferTy, Interner, TyVid, TypeSuperVisitable, TypeVisitable, + TypeVisitableExt, TypeVisitor, + inherent::{IntoKind, OpaqueTypeStorageEntries as _}, + solve::{Certainty, Goal, NoSolution}, }; use crate::{ Span, next_solver::{ - DbInterner, SolverContext, SolverDefId, Ty, TyKind, TypingMode, + Const, ConstKind, DbInterner, GenericArg, Predicate, SolverContext, SolverDefId, Ty, + TyKind, TypingMode, infer::{ InferCtxt, traits::{PredicateObligation, PredicateObligations}, @@ -164,6 +166,19 @@ impl<'db> FulfillmentCtxt<'db> { let mut any_changed = false; self.try_evaluate_obligations_scratch.extend(self.obligations.drain_pending(|_| true)); for (mut obligation, stalled_on) in self.try_evaluate_obligations_scratch.drain(..) { + // If we've evaluated this goal before and it stalled on a specific set of + // inference variables, and none of those variables have been touched since, + // evaluating it again is guaranteed to produce the same `Maybe` result (goal + // evaluation is pure). Skip it entirely rather than re-running the solver, so + // that re-processing pending obligations is proportional to how much changed + // since the last call, not to the total number of pending obligations. + if let Some(stalled_on) = &stalled_on + && is_still_stalled(infcx, stalled_on) + { + self.obligations.register(obligation, Some(stalled_on.clone())); + continue; + } + if obligation.recursion_depth >= infcx.interner.recursion_limit() { self.obligations.on_fulfillment_overflow(infcx); // Only return true errors that we have accumulated while processing. @@ -178,7 +193,8 @@ impl<'db> FulfillmentCtxt<'db> { match certainty { Certainty::Yes => {} Certainty::Maybe { .. } => { - self.obligations.register(obligation, None); + self.obligations + .register(obligation, fast_path_stalled_on(infcx, goal)); } } continue; @@ -278,6 +294,82 @@ impl<'db> FulfillmentCtxt<'db> { } } +/// Mirrors the stalled-goal fast path that `evaluate_goal_raw` runs internally on every +/// evaluation: if none of the tracked variables changed (and the opaque type storage didn't +/// grow) since `stalled_on` was recorded, the goal is guaranteed to still evaluate to the same +/// `Maybe` result. Running this cheap check *before* touching the solver at all is what makes +/// `try_evaluate_obligations` proportional to the obligations actually woken since the last +/// call rather than to the total pending set. +fn is_still_stalled<'db>( + infcx: &InferCtxt<'db>, + stalled_on: &GoalStalledOn>, +) -> bool { + !infcx.disable_trait_solver_fast_paths() + && !stalled_on.stalled_vars.iter().any(|&value| infcx.is_changed_arg(value)) + && !stalled_on.sub_roots.iter().any(|&vid| infcx.sub_unification_table_root_var(vid) != vid) + && !infcx.opaque_types_storage_num_entries().needs_reevaluation(stalled_on.num_opaques) +} + +/// Best-effort reconstruction of a [`GoalStalledOn`] for goals resolved by +/// [`crate::next_solver::solver::SolverContext::compute_goal_fast_path`]'s `Certainty::Maybe` +/// result. +/// +/// That fast path (unlike the real solver) does not build a `GoalStalledOn`, so without this +/// we would lose all stall-tracking for the obligations it intercepts, forcing them through the +/// fast path again on every single call to `try_evaluate_obligations` for the rest of the body. +/// We over-approximate by collecting every inference variable mentioned in the goal's +/// predicate: extra entries only make `is_still_stalled` wake the goal on unrelated changes, +/// which is wasteful but never unsound, whereas missing one could permanently mask real +/// progress. +fn fast_path_stalled_on<'db>( + infcx: &InferCtxt<'db>, + goal: Goal, Predicate<'db>>, +) -> Option>> { + struct CollectInferVars<'a, 'db> { + infcx: &'a InferCtxt<'db>, + stalled_vars: Vec>, + sub_roots: Vec, + } + + impl<'db> TypeVisitor> for CollectInferVars<'_, 'db> { + type Result = (); + + fn visit_ty(&mut self, ty: Ty<'db>) { + match ty.kind() { + TyKind::Infer(InferTy::TyVar(vid)) => { + self.stalled_vars.push(ty.into()); + self.sub_roots.push(self.infcx.sub_unification_table_root_var(vid)); + } + TyKind::Infer(_) => self.stalled_vars.push(ty.into()), + _ if ty.has_infer() => ty.super_visit_with(self), + _ => {} + } + } + + fn visit_const(&mut self, ct: Const<'db>) { + match ct.kind() { + ConstKind::Infer(InferConst::Var(_)) => self.stalled_vars.push(ct.into()), + _ if ct.has_infer() => ct.super_visit_with(self), + _ => {} + } + } + } + + let mut collector = CollectInferVars { infcx, stalled_vars: Vec::new(), sub_roots: Vec::new() }; + goal.predicate.visit_with(&mut collector); + if collector.stalled_vars.is_empty() { + // We couldn't pin down what this goal is blocked on (e.g. the ambiguity came from the + // param-env rather than the predicate); fall back to always rechecking it. + return None; + } + Some(GoalStalledOn { + num_opaques: infcx.opaque_types_storage_num_entries().opaque_type_count(), + stalled_vars: collector.stalled_vars, + sub_roots: collector.sub_roots, + stalled_certainty: Certainty::AMBIGUOUS, + }) +} + /// Detect if a goal is stalled on a coroutine that is owned by the current typeck root. /// /// This function can (erroneously) fail to detect a predicate, i.e. it doesn't need to diff --git a/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs b/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs index 894fe5eb7b87..bce877fde8d7 100644 --- a/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs +++ b/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs @@ -27,6 +27,15 @@ pub struct OpaqueTypeStorageEntries { duplicate_entries: usize, } +impl OpaqueTypeStorageEntries { + /// The raw entry count, for constructing a [`rustc_next_trait_solver::solve::GoalStalledOn`] + /// outside of the solver's own canonicalization (which is where `num_opaques` is normally + /// computed from). + pub(crate) fn opaque_type_count(self) -> usize { + self.opaque_types + } +} + impl rustc_type_ir::inherent::OpaqueTypeStorageEntries for OpaqueTypeStorageEntries { fn needs_reevaluation(self, canonicalized: usize) -> bool { self.opaque_types != canonicalized From 76cc8eaff1ea74c92b86c803a6137a5411f743bd Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 25 Jul 2026 01:11:10 +0200 Subject: [PATCH 2/2] perf: make fulfillment wake obligations by stalled-on variable instead of rescanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FulfillmentCtxt::try_evaluate_obligations` drained and re-touched the *entire* pending set on every call. Body inference calls it O(n) times (from `select_obligations_where_possible` via structural resolution, method calls, operators, ...), making single-body inference O(n²) in the number of expressions. Large macro-generated bodies dominate whole-workspace inference: `intern::symbol::symbols::prefill` (~13k exprs) alone was ~35% of all inference time on self-analysis. This replaces the rescan with an event-driven index: - `InferCtxtUndoLogs` now keeps an append-only `changed_vars` log of every inference-variable slot `ena` writes to. `ena` gates its undo-log `push` calls on `UndoLogs::in_snapshot()`, which would lose every mutation made while no snapshot is open, so that trait method deliberately answers `true` and the real undo entries are gated on `num_open_snapshots` internally instead; an inherent `in_snapshot` shadows the trait method for code that wants the truth. Draining the opaque-type storage (the one mutation path that bypasses the undo machinery) records an explicit event. - Each `FulfillmentCtxt` reads the log through its own cursor (non-destructively, so nested `ObligationCtxt`s sharing the infcx cannot steal the body-scoped context's wake signals) and wakes exactly the obligations parked on a changed variable via a slab + watch-key index. Obligations are only parked when their `GoalStalledOn` was just verified intact — which also guarantees the tracked variables are unification roots, the identity `ena` fires events on. Stall sets that cannot be watched (`Fresh*` vars) stay on a periodic recheck path that re-runs once per call and after rounds that made real progress, which is also what makes the fixpoint loop terminate. - A debug-builds-only sweep asserts after every wake pass that no parked obligation's stall condition was invalidated without a wake (the failure mode of this design is silently wrong inference results, so it must fail loudly in tests), and that the slab's live counter is in sync. Measured (macOS, release): synthetic one-body benchmark N=1000/2000/ 4000/8000 statements goes from 0.48s/1.61s/6.0s/23.6s to 0.13s/0.15s/ 0.23s/0.38s (per-doubling exponent ~1.9 -> ~0.7); `analysis-stats . --only prefill` inference 4.5s -> 2.6s; whole-workspace self-analysis inference 17.6s -> 14.0s; peak RSS unchanged. Differential check on full self-analysis: `unknown type`, `type mismatches`, pattern variants, `mir failed bodies`, `failed const evals` all identical to before. Three snapshots in `tests::regression` (`recursive_vars`, `recursive_vars_2`, `infer_std_crash_5`) were re-blessed: they encode the exact number of `&'?` layers accumulated before the recursion limit trips on pathological recursive-unknown code (annotated in-test as an acknowledged artifact), and the reworked loop reaches the limit with a ±1 round count. The final types are the same `{unknown}`-based fallback. --- crates/hir-ty/src/next_solver/fulfill.rs | 405 ++++++++++++++---- crates/hir-ty/src/next_solver/infer/mod.rs | 11 +- .../next_solver/infer/opaque_types/table.rs | 11 + .../infer/region_constraints/mod.rs | 7 +- .../src/next_solver/infer/snapshot/mod.rs | 120 +++++- .../next_solver/infer/snapshot/undo_log.rs | 126 +++++- crates/hir-ty/src/tests/regression.rs | 28 +- 7 files changed, 604 insertions(+), 104 deletions(-) diff --git a/crates/hir-ty/src/next_solver/fulfill.rs b/crates/hir-ty/src/next_solver/fulfill.rs index 57fa4ade6012..24ae84e860d9 100644 --- a/crates/hir-ty/src/next_solver/fulfill.rs +++ b/crates/hir-ty/src/next_solver/fulfill.rs @@ -2,17 +2,18 @@ use std::ops::ControlFlow; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use rustc_next_trait_solver::{ delegate::SolverDelegate, solve::{GoalEvaluation, GoalStalledOn, HasChanged, SolverDelegateEvalExt}, }; use rustc_type_ir::{ - InferConst, InferCtxtLike, InferTy, Interner, TyVid, TypeSuperVisitable, TypeVisitable, - TypeVisitableExt, TypeVisitor, - inherent::{IntoKind, OpaqueTypeStorageEntries as _}, + GenericArgKind, InferConst, InferCtxtLike, InferTy, Interner, TyVid, TypeSuperVisitable, + TypeVisitable, TypeVisitableExt, TypeVisitor, + inherent::{Const as _, IntoKind, OpaqueTypeStorageEntries as _, Ty as _}, solve::{Certainty, Goal, NoSolution}, }; +use smallvec::SmallVec; use crate::{ Span, @@ -20,7 +21,7 @@ use crate::{ Const, ConstKind, DbInterner, GenericArg, Predicate, SolverContext, SolverDefId, Ty, TyKind, TypingMode, infer::{ - InferCtxt, + InferCtxt, StalledVarKey, traits::{PredicateObligation, PredicateObligations}, }, inspect::ProofTreeVisitor, @@ -50,9 +51,30 @@ pub struct FulfillmentCtxt<'db> { /// gets rolled back. Because of this we explicitly check that we only /// use the context in exactly this snapshot. usable_in_snapshot: usize, + /// Read position into the infer context's changed-inference-variables log + /// (see [`InferCtxt::changed_vars_since`]). Everything before this position has already + /// been translated into wake-ups of the obligations parked in `obligations.watch`. The log + /// is shared by every `FulfillmentCtxt` on the same `InferCtxt` but reading it is + /// non-destructive, so short-lived contexts (nested probes, coercions, method resolution) + /// don't steal wake signals from the long-lived, body-scoped one. + changed_vars_cursor: usize, try_evaluate_obligations_scratch: PendingObligations<'db>, + changed_vars_scratch: Vec, } +/// An ambiguous obligation parked on the variables of its `GoalStalledOn`, together with the +/// watch keys it is filed under in [`ObligationStorage::watch`]. +#[derive(Debug, Clone)] +struct Slot<'db> { + obligation: PredicateObligation<'db>, + stalled_on: GoalStalledOn>, + keys: SmallVec<[StalledVarKey; 4]>, +} + +/// Obligations that are still ambiguous (`Certainty::Maybe`), indexed so that an inference +/// variable changing wakes exactly the obligations stalled on it, instead of every call to +/// `try_evaluate_obligations` rescanning everything still pending (which made large bodies +/// quadratic: O(calls) × O(pending)). #[derive(Default, Debug, Clone)] struct ObligationStorage<'db> { /// Obligations which resulted in an overflow in fulfillment itself. @@ -61,52 +83,232 @@ struct ObligationStorage<'db> { /// to avoid recomputing them each time `try_evaluate_obligations` is called. /// This also allows us to return the correct `FulfillmentError` for them. overflowed: Vec>, - pending: PendingObligations<'db>, + /// Obligations registered from the outside and not yet evaluated in this context. + /// Fully processed on the next `try_evaluate_obligations` call. + fresh: Vec>, + /// Obligations that evaluated to `Certainty::Maybe` but whose stall condition can't be + /// attached to any watchable variable (no `stalled_on` at all, a `Fresh*` inference + /// variable, or a stall set that had already been invalidated again by the time we got to + /// park it). Re-evaluated on the first round of every call and after each round that made + /// real progress — the same cadence at which the old code re-evaluated everything — but + /// crucially they do *not* requeue rounds by themselves, which is what guarantees the + /// fixpoint loop terminates once nothing is actually changing. + unwatchable: PendingObligations<'db>, + /// Ambiguous obligations parked on a known set of variables. `None` is a tombstone left + /// behind by `wake`; slots are compacted once tombstones outnumber live entries, and + /// `watch` is rebuilt from the keys stored in each slot at the same time. + slab: Vec>>, + /// Number of `Some` entries in `slab`. + live: usize, + /// Maps a variable (or the opaque-type storage as a whole, see [`StalledVarKey::Opaques`]) + /// to the slab slots of obligations parked on it. An obligation is listed under each of its + /// keys; waking it through any one of them empties its slot, turning the remaining entries + /// into harmless tombstone hits. + watch: FxHashMap>, +} + +/// Classifies every variable a [`GoalStalledOn`] is blocked on into a [`StalledVarKey`] for +/// indexing into [`ObligationStorage::watch`]. +/// +/// The caller must have just verified [`is_still_stalled`]: that check passing means every +/// tracked variable is an unresolved unification *root* (a resolved or re-rooted variable +/// counts as "changed"), so the variables can be used as watch keys directly. Mutations are +/// only observable on root slots, and a later union of the root itself fires a wake for it, +/// at which point the obligation re-registers under the merged root. Returns `None` if some +/// tracked variable isn't watchable (`Fresh*` inference variables, which no unification table +/// observes, and which `is_changed_arg` should already have rejected) — the caller must then +/// fall back to periodic rechecking rather than risk the obligation never being woken. +fn stalled_var_keys<'db>( + stalled_on: &GoalStalledOn>, +) -> Option> { + let mut keys = + SmallVec::with_capacity(stalled_on.stalled_vars.len() + stalled_on.sub_roots.len() + 1); + for arg in &stalled_on.stalled_vars { + match arg.kind() { + // Lifetimes can never stall a goal; the solver never puts one in `stalled_vars`. + GenericArgKind::Lifetime(_) => {} + GenericArgKind::Type(ty) => match ty.kind() { + TyKind::Infer(InferTy::TyVar(vid)) => keys.push(StalledVarKey::Ty(vid)), + TyKind::Infer(InferTy::IntVar(vid)) => keys.push(StalledVarKey::Int(vid)), + TyKind::Infer(InferTy::FloatVar(vid)) => keys.push(StalledVarKey::Float(vid)), + _ => return None, + }, + GenericArgKind::Const(ct) => match ct.kind() { + ConstKind::Infer(InferConst::Var(vid)) => keys.push(StalledVarKey::Const(vid)), + _ => return None, + }, + } + } + keys.extend(stalled_on.sub_roots.iter().map(|&vid| StalledVarKey::TySubRoot(vid))); + // `num_opaques` is a condition of every `GoalStalledOn`, not tied to a specific variable. + keys.push(StalledVarKey::Opaques); + Some(keys) } impl<'db> ObligationStorage<'db> { - fn register( + fn register_fresh(&mut self, obligation: PredicateObligation<'db>) { + self.fresh.push(obligation); + } + + /// Files an obligation that evaluated to `Certainty::Maybe`. If its stall condition is + /// intact and watchable it is parked in `slab`/`watch` and only ever revisited when one of + /// its variables is reported changed; otherwise it goes to `unwatchable` for periodic + /// rechecking. + fn park( &mut self, + infcx: &InferCtxt<'db>, obligation: PredicateObligation<'db>, stalled_on: Option>>, ) { - self.pending.push((obligation, stalled_on)); + match stalled_on { + // Only park an obligation whose stall condition currently holds: `is_still_stalled` + // is an absolute check ("no tracked variable is resolved or re-rooted"), so a stall + // set that is already invalidated here would never validate again, while the wake + // events for its variables may already have been consumed — parking it could leave + // it asleep forever. Such obligations must stay on the periodic recheck path. + Some(stalled_on) if is_still_stalled(infcx, &stalled_on) => { + self.park_verified(obligation, stalled_on) + } + stalled_on => self.unwatchable.push((obligation, stalled_on)), + } + } + + /// Like [`Self::park`], for callers that have just checked `is_still_stalled` themselves. + fn park_verified( + &mut self, + obligation: PredicateObligation<'db>, + stalled_on: GoalStalledOn>, + ) { + let Some(keys) = stalled_var_keys(&stalled_on) else { + self.unwatchable.push((obligation, Some(stalled_on))); + return; + }; + self.maybe_compact(); + let slot = self.slab.len(); + for &key in &keys { + self.watch.entry(key).or_default().push(slot); + } + self.slab.push(Some(Slot { obligation, stalled_on, keys })); + self.live += 1; + } + + /// Moves every obligation whose slab slot is watched under `key` into `out`, leaving + /// tombstones behind. + fn wake(&mut self, key: StalledVarKey, out: &mut PendingObligations<'db>) { + let Some(slots) = self.watch.remove(&key) else { return }; + for idx in slots { + if let Some(slot) = self.slab[idx].take() { + self.live -= 1; + out.push((slot.obligation, Some(slot.stalled_on))); + } + } + } + + /// Rebuilds `slab`/`watch` without tombstones once they outnumber live entries, so storage + /// stays proportional to what is actually parked instead of growing for the lifetime of + /// the context. + fn maybe_compact(&mut self) { + if self.slab.len() < 64 || 2 * self.live > self.slab.len() { + return; + } + self.slab.retain(Option::is_some); + self.watch.clear(); + for (idx, slot) in self.slab.iter().flatten().enumerate() { + for &key in &slot.keys { + self.watch.entry(key).or_default().push(idx); + } + } + } + + /// Removes and returns every pending obligation matching `pred`, across all three buckets. + fn extract_pending_if( + &mut self, + mut pred: impl FnMut(&PredicateObligation<'db>) -> bool, + ) -> PredicateObligations<'db> { + let mut result: PredicateObligations<'db> = + self.fresh.extract_if(.., |o| pred(o)).collect(); + result.extend(self.unwatchable.extract_if(.., |(o, _)| pred(o)).map(|(o, _)| o)); + for slot in self.slab.iter_mut() { + if let Some(taken) = slot.take_if(|slot| pred(&slot.obligation)) { + self.live -= 1; + result.push(taken.obligation); + } + } + result + } + + fn iter_pending(&self) -> impl Iterator> { + self.fresh + .iter() + .chain(self.unwatchable.iter().map(|(o, _)| o)) + .chain(self.slab.iter().flatten().map(|slot| &slot.obligation)) } fn clone_pending(&self) -> PredicateObligations<'db> { - let mut obligations: PredicateObligations<'db> = - self.pending.iter().map(|(o, _)| o.clone()).collect(); + let mut obligations: PredicateObligations<'db> = self.iter_pending().cloned().collect(); obligations.extend(self.overflowed.iter().cloned()); obligations } - fn drain_pending<'this, 'cond>( - &'this mut self, - cond: impl 'cond + Fn(&PredicateObligation<'db>) -> bool, - ) -> impl Iterator, Option>>)> - { - self.pending.extract_if(.., move |(o, _)| cond(o)) + fn drain_pending(&mut self) -> impl Iterator> { + self.watch.clear(); + self.live = 0; + self.fresh + .drain(..) + .chain(self.unwatchable.drain(..).map(|(o, _)| o)) + .chain(self.slab.drain(..).flatten().map(|slot| slot.obligation)) + } + + /// Debug-builds-only safety net for the event-driven wake machinery. Called after a + /// round's wake-ups have been applied: every obligation still parked must have an intact + /// stall condition, because anything that could invalidate one records a changed-variable + /// event, and consuming that event wakes the obligation. An entry failing this check has + /// missed a wake-up and could keep a stale ambiguity (i.e. wrong inference results) + /// forever. Also verifies the `live` counter, which the compaction heuristic relies on. + fn assert_no_missed_wakes(&self, infcx: &InferCtxt<'db>) { + if cfg!(debug_assertions) { + stdx::never!( + self.live != self.slab.iter().flatten().count(), + "`live` out of sync with the slab" + ); + for slot in self.slab.iter().flatten() { + stdx::never!( + !is_still_stalled(infcx, &slot.stalled_on), + "parked obligation missed a wake-up; its stall condition no longer holds" + ); + } + } } fn on_fulfillment_overflow(&mut self, infcx: &InferCtxt<'db>) { + // Overflow is rare enough that unconditionally re-evaluating the parked obligations + // (like the old flat storage did) and dropping their stall state is fine; whatever is + // retained ends up in `unwatchable` (its `stalled_on` is `take`n below) and gets + // rechecked from scratch on the next call. + self.unwatchable.extend( + self.slab.drain(..).flatten().map(|slot| (slot.obligation, Some(slot.stalled_on))), + ); + self.watch.clear(); + self.live = 0; infcx.probe(|_| { // IMPORTANT: we must not use solve any inference variables in the obligations // as this is all happening inside of a probe. We use a probe to make sure // we get all obligations involved in the overflow. We pretty much check: if // we were to do another step of `try_evaluate_obligations`, which goals would // change. - // FIXME: is merged, this can be removed. + let delegate = <&SolverContext<'db>>::from(infcx); + let has_changed = + |o: &PredicateObligation<'db>, + stalled_on: Option>>| { + let result = + delegate.evaluate_root_goal(o.as_goal(), o.cause.span(), stalled_on); + matches!(result, Ok(GoalEvaluation { has_changed: HasChanged::Yes, .. })) + }; + + self.overflowed.extend(self.fresh.extract_if(.., |o| has_changed(o, None))); self.overflowed.extend( - self.pending - .extract_if(.., |(o, stalled_on)| { - let goal = o.as_goal(); - let result = <&SolverContext<'db>>::from(infcx).evaluate_root_goal( - goal, - o.cause.span(), - stalled_on.take(), - ); - matches!(result, Ok(GoalEvaluation { has_changed: HasChanged::Yes, .. })) - }) + self.unwatchable + .extract_if(.., |(o, stalled_on)| has_changed(o, stalled_on.take())) .map(|(o, _)| o), ); }) @@ -118,7 +320,10 @@ impl<'db> FulfillmentCtxt<'db> { FulfillmentCtxt { obligations: Default::default(), usable_in_snapshot: infcx.num_open_snapshots(), + // Nothing is parked yet, so variable changes before this point are irrelevant. + changed_vars_cursor: infcx.changed_vars_len(), try_evaluate_obligations_scratch: Default::default(), + changed_vars_scratch: Default::default(), } } } @@ -131,7 +336,7 @@ impl<'db> FulfillmentCtxt<'db> { obligation: PredicateObligation<'db>, ) { assert_eq!(self.usable_in_snapshot, infcx.num_open_snapshots()); - self.obligations.register(obligation, None); + self.obligations.register_fresh(obligation); } pub(crate) fn register_predicate_obligations( @@ -140,19 +345,17 @@ impl<'db> FulfillmentCtxt<'db> { obligations: impl IntoIterator>, ) { assert_eq!(self.usable_in_snapshot, infcx.num_open_snapshots()); - obligations.into_iter().for_each(|obligation| self.obligations.register(obligation, None)); + obligations.into_iter().for_each(|obligation| self.obligations.register_fresh(obligation)); } pub(crate) fn collect_remaining_errors( &mut self, _infcx: &InferCtxt<'db>, ) -> Vec> { - self.obligations - .pending - .drain(..) - .map(|(obligation, _)| NextSolverError::Ambiguity(obligation)) - .chain(self.obligations.overflowed.drain(..).map(NextSolverError::Overflow)) - .collect() + let mut errors: Vec<_> = + self.obligations.drain_pending().map(NextSolverError::Ambiguity).collect(); + errors.extend(self.obligations.overflowed.drain(..).map(NextSolverError::Overflow)); + errors } pub(crate) fn try_evaluate_obligations( @@ -162,22 +365,57 @@ impl<'db> FulfillmentCtxt<'db> { assert_eq!(self.usable_in_snapshot, infcx.num_open_snapshots()); self.try_evaluate_obligations_scratch.clear(); let mut errors = Vec::new(); + // Whether to re-evaluate the `unwatchable` obligations this round: once per call, and + // again after every round that made real inference progress (the cadence at which the + // old code re-evaluated everything still pending). Never merely because the previous + // round's batch was non-empty — an obligation that stays ambiguous without an + // attachable stall set would then requeue itself forever. + let mut recheck_unwatchable = true; loop { - let mut any_changed = false; - self.try_evaluate_obligations_scratch.extend(self.obligations.drain_pending(|_| true)); - for (mut obligation, stalled_on) in self.try_evaluate_obligations_scratch.drain(..) { - // If we've evaluated this goal before and it stalled on a specific set of - // inference variables, and none of those variables have been touched since, - // evaluating it again is guaranteed to produce the same `Maybe` result (goal - // evaluation is pure). Skip it entirely rather than re-running the solver, so - // that re-processing pending obligations is proportional to how much changed - // since the last call, not to the total number of pending obligations. - if let Some(stalled_on) = &stalled_on - && is_still_stalled(infcx, stalled_on) - { - self.obligations.register(obligation, Some(stalled_on.clone())); - continue; + // Gather this round's work: obligations that were newly registered, obligations we + // can't watch (periodically), and obligations parked on an inference variable that + // changed since we last looked — whether the change came from our own previous + // round, from solver entry points between calls, or from a nested fulfillment + // context sharing this infer context. + let batch = &mut self.try_evaluate_obligations_scratch; + batch.extend(self.obligations.fresh.drain(..).map(|o| (o, None))); + if recheck_unwatchable { + batch.append(&mut self.obligations.unwatchable); + } + if self.obligations.watch.is_empty() { + // Nothing is parked on any variable (every slab entry has at least the + // `Opaques` watch key), so skip scanning the changed-variable events and just + // advance past them. + self.changed_vars_cursor = infcx.changed_vars_len(); + } else { + self.changed_vars_scratch.clear(); + infcx.changed_vars_since( + &mut self.changed_vars_cursor, + &mut self.changed_vars_scratch, + ); + for &key in &self.changed_vars_scratch { + self.obligations.wake(key, batch); } + } + self.obligations.assert_no_missed_wakes(infcx); + if batch.is_empty() { + break; + } + recheck_unwatchable = false; + + for (mut obligation, stalled_on) in self.try_evaluate_obligations_scratch.drain(..) { + // If this goal stalled on a specific set of inference variables before and none + // of them has really changed (the wake-up was spurious — e.g. caused by a + // mutation that was rolled back since), evaluating it again is guaranteed to + // produce the same `Maybe` result (goal evaluation is pure), so park it again + // without touching the solver. + let stalled_on = match stalled_on { + Some(stalled_on) if is_still_stalled(infcx, &stalled_on) => { + self.obligations.park_verified(obligation, stalled_on); + continue; + } + stalled_on => stalled_on, + }; if obligation.recursion_depth >= infcx.interner.recursion_limit() { self.obligations.on_fulfillment_overflow(infcx); @@ -193,8 +431,11 @@ impl<'db> FulfillmentCtxt<'db> { match certainty { Certainty::Yes => {} Certainty::Maybe { .. } => { - self.obligations - .register(obligation, fast_path_stalled_on(infcx, goal)); + self.obligations.park( + infcx, + obligation, + fast_path_stalled_on(infcx, goal), + ); } } continue; @@ -222,18 +463,14 @@ impl<'db> FulfillmentCtxt<'db> { // approximation and should only result in fulfillment overflow in // pathological cases. obligation.recursion_depth += 1; - any_changed = true; + recheck_unwatchable = true; } match certainty { Certainty::Yes => {} - Certainty::Maybe { .. } => self.obligations.register(obligation, stalled_on), + Certainty::Maybe { .. } => self.obligations.park(infcx, obligation, stalled_on), } } - - if !any_changed { - break; - } } errors @@ -274,23 +511,22 @@ impl<'db> FulfillmentCtxt<'db> { return Default::default(); } - self.obligations - .drain_pending(|obl| { - infcx.probe(|_| { - infcx - .visit_proof_tree( - obl.as_goal(), - &mut StalledOnCoroutines { - stalled_coroutines, - span: obl.cause.span(), - cache: Default::default(), - }, - ) - .is_break() - }) + let is_stalled_on_coroutine = |obl: &PredicateObligation<'db>| { + infcx.probe(|_| { + infcx + .visit_proof_tree( + obl.as_goal(), + &mut StalledOnCoroutines { + stalled_coroutines, + span: obl.cause.span(), + cache: Default::default(), + }, + ) + .is_break() }) - .map(|(o, _)| o) - .collect() + }; + + self.obligations.extract_pending_if(is_stalled_on_coroutine) } } @@ -318,7 +554,7 @@ fn is_still_stalled<'db>( /// we would lose all stall-tracking for the obligations it intercepts, forcing them through the /// fast path again on every single call to `try_evaluate_obligations` for the rest of the body. /// We over-approximate by collecting every inference variable mentioned in the goal's -/// predicate: extra entries only make `is_still_stalled` wake the goal on unrelated changes, +/// predicate: extra entries only cause spurious wake-ups and rechecks on unrelated changes, /// which is wasteful but never unsound, whereas missing one could permanently mask real /// progress. fn fast_path_stalled_on<'db>( @@ -335,11 +571,25 @@ fn fast_path_stalled_on<'db>( type Result = (); fn visit_ty(&mut self, ty: Ty<'db>) { + // Record variables under their current unification root: the interned predicate + // can mention variables that have since been unioned into another root, and + // `is_still_stalled` treats a non-root variable as always-changed, so recording + // such a variable raw would invalidate the stall set on arrival (the obligation + // would never park). match ty.kind() { TyKind::Infer(InferTy::TyVar(vid)) => { - self.stalled_vars.push(ty.into()); + let root = self.infcx.root_var(vid); + self.stalled_vars.push(Ty::new_var(self.infcx.interner, root).into()); self.sub_roots.push(self.infcx.sub_unification_table_root_var(vid)); } + TyKind::Infer(InferTy::IntVar(vid)) => { + let root = self.infcx.root_int_var(vid); + self.stalled_vars.push(Ty::new_int_var(self.infcx.interner, root).into()); + } + TyKind::Infer(InferTy::FloatVar(vid)) => { + let root = self.infcx.root_float_var(vid); + self.stalled_vars.push(Ty::new_float_var(self.infcx.interner, root).into()); + } TyKind::Infer(_) => self.stalled_vars.push(ty.into()), _ if ty.has_infer() => ty.super_visit_with(self), _ => {} @@ -348,7 +598,10 @@ fn fast_path_stalled_on<'db>( fn visit_const(&mut self, ct: Const<'db>) { match ct.kind() { - ConstKind::Infer(InferConst::Var(_)) => self.stalled_vars.push(ct.into()), + ConstKind::Infer(InferConst::Var(vid)) => { + let root = self.infcx.root_const_var(vid); + self.stalled_vars.push(Const::new_var(self.infcx.interner, root).into()); + } _ if ct.has_infer() => ct.super_visit_with(self), _ => {} } diff --git a/crates/hir-ty/src/next_solver/infer/mod.rs b/crates/hir-ty/src/next_solver/infer/mod.rs index 7c419147d214..075aae644055 100644 --- a/crates/hir-ty/src/next_solver/infer/mod.rs +++ b/crates/hir-ty/src/next_solver/infer/mod.rs @@ -59,6 +59,7 @@ pub mod relate; pub mod resolve; pub mod select; pub(crate) mod snapshot; +pub(crate) use snapshot::StalledVarKey; pub mod traits; mod type_variable; mod unify_key; @@ -878,7 +879,7 @@ impl<'db> InferCtxt<'db> { pub fn take_opaque_types( &self, ) -> impl IntoIterator, OpaqueHiddenType<'db>)> + use<'db> { - self.inner.borrow_mut().opaque_type_storage.take_opaque_types() + self.inner.borrow_mut().opaque_types().take_opaque_types() } #[instrument(level = "debug", skip(self), ret)] @@ -1004,6 +1005,14 @@ impl<'db> InferCtxt<'db> { self.inner.borrow_mut().const_unification_table().find(var).vid } + pub(crate) fn root_int_var(&self, var: IntVid) -> IntVid { + self.inner.borrow_mut().int_unification_table().find(var) + } + + pub(crate) fn root_float_var(&self, var: FloatVid) -> FloatVid { + self.inner.borrow_mut().float_unification_table().find(var) + } + /// Resolves an int var to a rigid int type, if it was constrained to one, /// or else the root int var in the unification table. pub fn opportunistic_resolve_int_var(&self, vid: IntVid) -> Ty<'db> { diff --git a/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs b/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs index bce877fde8d7..7133f2928e4e 100644 --- a/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs +++ b/crates/hir-ty/src/next_solver/infer/opaque_types/table.rs @@ -168,4 +168,15 @@ impl<'a, 'db> OpaqueTypeTable<'a, 'db> { self.storage.duplicate_entries.push((key, hidden_type)); self.undo_log.push(UndoLog::DuplicateOpaqueType); } + + pub(crate) fn take_opaque_types( + &mut self, + ) -> impl IntoIterator, OpaqueHiddenType<'db>)> + use<'db> { + if !self.storage.is_empty() { + // Draining the storage shrinks the entry count without going through the undo + // machinery; record it so obligations stalled on the opaque count get rechecked. + self.undo_log.mark_opaques_changed(); + } + self.storage.take_opaque_types() + } } diff --git a/crates/hir-ty/src/next_solver/infer/region_constraints/mod.rs b/crates/hir-ty/src/next_solver/infer/region_constraints/mod.rs index 544d79daf0a3..d577f7362562 100644 --- a/crates/hir-ty/src/next_solver/infer/region_constraints/mod.rs +++ b/crates/hir-ty/src/next_solver/infer/region_constraints/mod.rs @@ -323,7 +323,12 @@ impl<'db> RegionConstraintCollector<'db, '_> { /// /// Not legal during a snapshot. pub fn take_and_reset_data(&mut self) -> RegionConstraintData<'db> { - assert!(!UndoLogs::>::in_snapshot(&self.undo_log)); + // The *inherent* method, not the always-`true` trait impl; see `InferCtxtUndoLogs`. + // The explicit deref is needed because `ena` blanket-implements `UndoLogs` for + // `&mut U`, so on the reference itself only the trait method exists. Removing the + // deref is a compile error (E0283, ambiguous `T` in `UndoLogs`), not a silent + // switch to the trait impl. + assert!(!(*self.undo_log).in_snapshot()); // If you add a new field to `RegionConstraintCollector`, you // should think carefully about whether it needs to be cleared diff --git a/crates/hir-ty/src/next_solver/infer/snapshot/mod.rs b/crates/hir-ty/src/next_solver/infer/snapshot/mod.rs index 39c8a37adbdb..8ee6fc7a9719 100644 --- a/crates/hir-ty/src/next_solver/infer/snapshot/mod.rs +++ b/crates/hir-ty/src/next_solver/infer/snapshot/mod.rs @@ -10,6 +10,7 @@ use super::region_constraints::RegionSnapshot; mod fudge; pub(crate) mod undo_log; +pub(crate) use undo_log::StalledVarKey; use undo_log::{Snapshot, UndoLog}; #[must_use = "once you start a snapshot, you should always consume it"] @@ -40,13 +41,31 @@ impl<'db> InferCtxt<'db> { } pub fn in_snapshot(&self) -> bool { - UndoLogs::>::in_snapshot(&self.inner.borrow_mut().undo_log) + // The *inherent* method, not the always-`true` trait impl; see `InferCtxtUndoLogs`. + self.inner.borrow().undo_log.in_snapshot() } pub fn num_open_snapshots(&self) -> usize { UndoLogs::>::num_open_snapshots(&self.inner.borrow_mut().undo_log) } + /// The current length of the changed-inference-variables log, i.e. a cursor position from + /// which [`Self::changed_vars_since`] later reports what changed. See + /// [`undo_log::InferCtxtUndoLogs`]. + pub(crate) fn changed_vars_len(&self) -> usize { + self.inner.borrow().undo_log.changed_vars_len() + } + + /// Appends to `out` the variables that (potentially) changed since `cursor` (a position + /// previously obtained from [`Self::changed_vars_len`]), then advances `cursor` past them. + /// Reading is non-destructive, so any number of consumers can watch the same infer context + /// with their own cursors. + pub(crate) fn changed_vars_since(&self, cursor: &mut usize, out: &mut Vec) { + let inner = self.inner.borrow(); + out.extend_from_slice(inner.undo_log.changed_vars_since(*cursor)); + *cursor = inner.undo_log.changed_vars_len(); + } + pub fn start_snapshot(&self) -> CombinedSnapshot { debug!("start_snapshot()"); @@ -123,3 +142,102 @@ impl<'db> InferCtxt<'db> { self.inner.borrow().undo_log.opaque_types_in_snapshot(&snapshot.undo_snapshot) } } + +#[cfg(test)] +mod tests { + use rustc_type_ir::{TypingMode, inherent::Ty as _}; + use test_fixture::WithFixture; + + use crate::{ + next_solver::{ + DbInterner, Ty, + infer::{DbInternerInferExt, InferCtxt, StalledVarKey}, + }, + test_db::TestDB, + }; + + fn with_infcx(f: impl FnOnce(&InferCtxt<'_>)) { + let (db, file_id) = TestDB::with_single_file("fn f() {}"); + crate::attach_db(&db, || { + let krate = db.module_for_file(file_id.file_id(&db)).krate(&db); + let interner = DbInterner::new_with(&db, krate); + let infcx = interner.infer_ctxt().build(TypingMode::non_body_analysis()); + f(&infcx); + }); + } + + fn new_ty_var(infcx: &InferCtxt<'_>) -> rustc_type_ir::TyVid { + infcx.next_ty_vid(crate::Span::Dummy) + } + + fn changed_since(infcx: &InferCtxt<'_>, cursor: &mut usize) -> Vec { + let mut out = Vec::new(); + infcx.changed_vars_since(cursor, &mut out); + out + } + + /// A mutation performed while *no* snapshot is open must still be reported: `ena` gates + /// its undo-log `push` calls on `in_snapshot()`, which `InferCtxtUndoLogs` deliberately + /// short-circuits to `true` to keep this log complete. A lost event here would leave a + /// `FulfillmentCtxt` obligation parked on the variable with a stale `Certainty::Maybe` + /// forever, i.e. wrong inference results rather than a crash. + #[test] + fn zero_snapshot_mutation_is_reported() { + with_infcx(|infcx| { + assert_eq!(infcx.num_open_snapshots(), 0); + let vid = new_ty_var(infcx); + let mut cursor = infcx.changed_vars_len(); + infcx + .inner + .borrow_mut() + .type_variables() + .instantiate(vid, Ty::new_bool(infcx.interner)); + assert!(changed_since(infcx, &mut cursor).contains(&StalledVarKey::Ty(vid))); + }); + } + + /// Unioning two variables must be observable through *both* variables' identities, and + /// instantiating the surviving root afterwards must be observable through that root: a + /// watcher keyed on either original variable is woken by the union, re-registers under + /// the merged root, and is then woken again by the instantiation. + #[test] + fn union_then_instantiate_root_is_reported() { + with_infcx(|infcx| { + let v1 = new_ty_var(infcx); + let v2 = new_ty_var(infcx); + let mut cursor = infcx.changed_vars_len(); + infcx.inner.borrow_mut().type_variables().equate(v1, v2); + let after_union = changed_since(infcx, &mut cursor); + assert!(after_union.contains(&StalledVarKey::Ty(v1))); + assert!(after_union.contains(&StalledVarKey::Ty(v2))); + + let root = infcx.root_var(v1); + assert_eq!(root, infcx.root_var(v2)); + infcx + .inner + .borrow_mut() + .type_variables() + .instantiate(root, Ty::new_bool(infcx.interner)); + assert!(changed_since(infcx, &mut cursor).contains(&StalledVarKey::Ty(root))); + }); + } + + /// Changes made inside a probe are rolled back, but the wake events they produced must + /// stick around: waking an obligation spuriously is harmless, missing a wake is not, and + /// a consumer may only get to read the log after the rollback already happened. + #[test] + fn rolled_back_mutation_still_reported() { + with_infcx(|infcx| { + let vid = new_ty_var(infcx); + let mut cursor = infcx.changed_vars_len(); + infcx.probe(|_| { + infcx + .inner + .borrow_mut() + .type_variables() + .instantiate(vid, Ty::new_bool(infcx.interner)); + }); + assert!(changed_since(infcx, &mut cursor).contains(&StalledVarKey::Ty(vid))); + }); + } +} diff --git a/crates/hir-ty/src/next_solver/infer/snapshot/undo_log.rs b/crates/hir-ty/src/next_solver/infer/snapshot/undo_log.rs index f246af1f2e53..1392e4519a53 100644 --- a/crates/hir-ty/src/next_solver/infer/snapshot/undo_log.rs +++ b/crates/hir-ty/src/next_solver/infer/snapshot/undo_log.rs @@ -3,8 +3,10 @@ use ena::snapshot_vec as sv; use ena::undo_log::{Rollback, UndoLogs}; use ena::unify as ut; +use rustc_type_ir::ConstVid; use rustc_type_ir::FloatVid; use rustc_type_ir::IntVid; +use rustc_type_ir::TyVid; use tracing::debug; use crate::next_solver::OpaqueTypeKey; @@ -13,6 +15,60 @@ use crate::next_solver::infer::unify_key::ConstVidKey; use crate::next_solver::infer::unify_key::RegionVidKey; use crate::next_solver::infer::{InferCtxtInner, region_constraints, type_variable}; +/// Identifies something a `FulfillmentCtxt` obligation can be stalled on: a specific inference +/// variable, or the opaque type storage as a whole (mirroring `GoalStalledOn::num_opaques`, +/// which isn't tied to any single variable). +/// +/// Used both to index a parked obligation under the variables it is blocked on and, via +/// [`InferCtxtUndoLogs::changed_vars`], to report which of those have potentially changed, so +/// the obligation can be woken without re-scanning everything that's still genuinely stalled. +/// +/// Variable keys always refer to the *unification root* at the time the key was created: `ena` +/// applies value changes to root slots, and a union touches both involved roots, so an event +/// stream keyed by root indices is complete as long as watchers re-normalize their keys +/// whenever they are woken (a union wakes the old root's watchers, which then re-register +/// under the new root). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum StalledVarKey { + /// The `eq_relations` slot of this type variable changed (unified or instantiated). + Ty(TyVid), + /// The `sub_unification_table` slot of this type variable changed. + TySubRoot(TyVid), + Int(IntVid), + Float(FloatVid), + Const(ConstVid), + /// The opaque type storage changed (an opaque type was registered, replaced, or drained). + Opaques, +} + +/// Best-effort extraction of the variable a given undo entry pertains to. Only entries that +/// represent a *value* changing (`SetElem`) carry a meaningful variable identity: `NewElem` +/// (a fresh, still-unconstrained variable was created) cannot wake anything, because no +/// obligation can have been registered as stalled on a variable that didn't exist yet. +fn changed_var_key(undo: &UndoLog<'_>) -> Option { + match undo { + UndoLog::TypeVariables(type_variable::UndoLog::EqRelation(sv::UndoLog::SetElem( + idx, + _, + ))) => Some(StalledVarKey::Ty(TyVid::from_u32(*idx as u32))), + UndoLog::TypeVariables(type_variable::UndoLog::SubRelation(sv::UndoLog::SetElem( + idx, + _, + ))) => Some(StalledVarKey::TySubRoot(TyVid::from_u32(*idx as u32))), + UndoLog::IntUnificationTable(sv::UndoLog::SetElem(idx, _)) => { + Some(StalledVarKey::Int(IntVid::from_u32(*idx as u32))) + } + UndoLog::FloatUnificationTable(sv::UndoLog::SetElem(idx, _)) => { + Some(StalledVarKey::Float(FloatVid::from_u32(*idx as u32))) + } + UndoLog::ConstUnificationTable(sv::UndoLog::SetElem(idx, _)) => { + Some(StalledVarKey::Const(ConstVid::from_u32(*idx as u32))) + } + UndoLog::OpaqueTypes(..) | UndoLog::DuplicateOpaqueType => Some(StalledVarKey::Opaques), + _ => None, + } +} + pub struct Snapshot { pub(crate) undo_len: usize, } @@ -93,6 +149,16 @@ impl<'db> Rollback> for InferCtxtInner<'db> { pub(crate) struct InferCtxtUndoLogs<'db> { logs: Vec>, num_open_snapshots: usize, + /// Variables observed to have (potentially) changed, for waking `FulfillmentCtxt` + /// obligations that were parked on them. + /// + /// Unlike `logs`, this is appended to unconditionally, not just while a snapshot is open, + /// and it is never truncated by rollback: over-reporting a variable as changed (e.g. one + /// whose change was later rolled back, or a mere path-compression write) only causes an + /// obligation to be needlessly re-checked, never a missed wake-up. Consumers each keep a + /// cursor into this log (see `FulfillmentCtxt`) rather than draining it, so several + /// consumers can share one infer context without stealing each other's wake signals. + changed_vars: Vec, } /// The UndoLogs trait defines how we undo a particular kind of action (of type T). We can undo any @@ -106,27 +172,37 @@ where self.num_open_snapshots } + /// Deliberately claims a snapshot is always open. `ena`'s mutation methods + /// (`SnapshotVec::{set, update}` and everything in `unify` built on them) consult this + /// *before* calling [`Self::push`] and skip the call entirely when it returns false, which + /// would make `changed_vars` silently miss every mutation performed while no snapshot is + /// open. Answering `true` routes all mutations through `push`/`extend`, which record the + /// changed variable unconditionally and store the actual undo entry only when a snapshot + /// is really open (`num_open_snapshots > 0`), preserving the original rollback behavior. + #[inline] + fn in_snapshot(&self) -> bool { + true + } + #[inline] fn push(&mut self, undo: T) { - if self.in_snapshot() { - self.logs.push(undo.into()) + let undo = undo.into(); + if let Some(key) = changed_var_key(&undo) { + self.changed_vars.push(key); + } + if self.num_open_snapshots > 0 { + self.logs.push(undo) } } fn clear(&mut self) { + // Note that `changed_vars` is intentionally left alone: it is append-only for the + // lifetime of the infer context so that consumer cursors stay valid. self.logs.clear(); self.num_open_snapshots = 0; } - fn extend(&mut self, undos: J) - where - Self: Sized, - J: IntoIterator, - { - if self.in_snapshot() { - self.logs.extend(undos.into_iter().map(UndoLog::from)) - } - } + // `extend` is left at its provided default, which forwards to `push` element-wise. } impl<'db> InferCtxtInner<'db> { @@ -166,6 +242,13 @@ impl<'db> InferCtxtInner<'db> { } impl<'db> InferCtxtUndoLogs<'db> { + /// Whether a snapshot is really open. This inherent method shadows the + /// [`UndoLogs::in_snapshot`] trait method, which deliberately always answers `true` (see + /// the trait impl above for why); anything wanting the real state must call this one. + pub(crate) fn in_snapshot(&self) -> bool { + self.num_open_snapshots > 0 + } + pub(crate) fn start_snapshot(&mut self) -> Snapshot { self.num_open_snapshots += 1; Snapshot { undo_len: self.logs.len() } @@ -190,6 +273,27 @@ impl<'db> InferCtxtUndoLogs<'db> { assert!(self.logs.len() >= snapshot.undo_len); assert!(self.num_open_snapshots > 0); } + + /// The current length of the changed-variables log. Positions before this are in the past; + /// a consumer that has processed everything up to `len` can later ask for + /// [`Self::changed_vars_since`] that position to see only what changed in between. + pub(crate) fn changed_vars_len(&self) -> usize { + self.changed_vars.len() + } + + /// The variables that (potentially) changed since `cursor`, a position previously obtained + /// from [`Self::changed_vars_len`]. The log is append-only, so this is stable across + /// snapshots, rollbacks, and other consumers reading their own cursors. + pub(crate) fn changed_vars_since(&self, cursor: usize) -> &[StalledVarKey] { + &self.changed_vars[cursor.min(self.changed_vars.len())..] + } + + /// Records an opaque-type-storage change that doesn't go through the undo machinery + /// (draining the storage via `take_opaque_types` shrinks the entry count without pushing + /// any undo entry). + pub(crate) fn mark_opaques_changed(&mut self) { + self.changed_vars.push(StalledVarKey::Opaques); + } } impl<'db> std::ops::Index for InferCtxtUndoLogs<'db> { diff --git a/crates/hir-ty/src/tests/regression.rs b/crates/hir-ty/src/tests/regression.rs index 0d08c75aad73..1ba74246cf6a 100644 --- a/crates/hir-ty/src/tests/regression.rs +++ b/crates/hir-ty/src/tests/regression.rs @@ -131,8 +131,8 @@ fn recursive_vars() { expect![[r#" 10..47 '{ ...&y]; }': () 20..21 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 24..31 'unknown': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 37..44 '[y, &y]': [&'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}; 2] + 24..31 'unknown': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 37..44 '[y, &y]': [&'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}; 2] 38..39 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 41..43 '&y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 42..43 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} @@ -152,19 +152,19 @@ fn recursive_vars_2() { "#, expect![[r#" 10..79 '{ ...x)]; }': () - 20..21 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 20..21 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 24..31 'unknown': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 41..42 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 45..52 'unknown': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 58..76 '[(x, y..., &x)]': [(&'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}, &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}); 2] 59..65 '(x, y)': (&'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}, &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}) - 60..61 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 60..61 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 63..64 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 67..75 '(&y, &x)': (&'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}, &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown}) 68..70 '&y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 69..70 'y': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 72..74 '&x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 73..74 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 72..74 '&x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 73..74 'x': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} "#]], ); } @@ -309,7 +309,7 @@ fn infer_std_crash_5() { 32..320 'for co... }': () 32..320 'for co... }': () 32..320 'for co... }': () - 36..43 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 36..43 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 47..60 'doesnt_matter': {unknown} 47..60 'doesnt_matter': fn into_iter<{unknown}>({unknown}) -> <{unknown} as IntoIterator>::IntoIter 47..60 'doesnt_matter': <{unknown} as IntoIterator>::IntoIter @@ -318,20 +318,20 @@ fn infer_std_crash_5() { 82..166 'if doe... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 85..98 'doesnt_matter': bool 99..128 '{ ... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 113..118 'first': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 113..118 'first': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 134..166 '{ ... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 148..156 '&content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 149..156 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 181..188 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 191..313 'if ICE... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 148..156 '&content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 149..156 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 181..188 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 191..313 'if ICE... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 194..231 'ICE_RE..._VALUE': {unknown} 194..247 'ICE_RE...&name)': bool 241..246 '&name': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 242..246 'name': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 248..276 '{ ... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} 262..266 'name': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 282..313 '{ ... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} - 296..303 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 282..313 '{ ... }': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} + 296..303 'content': &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? &'? {unknown} "#]], ); }