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
6 changes: 5 additions & 1 deletion src/bin/pattern_typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,15 @@ fn main() {
"case,category,expected,got,check_med_ns,check_min_ns,parse_med_ns,\
refine_calls,refine_cache_hits,node_scanned,edge_scanned,refine_to_nodes,\
env_meets,env_unions,env_outer_joins,env_to_groups,pt_meets,\
vt_meets,vt_joins,env_copied,\
phase_pattern_ns,phase_rep_ns,phase_group_by_ns,phase_returns_ns,phase_order_by_ns,\
status"
)
.unwrap();
for r in &rows {
writeln!(
f,
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
r.id,
r.category,
r.expected,
Expand All @@ -505,6 +506,9 @@ fn main() {
r.st.env_outer_join_calls,
r.st.env_to_group_calls,
r.st.pathtype_meet_calls,
r.st.vt_meet_calls,
r.st.vt_join_calls,
r.st.env_bindings_copied,
r.phases[0],
r.phases[1],
r.phases[2],
Expand Down
134 changes: 81 additions & 53 deletions src/typing/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,15 +444,16 @@ impl Typechecker {
let r = self.check_path_pattern(m.pattern());
env = match m {
MatchStatement::Simple { .. } => {
match TypeEnvironment::meet(&self.schema, &env, &r.env) {
let left_shared = shared_bindings(&env, &r.env);
match TypeEnvironment::meet_owned(&self.schema, env, &r.env) {
Ok(e) => {
self.warn_for_collapsed_bindings(&e, &env, &r.env);
self.warn_for_collapsed_bindings_shared(&e, &left_shared, &r.env);
e
}
Err(e) => {
Err((prev, e)) => {
self.errors
.push(format!("Concatenation of contexts failed: {}", e));
env
prev
}
}
}
Expand Down Expand Up @@ -594,16 +595,22 @@ impl Typechecker {
let r1 = self.check_path_pattern(p1);
let r2 = self.check_path_pattern(p2);

let cm = match TypeEnvironment::meet(&self.schema, &r1.env, &r2.env) {
// The warning pass only needs the SHARED keys' originals;
// meet_owned consumes r1's env without cloning it, so
// stash the left-side bindings for vars the right also
// binds (few, usually zero or one).
let left_shared = shared_bindings(&r1.env, &r2.env);

let cm = match TypeEnvironment::meet_owned(&self.schema, r1.env, &r2.env) {
Ok(env) => env,
Err(e) => {
Err((env, e)) => {
self.errors
.push(format!("Concatenation of contexts failed: {}", e));
r1.env.clone()
env
}
};

self.warn_for_collapsed_bindings(&cm, &r1.env, &r2.env);
self.warn_for_collapsed_bindings_shared(&cm, &left_shared, &r2.env);

let p = PathSummary::meet(&self.schema, &r1.path, &r2.path);
TypecheckResult::new(p, cm)
Expand All @@ -618,16 +625,17 @@ impl Typechecker {
let r1 = self.check_path_pattern(p1);
let r2 = self.check_path_pattern(p2);

let cm = match TypeEnvironment::meet(&self.schema, &r1.env, &r2.env) {
let left_shared = shared_bindings(&r1.env, &r2.env);
let cm = match TypeEnvironment::meet_owned(&self.schema, r1.env, &r2.env) {
Ok(env) => env,
Err(e) => {
Err((env, e)) => {
self.errors
.push(format!("Concatenation of contexts failed: {}", e));
r1.env.clone()
env
}
};

self.warn_for_collapsed_bindings(&cm, &r1.env, &r2.env);
self.warn_for_collapsed_bindings_shared(&cm, &left_shared, &r2.env);

let p = if r1.path.is_unsatisfiable() || r2.path.is_unsatisfiable() {
PathSummary::zero()
Expand Down Expand Up @@ -1091,20 +1099,32 @@ impl Typechecker {
if path.is_none() {
path = Some(r.path.clone());
}
let base = env.as_ref().unwrap_or(outer);
let next = match m {
MatchStatement::Simple { .. } => {
match TypeEnvironment::meet(&self.schema, base, &r.env) {
MatchStatement::Simple { .. } => match env.take() {
// Owned accumulator: meet without cloning; the error
// path hands the previous environment back.
Some(base) => match TypeEnvironment::meet_owned(&self.schema, base, &r.env) {
Ok(e) => Some(e),
Err((prev, msg)) => {
self.errors.push(format!(
"EXISTS body: concatenation of contexts failed: {msg}"
));
Some(prev)
}
},
// First fold step: the base is the borrowed outer env.
None => match TypeEnvironment::meet(&self.schema, outer, &r.env) {
Ok(e) => Some(e),
Err(msg) => {
self.errors.push(format!(
"EXISTS body: concatenation of contexts failed: {msg}"
));
None // keep the previous environment
None // keep the previous (outer) environment
}
}
}
},
},
MatchStatement::Optional { .. } => {
let base = env.as_ref().unwrap_or(outer);
Some(TypeEnvironment::outer_join(&self.schema, base, &r.env))
}
};
Expand Down Expand Up @@ -1193,47 +1213,38 @@ impl Typechecker {
}
}

/// After meeting two pattern contexts, surface any variable whose
/// type collapsed to bottom. Each side's contribution is shown so
/// the user can see the conflict directly. Pre-existing empties
/// (already empty in `left` or `right`) are skipped to avoid
/// double-warning.
fn warn_for_collapsed_bindings(
/// After meeting two pattern contexts, surface any SHARED variable
/// whose type collapsed to bottom. Only shared keys can collapse: a
/// one-sided key passes through `meet` untouched, so if its merged
/// binding is empty it was already empty on its own side — which the
/// previous all-keys walk skipped as a "pre-existing empty" (its
/// one-sided message arms were unreachable for exactly this reason).
/// Each side's contribution is shown so the user can see the
/// conflict directly.
fn warn_for_collapsed_bindings_shared(
&mut self,
merged: &TypeEnvironment,
left: &TypeEnvironment,
left_shared: &[(String, std::rc::Rc<VariableType>)],
right: &TypeEnvironment,
) {
for (var, merged_t) in merged.iter() {
if !merged_t.is_empty() {
continue;
}
let l_t = left.get(var);
let r_t = right.get(var);
let l_was_empty = l_t.is_some_and(VariableType::is_empty);
let r_was_empty = r_t.is_some_and(VariableType::is_empty);
if l_was_empty || r_was_empty {
for (var, l) in left_shared {
let merged_t = match merged.get(var) {
Some(t) => t,
None => continue,
};
if !merged_t.is_empty() || l.is_empty() {
continue;
}
match (l_t, r_t) {
(Some(l), Some(r)) => self.warnings.push(format!(
"variable {} cannot be both {} and {} under the active schema",
var,
short_var_type(l),
short_var_type(r)
)),
(Some(l), None) => self.warnings.push(format!(
"variable {} bound to {} collapses to empty under the active schema",
var,
short_var_type(l)
)),
(None, Some(r)) => self.warnings.push(format!(
"variable {} bound to {} collapses to empty under the active schema",
var,
short_var_type(r)
)),
(None, None) => {}
}
let r = match right.get(var) {
Some(r) if !r.is_empty() => r,
_ => continue,
};
self.warnings.push(format!(
"variable {} cannot be both {} and {} under the active schema",
var,
short_var_type(l),
short_var_type(r)
));
}
}

Expand All @@ -1254,6 +1265,23 @@ fn descriptor_type_of(desc: &Option<Descriptor>) -> DescriptorType {

/// Build the binding environment for a pattern position. Anonymous patterns
/// contribute no bindings.
/// The left side's bindings for every key the right side also binds —
/// the inputs `warn_for_collapsed_bindings_shared` needs after
/// `meet_owned` has consumed the left environment. Usually empty or a
/// single entry, so the `Vec` rarely allocates.
fn shared_bindings(
left: &TypeEnvironment,
right: &TypeEnvironment,
) -> Vec<(String, std::rc::Rc<VariableType>)> {
right
.iter()
.filter_map(|(k, _)| {
left.get_shared(k)
.map(|v| (k.clone(), std::rc::Rc::clone(v)))
})
.collect()
}

fn create_context(desc: &Option<Descriptor>, t: std::rc::Rc<VariableType>) -> TypeEnvironment {
match desc {
Some(d) => TypeEnvironment::create_context_shared(d, t),
Expand Down
24 changes: 24 additions & 0 deletions src/typing/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ pub struct TcStats {
pub env_to_group_calls: u64,
/// Calls to `PathType::meet` (every Concat, recursing through Unions).
pub pathtype_meet_calls: u64,
/// Calls to `VariableType::meet` (incl. recursion into Union arms).
pub vt_meet_calls: u64,
/// Calls to `VariableType::join` (incl. list folds).
pub vt_join_calls: u64,
/// Bindings copied by env clones/merges (`String` key + `Rc` bump each).
pub env_bindings_copied: u64,
}

const ZERO: TcStats = TcStats {
Expand All @@ -47,6 +53,9 @@ const ZERO: TcStats = TcStats {
env_outer_join_calls: 0,
env_to_group_calls: 0,
pathtype_meet_calls: 0,
vt_meet_calls: 0,
vt_join_calls: 0,
env_bindings_copied: 0,
};

thread_local! {
Expand Down Expand Up @@ -122,3 +131,18 @@ pub(crate) fn record_env_to_group() {
pub(crate) fn record_pathtype_meet() {
bump(|s| s.pathtype_meet_calls += 1);
}

#[inline]
pub(crate) fn record_vt_meet() {
bump(|s| s.vt_meet_calls += 1);
}

#[inline]
pub(crate) fn record_vt_join() {
bump(|s| s.vt_join_calls += 1);
}

#[inline]
pub(crate) fn record_env_bindings_copied(n: usize) {
bump(|s| s.env_bindings_copied += n as u64);
}
55 changes: 42 additions & 13 deletions src/typing/type_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ impl TypeEnvironment {
self.bindings.get(key).map(Rc::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<VariableType>> {
self.bindings.get(key)
}

pub fn keys(&self) -> impl Iterator<Item = &String> {
self.bindings.keys()
}
Expand Down Expand Up @@ -74,12 +80,16 @@ impl TypeEnvironment {
let mut result = HashMap::with_capacity(keys.len());
for key in keys {
let merged = match (a.bindings.get(key), b.bindings.get(key)) {
(Some(ta), Some(tb)) => VariableType::join((**ta).clone(), (**tb).clone()),
(Some(ta), None) => VariableType::join((**ta).clone(), VariableType::Null),
(None, Some(tb)) => VariableType::join(VariableType::Null, (**tb).clone()),
// 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())),
(None, None) => unreachable!(),
};
result.insert(key.clone(), Rc::new(merged));
result.insert(key.clone(), merged);
}
TypeEnvironment { bindings: result }
}
Expand All @@ -95,26 +105,45 @@ impl TypeEnvironment {
a: &TypeEnvironment,
b: &TypeEnvironment,
) -> Result<TypeEnvironment, String> {
super::stats::record_env_bindings_copied(a.bindings.len());
TypeEnvironment::meet_owned(schema, a.clone(), b).map_err(|(_, msg)| msg)
}

/// `meet` that consumes the left environment instead of cloning it —
/// the checker's Concat/Join/match-chain folds own their accumulator
/// and were paying O(vars) String-key clones per operator (quadratic
/// along a chain). Merged bindings are staged in a scratch `Vec` and
/// applied only on success, so the `Err` case hands `a` back
/// untouched (callers keep the pre-meet environment on error, exactly
/// as the cloning version behaved).
pub fn meet_owned(
schema: &Schema,
mut a: TypeEnvironment,
b: &TypeEnvironment,
) -> Result<TypeEnvironment, (TypeEnvironment, String)> {
super::stats::record_env_meet();
let mut result = a.bindings.clone();
let mut merged: Vec<(&String, Rc<VariableType>)> = Vec::new();
for (key, other) in &b.bindings {
let merged: Rc<VariableType> = match result.get(key) {
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() {
return Err(format!(
let msg = format!(
"Cannot reconcile types for variable {}: {} and {}",
key, self_t, other
));
);
return Err((a, msg));
}
VariableType::refine_rc(schema, &met)
merged.push((key, VariableType::refine_rc(schema, &met)));
}
// Right-only key: keep the binding as-is.
None => Rc::clone(other),
};
result.insert(key.clone(), merged);
None => merged.push((key, Rc::clone(other))),
}
}
for (k, v) in merged {
a.bindings.insert(k.clone(), v);
}
Ok(TypeEnvironment { bindings: result })
Ok(a)
}

/// Left outer join — the typing operator `Γ₁ ⟕ Γ₂` for OPTIONAL MATCH,
Expand Down
2 changes: 2 additions & 0 deletions src/typing/variable_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ impl VariableType {
}

pub fn meet(a: &VariableType, b: &VariableType) -> VariableType {
super::stats::record_vt_meet();
match (a, b) {
(VariableType::Group(ta), VariableType::Group(tb)) => {
VariableType::Group(Box::new(VariableType::meet(ta, tb)))
Expand Down Expand Up @@ -193,6 +194,7 @@ impl VariableType {
// --- Join ---

pub fn join(a: VariableType, b: VariableType) -> VariableType {
super::stats::record_vt_join();
if a == VariableType::Zero {
return b;
}
Expand Down