Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions src/typing/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,21 +1124,25 @@ impl Typechecker {
// Refinement helpers
// -----------------------------------------------

fn refine_pattern_node(&mut self, desc: &Option<Descriptor>) -> VariableType {
fn refine_pattern_node(&mut self, desc: &Option<Descriptor>) -> std::rc::Rc<VariableType> {
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<Descriptor>) -> VariableType {
fn refine_pattern_edge(
&mut self,
dir: EdgeDir,
desc: &Option<Descriptor>,
) -> std::rc::Rc<VariableType> {
let dtype = descriptor_type_of(desc);
if let Some(d) = desc {
self.assert_filters_drained(d);
Expand All @@ -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()),
),
Expand All @@ -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));
}
Expand Down Expand Up @@ -1249,9 +1254,9 @@ fn descriptor_type_of(desc: &Option<Descriptor>) -> DescriptorType {

/// Build the binding environment for a pattern position. Anonymous patterns
/// contribute no bindings.
fn create_context(desc: &Option<Descriptor>, t: VariableType) -> TypeEnvironment {
fn create_context(desc: &Option<Descriptor>, t: std::rc::Rc<VariableType>) -> TypeEnvironment {
match desc {
Some(d) => TypeEnvironment::create_context(d, t),
Some(d) => TypeEnvironment::create_context_shared(d, t),
None => TypeEnvironment::new(),
}
}
Expand Down
41 changes: 11 additions & 30 deletions src/typing/path_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DescriptorType>,
)> = Vec::new();
let mut junction = |l1: &DescriptorType, f2: &DescriptorType| -> Vec<DescriptorType> {
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<DescriptorType> = 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 {
Expand All @@ -234,25 +215,25 @@ 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);
}
}
}
// nodes × nodes: both are the junction; result is the refined
// 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());
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/typing/type_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariableType>) -> Self {
let mut env = TypeEnvironment::new();
if let Some(var) = &descriptor.var {
env.set(var, t);
env.set_shared(var, t);
}
env
}
Expand Down Expand Up @@ -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),
Expand Down
136 changes: 112 additions & 24 deletions src/typing/variable_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariableType> {
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);
}
_ => {}
}
Expand All @@ -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<VariableType>` anyway.
pub fn refine_rc(schema: &Schema, node: &VariableType) -> Rc<VariableType> {
match node {
VariableType::Node(_) => {
if !refine_cache_disabled() {
Expand All @@ -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
}
Expand All @@ -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)),
}
}

Expand Down Expand Up @@ -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<std::cell::RefCell<std::collections::HashMap<VariableType, Rc<VariableType>>>>,
/// 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<std::cell::RefCell<std::collections::HashMap<VariableType, VariableType>>>,
#[allow(clippy::type_complexity)]
junction_cache: Rc<
std::cell::RefCell<
std::collections::HashMap<
DescriptorType,
std::collections::HashMap<DescriptorType, Rc<Vec<DescriptorType>>>,
>,
>,
>,
}

/// Safety valve for adversarial/degenerate sessions: the cache resets when
Expand All @@ -436,6 +477,7 @@ impl Schema {
VariableType::edge_non_directional(DescriptorType::star()),
]),
refine_cache: Rc::default(),
junction_cache: Rc::default(),
}
}

Expand All @@ -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<VariableType> {
self.refine_cache.borrow().get(key).cloned()
fn refine_cache_get(&self, key: &VariableType) -> Option<Rc<VariableType>> {
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<VariableType>) {
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<Vec<DescriptorType>> {
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<Vec<DescriptorType>> = 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::*;
Expand Down
Loading