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
79 changes: 71 additions & 8 deletions src/bin/pattern_typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ fn median_min_ns(mut v: Vec<u128>) -> (u128, u128) {
(v[v.len() / 2], v[0])
}

/// Cold-compile timing: every sample runs against a schema whose refine
/// cache has never been used, so each check pays full misses (hash +
/// key/result clones + insert on top of the scan). This is the regime a
/// per-query cache would live in permanently — the honest upper bound on
/// the cache's overhead. Code/branch/data caches stay warm (that part is
/// shared with the steady-state protocol). Heavy cases are capped by the
/// same per-case budget.
fn time_case_cold(schema: &Schema, q: &Query) -> (u128, u128) {
let fresh = || Schema::from_parts((*schema.nodes).clone(), (*schema.edges).clone());
// Pilot on one fresh schema.
let t = Instant::now();
black_box(Typechecker::new(fresh()).check_query(black_box(q)));
let est = t.elapsed().as_nanos().max(1);
let iters = ((CASE_BUDGET_NS / est) as usize).clamp(3, 500);
// Pre-build outside the timed region.
let schemas: Vec<Schema> = (0..iters).map(|_| fresh()).collect();
let mut samples = Vec::with_capacity(iters);
for s in schemas {
let mut tc = Typechecker::new(s);
let t = Instant::now();
black_box(tc.check_query(black_box(q)));
samples.push(t.elapsed().as_nanos());
}
median_min_ns(samples)
}

/// Hot-loop timing with the established warmup + median protocol; falls
/// back to batch timing when a case is too fast for per-call resolution.
fn time_case(tc: &mut Typechecker, q: &Query) -> (u128, u128) {
Expand Down Expand Up @@ -86,6 +112,23 @@ fn time_case(tc: &mut Typechecker, q: &Query) -> (u128, u128) {
median_min_ns(samples)
}

/// Median parse time of the case's query string — the north-star
/// yardstick: a fully optimized check should cost no more than parsing
/// the same input (both walk the same AST; parsing also allocates it).
fn time_parse(q: &str) -> u128 {
const P_ITERS: usize = 512;
for _ in 0..32 {
black_box(frogql::parser::parse_query(black_box(q)).expect("parse"));
}
let mut samples = Vec::with_capacity(P_ITERS);
for _ in 0..P_ITERS {
let t = Instant::now();
black_box(frogql::parser::parse_query(black_box(q)).expect("parse"));
samples.push(t.elapsed().as_nanos());
}
median_min_ns(samples).0
}

// --- Generated scaling families (all expected `valid` on the LDBC schema) ---

/// `(p0: Person)~[:knows]~(p1: Person)~...` — N undirected labeled hops.
Expand Down Expand Up @@ -150,9 +193,12 @@ fn main() {
let mut gdb: Option<String> = None;
let mut out: Option<String> = None;
let mut star_schema = false;
let mut cold = false;
for arg in std::env::args().skip(1) {
if arg == "--star-schema" {
star_schema = true;
} else if arg == "--cold" {
cold = true;
} else if gdb.is_none() {
gdb = Some(arg);
} else if out.is_none() {
Expand Down Expand Up @@ -339,16 +385,25 @@ fn main() {
got: String,
med: u128,
min: u128,
parse: u128,
st: stats::TcStats,
phases: [u128; 5],
ok: bool,
}

println!(
"\n{:<26}{:<11}{:>7}{:>9}{:>13}{:>9}{:>7}{:>11}",
"case", "category", "exp", "got", "check_med_us", "refines", "hits", "edge_scan"
"\n{:<26}{:<11}{:>7}{:>9}{:>13}{:>10}{:>9}{:>7}{:>11}",
"case",
"category",
"exp",
"got",
"check_med_us",
"chk/parse",
"refines",
"hits",
"edge_scan"
);
println!("{}", "-".repeat(93));
println!("{}", "-".repeat(103));
let mut rows: Vec<Row> = Vec::new();
let mut mismatches = 0usize;
for (id, category, expected, q) in &cases {
Expand All @@ -364,7 +419,12 @@ fn main() {
"valid"
};

let (med, min) = time_case(&mut tc, &elaborated);
let (med, min) = if cold {
time_case_cold(&schema, &elaborated)
} else {
time_case(&mut tc, &elaborated)
};
let parse = time_parse(q);

// One un-timed run for the counter shape, one profiled run for the
// phase split. Both after the timed loop so they cannot pollute it.
Expand All @@ -387,12 +447,13 @@ fn main() {
mismatches += 1;
}
println!(
"{:<26}{:<11}{:>7}{:>9}{:>13.3}{:>9}{:>7}{:>11}{}",
"{:<26}{:<11}{:>7}{:>9}{:>13.3}{:>10.2}{:>9}{:>7}{:>11}{}",
id,
category,
expected,
got,
med as f64 / 1000.0,
med as f64 / parse.max(1) as f64,
st.refine_calls,
st.refine_cache_hits,
st.edge_entries_scanned,
Expand All @@ -405,17 +466,18 @@ fn main() {
got: got.to_string(),
med,
min,
parse,
st,
phases,
ok,
});
}
println!("{}", "-".repeat(93));
println!("{}", "-".repeat(103));

let mut f = File::create(&out).unwrap_or_else(|e| panic!("create {out}: {e}"));
writeln!(
f,
"case,category,expected,got,check_med_ns,check_min_ns,\
"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,\
phase_pattern_ns,phase_rep_ns,phase_group_by_ns,phase_returns_ns,phase_order_by_ns,\
Expand All @@ -425,13 +487,14 @@ fn main() {
for r in &rows {
writeln!(
f,
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
r.id,
r.category,
r.expected,
r.got,
r.med,
r.min,
r.parse,
r.st.refine_calls,
r.st.refine_cache_hits,
r.st.node_entries_scanned,
Expand Down
35 changes: 18 additions & 17 deletions src/typing/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use crate::syntax::query::{Aggregator, MatchStatement, Query, ReturnItem, SortKe

use super::descriptor_type::DescriptorType;
use super::label_type::LabelType;
use super::path_type::{EdgeDir, PathType};
use super::path_summary::PathSummary;
use super::path_type::EdgeDir;
use super::property_type::PropertyType;
use super::simple_type::SimpleType;
use super::type_environment::TypeEnvironment;
Expand All @@ -40,16 +41,20 @@ pub struct PhaseProfile {
}

/// Result of type-checking a path pattern or query.
///
/// `path` is the boundary-pair summary (`PathSummary`), the checker's
/// evaluation form of the paper's inductive path type — see
/// `path_summary.rs` for the correspondence.
#[derive(Clone, Debug)]
pub struct TypecheckResult {
pub path: PathType,
pub path: PathSummary,
pub env: TypeEnvironment,
pub ok: bool,
pub empty: bool,
}

impl TypecheckResult {
fn new(path: PathType, env: TypeEnvironment) -> Self {
fn new(path: PathSummary, env: TypeEnvironment) -> Self {
TypecheckResult {
path,
env,
Expand Down Expand Up @@ -573,7 +578,7 @@ impl Typechecker {
match node {
PathPattern::Node(desc) => {
let t = self.refine_pattern_node(desc);
let p = PathType::from_variable(&t, EdgeDir::Any);
let p = PathSummary::from_variable(&t, EdgeDir::Any);
let env = create_context(desc, t);
TypecheckResult::new(p, env)
}
Expand All @@ -600,7 +605,7 @@ impl Typechecker {

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

let p = PathType::meet(&self.schema, &r1.path, &r2.path);
let p = PathSummary::meet(&self.schema, &r1.path, &r2.path);
TypecheckResult::new(p, cm)
}

Expand All @@ -625,7 +630,7 @@ impl Typechecker {
self.warn_for_collapsed_bindings(&cm, &r1.env, &r2.env);

let p = if r1.path.is_unsatisfiable() || r2.path.is_unsatisfiable() {
PathType::Zero
PathSummary::zero()
} else {
r1.path
};
Expand Down Expand Up @@ -654,7 +659,7 @@ impl Typechecker {
"Filter expression has type {}, which is not a boolean",
t
));
TypecheckResult::new(PathType::Zero, r.env)
TypecheckResult::new(PathSummary::zero(), r.env)
} else {
r
}
Expand All @@ -664,7 +669,7 @@ impl Typechecker {
let r1 = self.check_path_pattern(p1);
let r2 = self.check_path_pattern(p2);
TypecheckResult::new(
PathType::union(r1.path, r2.path),
PathSummary::union(r1.path, r2.path),
TypeEnvironment::union(&r1.env, &r2.env),
)
}
Expand Down Expand Up @@ -727,7 +732,7 @@ impl Typechecker {

fn check_edge(&mut self, dir: EdgeDir, desc: &Option<Descriptor>) -> TypecheckResult {
let t = self.refine_pattern_edge(dir, desc);
let p = PathType::from_variable(&t, dir);
let p = PathSummary::from_variable(&t, dir);
let env = create_context(desc, t);
TypecheckResult::new(p, env)
}
Expand Down Expand Up @@ -1075,7 +1080,7 @@ impl Typechecker {
// from `outer` directly, so the only up-front clone is the one
// pushed onto the ambient stack.
let mut env: Option<TypeEnvironment> = None;
let mut path: Option<PathType> = None;
let mut path: Option<PathSummary> = None;
// Make the outer environment available to the body's `Filter`
// predicates so a correlated WHERE (e.g. `WHERE x IN NODES(path)`)
// can reference outer-bound variables. The runtime evaluates such a
Expand Down Expand Up @@ -1109,7 +1114,7 @@ impl Typechecker {
}
self.ambient_env.pop();
let env = env.unwrap_or_else(|| outer.clone());
let path = path.unwrap_or(PathType::Zero);
let path = path.unwrap_or_else(PathSummary::zero);
let mut r = TypecheckResult::new(path, env);
r.empty = r.path.is_unsatisfiable() || r.env.is_empty();
r
Expand Down Expand Up @@ -1228,12 +1233,8 @@ impl Typechecker {
}

/// p^0 = identity (default node path), p^1 = p, p^n = meet(p, p^(n-1)).
fn pow_path_type(&self, p: &PathType, n: u64) -> PathType {
match n {
0 => PathType::default(),
1 => p.clone(),
_ => PathType::meet(&self.schema, p, &self.pow_path_type(p, n - 1)),
}
fn pow_path_type(&self, p: &PathSummary, n: u64) -> PathSummary {
PathSummary::pow(&self.schema, p, n)
}
}

Expand Down
1 change: 1 addition & 0 deletions src/typing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod descriptor_type;
pub mod format;
pub mod inference;
pub mod label_type;
pub mod path_summary;
pub mod path_type;
pub mod property_type;
pub mod simple_type;
Expand Down
Loading