diff --git a/src/typing/checker.rs b/src/typing/checker.rs index 0544700..18f5a8f 100644 --- a/src/typing/checker.rs +++ b/src/typing/checker.rs @@ -678,7 +678,7 @@ impl Typechecker { let r2 = self.check_path_pattern(p2); TypecheckResult::new( PathSummary::union(r1.path, r2.path), - TypeEnvironment::union(&r1.env, &r2.env), + TypeEnvironment::union(&self.schema, &r1.env, &r2.env), ) } diff --git a/src/typing/type_environment.rs b/src/typing/type_environment.rs index a02a3c7..3779c5f 100644 --- a/src/typing/type_environment.rs +++ b/src/typing/type_environment.rs @@ -5,6 +5,55 @@ use crate::syntax::descriptor::Descriptor; use super::variable_type::{Schema, VariableType}; +/// One environment binding: the shared type plus a lazily-computed +/// interned id (`Schema::intern_vt_rc`). The id exists so the lattice +/// memos (`meet_refined`/`join_interned`) can key by integer pair; it is +/// a cache, not part of the value — equality compares the type only. +#[derive(Debug, Clone)] +struct Binding { + ty: Rc, + id: std::cell::Cell>, +} + +impl Binding { + fn new(ty: Rc) -> Self { + Binding { + ty, + id: std::cell::Cell::new(None), + } + } + + fn with_id(ty: Rc, id: u32) -> Self { + Binding { + ty, + id: std::cell::Cell::new(Some(id)), + } + } + + fn id(&self, schema: &Schema) -> u32 { + match self.id.get() { + Some(i) => i, + None => { + let i = schema.intern_vt_rc(&self.ty); + self.id.set(Some(i)); + i + } + } + } +} + +impl PartialEq for Binding { + fn eq(&self, other: &Self) -> bool { + self.ty == other.ty + } +} +impl Eq for Binding {} + +thread_local! { + /// Shared `Null` value for the one-sided join arms. + static NULL_VT: Rc = Rc::new(VariableType::Null); +} + /// A type environment mapping variable names to their inferred `VariableType`. /// /// Bindings are stored as `Rc` so cloning the environment — @@ -12,7 +61,7 @@ use super::variable_type::{Schema, VariableType}; /// instead of deep-cloning each binding's descriptor tree. #[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct TypeEnvironment { - bindings: HashMap>, + bindings: HashMap, } impl TypeEnvironment { @@ -39,17 +88,18 @@ impl TypeEnvironment { } pub fn set(&mut self, key: &str, value: VariableType) { - self.bindings.insert(key.to_string(), Rc::new(value)); + self.bindings + .insert(key.to_string(), Binding::new(Rc::new(value))); } pub fn get(&self, key: &str) -> Option<&VariableType> { - self.bindings.get(key).map(Rc::as_ref) + self.bindings.get(key).map(|b| b.ty.as_ref()) } /// Like `get`, but exposes the shared binding for callers that need /// to retain it without deep-cloning. pub fn get_shared(&self, key: &str) -> Option<&Rc> { - self.bindings.get(key) + self.bindings.get(key).map(|b| &b.ty) } pub fn keys(&self) -> impl Iterator { @@ -60,12 +110,12 @@ impl TypeEnvironment { /// merging environments can share bindings instead of deep-cloning /// the descriptor trees. pub fn iter(&self) -> impl Iterator)> { - self.bindings.iter() + self.bindings.iter().map(|(k, b)| (k, &b.ty)) } /// Insert an already-shared binding without cloning the inner type. pub fn set_shared(&mut self, key: &str, value: Rc) { - self.bindings.insert(key.to_string(), value); + self.bindings.insert(key.to_string(), Binding::new(value)); } /// Pointwise join (least upper bound) of two environments. @@ -74,8 +124,10 @@ impl TypeEnvironment { /// types. For keys present in only one side, the result binds the type /// joined with `Null` — the variable may be absent in the other branch. /// This matches the rule `Γ₁ ⊔ Γ₂` in the paper. - pub fn union(a: &TypeEnvironment, b: &TypeEnvironment) -> TypeEnvironment { + pub fn union(schema: &Schema, a: &TypeEnvironment, b: &TypeEnvironment) -> TypeEnvironment { super::stats::record_env_union(); + let null_rc = NULL_VT.with(Rc::clone); + let null_id = schema.intern_vt_rc(&null_rc); let keys: HashSet<&String> = a.bindings.keys().chain(b.bindings.keys()).collect(); let mut result = HashMap::with_capacity(keys.len()); for key in keys { @@ -83,10 +135,20 @@ impl TypeEnvironment { // Same shared binding on both sides (common: both arms // hold the same refine-cache Rc): `join(v, v)` collapses // to `v`, so share it without cloning or walking. - (Some(ta), Some(tb)) if Rc::ptr_eq(ta, tb) => Rc::clone(ta), - (Some(ta), Some(tb)) => Rc::new(VariableType::join((**ta).clone(), (**tb).clone())), - (Some(ta), None) => Rc::new(VariableType::join((**ta).clone(), VariableType::Null)), - (None, Some(tb)) => Rc::new(VariableType::join(VariableType::Null, (**tb).clone())), + (Some(ta), Some(tb)) if Rc::ptr_eq(&ta.ty, &tb.ty) => ta.clone(), + (Some(ta), Some(tb)) => { + let (id, ty) = + schema.join_interned(ta.id(schema), &ta.ty, tb.id(schema), &tb.ty); + Binding::with_id(ty, id) + } + (Some(ta), None) => { + let (id, ty) = schema.join_interned(ta.id(schema), &ta.ty, null_id, &null_rc); + Binding::with_id(ty, id) + } + (None, Some(tb)) => { + let (id, ty) = schema.join_interned(null_id, &null_rc, tb.id(schema), &tb.ty); + Binding::with_id(ty, id) + } (None, None) => unreachable!(), }; result.insert(key.clone(), merged); @@ -122,22 +184,30 @@ impl TypeEnvironment { b: &TypeEnvironment, ) -> Result { super::stats::record_env_meet(); - let mut merged: Vec<(&String, Rc)> = Vec::new(); + let mut merged: Vec<(&String, Binding)> = Vec::new(); for (key, other) in &b.bindings { match a.bindings.get(key) { - Some(self_t) => { - let met = VariableType::meet(self_t, other); - if met == VariableType::Zero && !self_t.is_empty() && !other.is_empty() { - let msg = format!( - "Cannot reconcile types for variable {}: {} and {}", - key, self_t, other - ); - return Err((a, msg)); + Some(self_b) => { + match schema.meet_refined( + self_b.id(schema), + &self_b.ty, + other.id(schema), + &other.ty, + ) { + Some((rid, refined)) => merged.push((key, Binding::with_id(refined, rid))), + // Collapse marker: met to Zero with both sides + // non-empty — same message the uncached path built. + None => { + let msg = format!( + "Cannot reconcile types for variable {}: {} and {}", + key, self_b.ty, other.ty + ); + return Err((a, msg)); + } } - merged.push((key, VariableType::refine_rc(schema, &met))); } // Right-only key: keep the binding as-is. - None => merged.push((key, Rc::clone(other))), + None => merged.push((key, other.clone())), } } for (k, v) in merged { @@ -169,20 +239,30 @@ impl TypeEnvironment { b: &TypeEnvironment, ) -> TypeEnvironment { super::stats::record_env_outer_join(); - let mut result: HashMap> = HashMap::new(); + let null_rc = NULL_VT.with(Rc::clone); + let null_id = schema.intern_vt_rc(&null_rc); + let mut result: HashMap = HashMap::new(); // Shared keys (i) and left-only keys (j) start from `a`. for (key, t1) in &a.bindings { - let merged: Rc = match b.bindings.get(key) { + let merged: Binding = match b.bindings.get(key) { Some(t2) => { // T'_i := refine(schema, meet(T_{i1}, T_{i2})) - let met = VariableType::meet(t1, t2); - let refined = VariableType::refine(schema, &met); - // x_i ↦ T_{i1} ⊔ T'_i - Rc::new(VariableType::join((**t1).clone(), refined)) + match schema.meet_refined(t1.id(schema), &t1.ty, t2.id(schema), &t2.ty) { + // x_i ↦ T_{i1} ⊔ T'_i + Some((rid, refined)) => { + let (jid, joined) = + schema.join_interned(t1.id(schema), &t1.ty, rid, &refined); + Binding::with_id(joined, jid) + } + // Collapse marker ⇒ T'_i = Zero and + // join(T_{i1}, Zero) = T_{i1}: keep the left + // binding, as the uncached path did. + None => t1.clone(), + } } // x_j ↦ T_j (left-only, kept as-is). - None => Rc::clone(t1), + None => t1.clone(), }; result.insert(key.clone(), merged); } @@ -192,17 +272,15 @@ impl TypeEnvironment { if a.bindings.contains_key(key) { continue; } - result.insert( - key.clone(), - Rc::new(VariableType::join((**t2).clone(), VariableType::Null)), - ); + let (jid, joined) = schema.join_interned(t2.id(schema), &t2.ty, null_id, &null_rc); + result.insert(key.clone(), Binding::with_id(joined, jid)); } TypeEnvironment { bindings: result } } pub fn is_empty(&self) -> bool { - self.bindings.values().any(|v| v.is_empty()) + self.bindings.values().any(|b| b.ty.is_empty()) } /// Wrap each binding in `VariableType::Group`. Used for repeated/quantified @@ -214,10 +292,12 @@ impl TypeEnvironment { bindings: self .bindings .iter() - .map(|(k, v)| { + .map(|(k, b)| { ( k.clone(), - Rc::new(VariableType::Group(Box::new(v.as_ref().clone()))), + Binding::new(Rc::new(VariableType::Group(Box::new( + b.ty.as_ref().clone(), + )))), ) }) .collect(), @@ -239,7 +319,7 @@ mod tests { let mut b = TypeEnvironment::new(); a.set("x", nstar()); b.set("x", nstar()); - let u = TypeEnvironment::union(&a, &b); + let u = TypeEnvironment::union(&Schema::star(), &a, &b); // Equal types collapse under join. assert_eq!(u.get("x"), Some(&nstar())); } @@ -249,7 +329,7 @@ mod tests { let mut a = TypeEnvironment::new(); let b = TypeEnvironment::new(); a.set("x", nstar()); - let u = TypeEnvironment::union(&a, &b); + let u = TypeEnvironment::union(&Schema::star(), &a, &b); assert_eq!( u.get("x"), Some(&VariableType::Union( @@ -264,7 +344,7 @@ mod tests { let a = TypeEnvironment::new(); let mut b = TypeEnvironment::new(); b.set("y", nstar()); - let u = TypeEnvironment::union(&a, &b); + let u = TypeEnvironment::union(&Schema::star(), &a, &b); assert_eq!( u.get("y"), Some(&VariableType::Union( diff --git a/src/typing/variable_type.rs b/src/typing/variable_type.rs index 4356e54..8527ae1 100644 --- a/src/typing/variable_type.rs +++ b/src/typing/variable_type.rs @@ -462,11 +462,47 @@ pub struct Schema { /// bypasses it. #[serde(skip, default)] junction_cache: Rc>, + /// Interner for whole `VariableType`s (refined types, met results, + /// join results — the values environment bindings hold). Bindings + /// carry a lazily-computed id so the lattice memos below can key by + /// integer pair; each distinct type pays one hash at intern time. + /// Ids are never recycled (bounded by distinct types seen against + /// this schema; DDL/inference replace the whole Schema). + #[serde(skip, default)] + vt_interner: Rc>, + /// Memo for the environment-meet step `refine(meet(a, b))` keyed by + /// interned type ids. `None` marks the collapse-error outcome (met + /// to Zero with both sides non-empty) so the error path is memoized + /// too — the message is regenerated from the operand types, which is + /// what the uncached path formats as well. + /// `GQLITE_DISABLE_TC_MEET_CACHE=1` bypasses both this and + /// `join_cache`. + #[serde(skip, default)] + meet_refine_cache: Rc>, + /// Memo for environment joins (`Γ₁ ⊔ Γ₂` arms and TLEFTJOIN's + /// `T ⊔ T'`), keyed by interned id pair (order-sensitive, matching + /// `join`'s structural asymmetry). + #[serde(skip, default)] + join_cache: Rc>, } /// Junction memo: boundary id pair → refined junction descriptor ids. type JunctionCache = std::collections::HashMap<(u32, u32), Rc>>; +/// Backing store for [`Schema`]'s variable-type interner. +#[derive(Debug, Default)] +struct VtInterner { + ids: std::collections::HashMap, + vts: Vec>, +} + +/// Env-meet memo: `(a, b)` ids → `refine(meet(a,b))` or the collapse +/// marker. +type MeetRefineCache = std::collections::HashMap<(u32, u32), Option<(u32, Rc)>>; + +/// Env-join memo: `(a, b)` ids → `join(a, b)`. +type JoinCache = std::collections::HashMap<(u32, u32), (u32, Rc)>; + /// Backing store for [`Schema`]'s descriptor interner. #[derive(Debug, Default)] struct DescInterner { @@ -491,6 +527,9 @@ impl Schema { refine_cache: Rc::default(), interner: Rc::default(), junction_cache: Rc::default(), + vt_interner: Rc::default(), + meet_refine_cache: Rc::default(), + join_cache: Rc::default(), } } @@ -502,7 +541,92 @@ impl Schema { refine_cache: Rc::default(), interner: Rc::default(), junction_cache: Rc::default(), + vt_interner: Rc::default(), + meet_refine_cache: Rc::default(), + join_cache: Rc::default(), + } + } + + /// Intern a shared variable type, returning its dense id. The first + /// `Rc` seen for a value becomes the canonical allocation handed back + /// by `vt_of`, which keeps downstream `Rc::ptr_eq` fast paths hitting. + pub(crate) fn intern_vt_rc(&self, t: &Rc) -> u32 { + let mut i = self.vt_interner.borrow_mut(); + if let Some(&id) = i.ids.get(&**t) { + return id; + } + let id = i.vts.len() as u32; + i.vts.push(Rc::clone(t)); + i.ids.insert((**t).clone(), id); + id + } + + /// Resolve an interned variable-type id to its canonical `Rc`. + pub(crate) fn vt_of(&self, id: u32) -> Rc { + Rc::clone(&self.vt_interner.borrow().vts[id as usize]) + } + + /// The environment-meet step `refine(meet(a, b))`, memoized by id + /// pair. `None` is the collapse-error outcome (`met == Zero` with + /// both operands non-empty); callers format the same message the + /// uncached path did, from the operand types. + pub(crate) fn meet_refined( + &self, + ia: u32, + a: &Rc, + ib: u32, + b: &Rc, + ) -> Option<(u32, Rc)> { + let cache_on = !meet_cache_disabled(); + if cache_on { + if let Some(hit) = self.meet_refine_cache.borrow().get(&(ia, ib)) { + return hit.clone(); + } } + let met = VariableType::meet(a, b); + let out = if met == VariableType::Zero && !a.is_empty() && !b.is_empty() { + None + } else { + let refined = VariableType::refine_rc(self, &met); + let rid = self.intern_vt_rc(&refined); + Some((rid, self.vt_of(rid))) + }; + if cache_on { + let mut m = self.meet_refine_cache.borrow_mut(); + if m.len() >= REFINE_CACHE_CAP { + m.clear(); + } + m.insert((ia, ib), out.clone()); + } + out + } + + /// `join(a, b)` memoized by id pair (order-sensitive: `join` is + /// structurally asymmetric). Returns the canonical interned `Rc`. + pub(crate) fn join_interned( + &self, + ia: u32, + a: &Rc, + ib: u32, + b: &Rc, + ) -> (u32, Rc) { + let cache_on = !meet_cache_disabled(); + if cache_on { + if let Some(hit) = self.join_cache.borrow().get(&(ia, ib)) { + return hit.clone(); + } + } + let joined = Rc::new(VariableType::join((**a).clone(), (**b).clone())); + let jid = self.intern_vt_rc(&joined); + let out = (jid, self.vt_of(jid)); + if cache_on { + let mut m = self.join_cache.borrow_mut(); + if m.len() >= REFINE_CACHE_CAP { + m.clear(); + } + m.insert((ia, ib), out.clone()); + } + out } /// Intern a descriptor, returning its dense id for this Schema. @@ -575,6 +699,10 @@ fn junction_cache_disabled() -> bool { std::env::var("GQLITE_DISABLE_TC_JUNCTION_CACHE").is_ok() } +fn meet_cache_disabled() -> bool { + std::env::var("GQLITE_DISABLE_TC_MEET_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 89259d1..1b2c623 100644 --- a/tests/tc_refine_cache_test.rs +++ b/tests/tc_refine_cache_test.rs @@ -62,9 +62,13 @@ fn assert_cache_transparent(schema: &Schema, label: &str) { 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"); + std::env::set_var("GQLITE_DISABLE_TC_MEET_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"); + let meet_off = verdict(schema, q); + std::env::remove_var("GQLITE_DISABLE_TC_MEET_CACHE"); + assert_eq!(meet_off, both_off, "[{label}] meet-off != all-off for: {q}"); assert_eq!( cold, both_off, "[{label}] caches-on (cold) != caches-off for: {q}"