diff --git a/src/typing/checker.rs b/src/typing/checker.rs index a433491..7243deb 100644 --- a/src/typing/checker.rs +++ b/src/typing/checker.rs @@ -1124,21 +1124,25 @@ impl Typechecker { // Refinement helpers // ----------------------------------------------- - fn refine_pattern_node(&mut self, desc: &Option) -> VariableType { + fn refine_pattern_node(&mut self, desc: &Option) -> std::rc::Rc { let dtype = descriptor_type_of(desc); if let Some(d) = desc { self.assert_filters_drained(d); } let vt = VariableType::Node(dtype.clone()); - let refined = VariableType::refine(&self.schema, &vt); - if matches!(refined, VariableType::Zero) { + let refined = VariableType::refine_rc(&self.schema, &vt); + if matches!(&*refined, VariableType::Zero) { self.warnings .push(diagnose_node_mismatch(&self.schema, &dtype)); } refined } - fn refine_pattern_edge(&mut self, dir: EdgeDir, desc: &Option) -> VariableType { + fn refine_pattern_edge( + &mut self, + dir: EdgeDir, + desc: &Option, + ) -> std::rc::Rc { let dtype = descriptor_type_of(desc); if let Some(d) = desc { self.assert_filters_drained(d); @@ -1148,10 +1152,11 @@ impl Typechecker { // as directional silently drops every undirected schema entry — e.g. // `knows` in LDBC, which is registered as non-directional. let refined = match dir { - EdgeDir::Right | EdgeDir::Left => { - VariableType::refine(&self.schema, &VariableType::edge_directional(dtype.clone())) - } - EdgeDir::None => VariableType::refine( + EdgeDir::Right | EdgeDir::Left => VariableType::refine_rc( + &self.schema, + &VariableType::edge_directional(dtype.clone()), + ), + EdgeDir::None => VariableType::refine_rc( &self.schema, &VariableType::edge_non_directional(dtype.clone()), ), @@ -1164,10 +1169,10 @@ impl Typechecker { &self.schema, &VariableType::edge_non_directional(dtype.clone()), ); - VariableType::join(t_fwd, t_und) + std::rc::Rc::new(VariableType::join(t_fwd, t_und)) } }; - if matches!(refined, VariableType::Zero) { + if matches!(&*refined, VariableType::Zero) { self.warnings .push(diagnose_edge_mismatch(&self.schema, &dtype, dir)); } @@ -1249,9 +1254,9 @@ fn descriptor_type_of(desc: &Option) -> DescriptorType { /// Build the binding environment for a pattern position. Anonymous patterns /// contribute no bindings. -fn create_context(desc: &Option, t: VariableType) -> TypeEnvironment { +fn create_context(desc: &Option, t: std::rc::Rc) -> TypeEnvironment { match desc { - Some(d) => TypeEnvironment::create_context(d, t), + Some(d) => TypeEnvironment::create_context_shared(d, t), None => TypeEnvironment::new(), } } diff --git a/src/typing/path_summary.rs b/src/typing/path_summary.rs index 7bd96f6..d39076d 100644 --- a/src/typing/path_summary.rs +++ b/src/typing/path_summary.rs @@ -197,30 +197,11 @@ impl PathSummary { pub fn meet(schema: &Schema, a: &PathSummary, b: &PathSummary) -> PathSummary { super::stats::record_pathtype_meet(); let mut out = PathSummary::zero(); - // The junction only depends on (a.last, b.first). Memoized by - // pointer identity of the operand descriptors — w×w arm combos - // cost w distinct refinements; a value-equal miss just recomputes. - let mut junctions: Vec<( - *const DescriptorType, - *const DescriptorType, - Vec, - )> = Vec::new(); - let mut junction = |l1: &DescriptorType, f2: &DescriptorType| -> Vec { - let key = (l1 as *const _, f2 as *const _); - if let Some((_, _, rs)) = junctions.iter().find(|(p1, p2, _)| (*p1, *p2) == key) { - return rs.clone(); - } - let met = VariableType::Node(DescriptorType::meet(l1, f2)); - let rs: Vec = VariableType::refine_to_nodes(schema, &met) - .into_iter() - .filter_map(|v| match v { - VariableType::Node(d) => Some(d), - _ => None, - }) - .collect(); - junctions.push((key.0, key.1, rs.clone())); - rs - }; + // The junction only depends on (a.last, b.first) and is memoized + // on the Schema itself (`Schema::junction_nodes`) — cross-hop AND + // cross-query: a chain reuses one junction at every position, a + // REPL session across queries. A hit is an `Rc` bump. + let junction = |l1: &DescriptorType, f2: &DescriptorType| schema.junction_nodes(l1, f2); // pairs × pairs: junction interior, outer boundaries survive. for (f1, l1, len1) in &a.pairs { @@ -234,16 +215,16 @@ impl PathSummary { // refined junction becomes the result's last boundary. for (f1, l1, len1) in &a.pairs { for d in &b.nodes { - for r in junction(l1, d) { - out.insert_pair(f1.clone(), r, *len1); + for r in junction(l1, d).iter() { + out.insert_pair(f1.clone(), r.clone(), *len1); } } } // nodes × pairs: symmetric — refined junction becomes first. for d in &a.nodes { for (f2, l2, len2) in &b.pairs { - for r in junction(d, f2) { - out.insert_pair(r, l2.clone(), *len2); + for r in junction(d, f2).iter() { + out.insert_pair(r.clone(), l2.clone(), *len2); } } } @@ -251,8 +232,8 @@ impl PathSummary { // node itself. for d1 in &a.nodes { for d2 in &b.nodes { - for r in junction(d1, d2) { - out.insert_node(r); + for r in junction(d1, d2).iter() { + out.insert_node(r.clone()); } } } diff --git a/src/typing/type_environment.rs b/src/typing/type_environment.rs index 7d6e553..4b3055e 100644 --- a/src/typing/type_environment.rs +++ b/src/typing/type_environment.rs @@ -25,9 +25,15 @@ impl TypeEnvironment { /// If the descriptor has a variable name, bind it to `t`. /// If not, return an empty environment. pub fn create_context(descriptor: &Descriptor, t: VariableType) -> Self { + TypeEnvironment::create_context_shared(descriptor, Rc::new(t)) + } + + /// `create_context` for an already-shared binding (refine-cache hits + /// arrive as `Rc`; re-wrapping would deep-clone for nothing). + pub fn create_context_shared(descriptor: &Descriptor, t: Rc) -> Self { let mut env = TypeEnvironment::new(); if let Some(var) = &descriptor.var { - env.set(var, t); + env.set_shared(var, t); } env } @@ -101,7 +107,7 @@ impl TypeEnvironment { key, self_t, other )); } - Rc::new(VariableType::refine(schema, &met)) + VariableType::refine_rc(schema, &met) } // Right-only key: keep the binding as-is. None => Rc::clone(other), diff --git a/src/typing/variable_type.rs b/src/typing/variable_type.rs index e0941d6..7a39806 100644 --- a/src/typing/variable_type.rs +++ b/src/typing/variable_type.rs @@ -285,14 +285,18 @@ impl VariableType { /// `VariableType::refine_to_nodes` and is consumed by `PathType::meet`. pub fn refine_to_nodes(schema: &Schema, t: &VariableType) -> Vec { super::stats::record_refine_to_nodes(); + // Borrow-walk the (possibly cache-shared) refined tree and clone + // only the matching Node leaves — a cache hit no longer deep- + // clones the whole refined union up front. + let refined = VariableType::refine_rc(schema, t); let mut out = Vec::new(); - let mut stack = vec![VariableType::refine(schema, t)]; + let mut stack: Vec<&VariableType> = vec![&refined]; while let Some(curr) = stack.pop() { match curr { - VariableType::Node(_) => out.push(curr), + VariableType::Node(_) => out.push(curr.clone()), VariableType::Union(t1, t2) => { - stack.push(*t2); - stack.push(*t1); + stack.push(t2); + stack.push(t1); } _ => {} } @@ -302,7 +306,34 @@ impl VariableType { // --- Refine --- + /// By-value refine, kept for callers that need an owned result (and + /// for the reference models in `lattice_proptest`). The scan arms + /// delegate to [`VariableType::refine_rc`], so they share the memo. pub fn refine(schema: &Schema, node: &VariableType) -> VariableType { + match node { + VariableType::Node(_) + | VariableType::EdgeDirectional { .. } + | VariableType::EdgeNonDirectional { .. } => { + (*VariableType::refine_rc(schema, node)).clone() + } + VariableType::Union(t1, t2) => VariableType::join( + VariableType::refine(schema, t1), + VariableType::refine(schema, t2), + ), + VariableType::Group(t) => { + VariableType::Group(Box::new(VariableType::refine(schema, t))) + } + VariableType::Null => VariableType::Null, + VariableType::Path => VariableType::Path, + VariableType::Zero => VariableType::Zero, + } + } + + /// `Rc`-valued refine: on the Node/Edge scan arms a memo hit is a + /// refcount bump instead of a deep clone of the refined tree. This is + /// the form the checker and the environment operators consume — they + /// store bindings as `Rc` anyway. + pub fn refine_rc(schema: &Schema, node: &VariableType) -> Rc { match node { VariableType::Node(_) => { if !refine_cache_disabled() { @@ -318,9 +349,9 @@ impl VariableType { .filter(|n| VariableType::is_subtype(n, node)) .map(|n| VariableType::meet(n, node)) .collect(); - let refined = VariableType::join_from_list(matches); + let refined = Rc::new(VariableType::join_from_list(matches)); if !refine_cache_disabled() { - schema.refine_cache_put(node.clone(), refined.clone()); + schema.refine_cache_put(node.clone(), Rc::clone(&refined)); } refined } @@ -338,22 +369,13 @@ impl VariableType { .filter(|e| VariableType::is_subtype(e, node)) .map(|e| VariableType::meet(e, node)) .collect(); - let refined = VariableType::join_from_list(matches); + let refined = Rc::new(VariableType::join_from_list(matches)); if !refine_cache_disabled() { - schema.refine_cache_put(node.clone(), refined.clone()); + schema.refine_cache_put(node.clone(), Rc::clone(&refined)); } refined } - VariableType::Union(t1, t2) => VariableType::join( - VariableType::refine(schema, t1), - VariableType::refine(schema, t2), - ), - VariableType::Group(t) => { - VariableType::Group(Box::new(VariableType::refine(schema, t))) - } - VariableType::Null => VariableType::Null, - VariableType::Path => VariableType::Path, - VariableType::Zero => VariableType::Zero, + other => Rc::new(VariableType::refine(schema, other)), } } @@ -415,10 +437,29 @@ pub struct Schema { /// cross-query for REPL/Connection lifetimes) and safely invalidated by /// construction: DDL and inference replace the whole `Schema`, never /// mutate one in place, so a cache can never outlive its entries. - /// Skipped by serde — a deserialized schema starts cold. - /// `GQLITE_DISABLE_TC_REFINE_CACHE=1` bypasses it (A/B kill switch). + /// Values are `Rc` so a hit is a refcount bump, not a deep clone of the + /// refined descriptor tree. Skipped by serde — a deserialized schema + /// starts cold. `GQLITE_DISABLE_TC_REFINE_CACHE=1` bypasses it (A/B + /// kill switch). + #[serde(skip, default)] + refine_cache: Rc>>>, + /// Memo for `PathSummary::meet`'s junction refinement: for a boundary + /// pair `(last, first)` the satisfiable refined junction node + /// descriptors, i.e. `refine_to_nodes(meet(last, first))` flattened to + /// descriptors. Nested map so lookups need no key clones. Same + /// lifetime/invalidation story as `refine_cache`; cross-query AND + /// cross-hop (chains reuse the same junction at every position). + /// `GQLITE_DISABLE_TC_JUNCTION_CACHE=1` bypasses it. #[serde(skip, default)] - refine_cache: Rc>>, + #[allow(clippy::type_complexity)] + junction_cache: Rc< + std::cell::RefCell< + std::collections::HashMap< + DescriptorType, + std::collections::HashMap>>, + >, + >, + >, } /// Safety valve for adversarial/degenerate sessions: the cache resets when @@ -436,6 +477,7 @@ impl Schema { VariableType::edge_non_directional(DescriptorType::star()), ]), refine_cache: Rc::default(), + junction_cache: Rc::default(), } } @@ -445,26 +487,72 @@ impl Schema { nodes: Rc::new(nodes), edges: Rc::new(edges), refine_cache: Rc::default(), + junction_cache: Rc::default(), } } - fn refine_cache_get(&self, key: &VariableType) -> Option { - self.refine_cache.borrow().get(key).cloned() + fn refine_cache_get(&self, key: &VariableType) -> Option> { + self.refine_cache.borrow().get(key).map(Rc::clone) } - fn refine_cache_put(&self, key: VariableType, value: VariableType) { + fn refine_cache_put(&self, key: VariableType, value: Rc) { let mut m = self.refine_cache.borrow_mut(); if m.len() >= REFINE_CACHE_CAP { m.clear(); } m.insert(key, value); } + + /// The satisfiable refined junction descriptors for a boundary pair — + /// `refine_to_nodes(meet(last, first))` flattened to descriptors, + /// memoized per schema (see `junction_cache`). + pub(crate) fn junction_nodes( + &self, + last: &DescriptorType, + first: &DescriptorType, + ) -> Rc> { + let cache_on = !junction_cache_disabled(); + if cache_on { + if let Some(hit) = self + .junction_cache + .borrow() + .get(last) + .and_then(|m| m.get(first)) + { + return Rc::clone(hit); + } + } + let met = VariableType::Node(DescriptorType::meet(last, first)); + let rs: Rc> = Rc::new( + VariableType::refine_to_nodes(self, &met) + .into_iter() + .filter_map(|v| match v { + VariableType::Node(d) => Some(d), + _ => None, + }) + .collect(), + ); + if cache_on { + let mut m = self.junction_cache.borrow_mut(); + if m.len() >= REFINE_CACHE_CAP { + m.clear(); + } + m.entry(last.clone()) + .or_default() + .insert(first.clone(), Rc::clone(&rs)); + } + rs + } } fn refine_cache_disabled() -> bool { std::env::var("GQLITE_DISABLE_TC_REFINE_CACHE").is_ok() } +fn junction_cache_disabled() -> bool { + std::env::var("GQLITE_DISABLE_TC_JUNCTION_CACHE").is_ok() +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/tc_refine_cache_test.rs b/tests/tc_refine_cache_test.rs index eb2d73b..89259d1 100644 --- a/tests/tc_refine_cache_test.rs +++ b/tests/tc_refine_cache_test.rs @@ -50,16 +50,37 @@ const QUERIES: &[&str] = &[ fn assert_cache_transparent(schema: &Schema, label: &str) { for q in QUERIES { - // Cold + warm with the cache on (second run hits the memo). + // Cold + warm with both caches on (second run hits the memos). std::env::remove_var("GQLITE_DISABLE_TC_REFINE_CACHE"); + std::env::remove_var("GQLITE_DISABLE_TC_JUNCTION_CACHE"); let cold = verdict(schema, q); let warm = verdict(schema, q); - // Cache off. + // Each cache off individually, then both off. std::env::set_var("GQLITE_DISABLE_TC_REFINE_CACHE", "1"); - let off = verdict(schema, q); + let refine_off = verdict(schema, q); std::env::remove_var("GQLITE_DISABLE_TC_REFINE_CACHE"); - assert_eq!(cold, off, "[{label}] cache-on (cold) != cache-off for: {q}"); - assert_eq!(warm, off, "[{label}] cache-on (warm) != cache-off for: {q}"); + std::env::set_var("GQLITE_DISABLE_TC_JUNCTION_CACHE", "1"); + let junction_off = verdict(schema, q); + std::env::set_var("GQLITE_DISABLE_TC_REFINE_CACHE", "1"); + let both_off = verdict(schema, q); + std::env::remove_var("GQLITE_DISABLE_TC_REFINE_CACHE"); + std::env::remove_var("GQLITE_DISABLE_TC_JUNCTION_CACHE"); + assert_eq!( + cold, both_off, + "[{label}] caches-on (cold) != caches-off for: {q}" + ); + assert_eq!( + warm, both_off, + "[{label}] caches-on (warm) != caches-off for: {q}" + ); + assert_eq!( + refine_off, both_off, + "[{label}] refine-off != both-off for: {q}" + ); + assert_eq!( + junction_off, both_off, + "[{label}] junction-off != both-off for: {q}" + ); } }