From b9fa773f289d35f361ccc959104a7299d6da724d Mon Sep 17 00:00:00 2001 From: ecoricemon Date: Tue, 7 Jul 2026 07:10:04 +0900 Subject: [PATCH] optimize: Database build * Do not make error messages it it's not needed * Split dataset into clauses first * Organize benches --- crates/logic-eval/Cargo.toml | 10 +- crates/logic-eval/benches/candidate_filter.rs | 189 --------------- crates/logic-eval/benches/common/mod.rs | 173 ++++++++++++++ crates/logic-eval/benches/database_build.rs | 223 ++++++++++++++++++ crates/logic-eval/benches/parse.rs | 130 ++++++++++ crates/logic-eval/benches/query.rs | 95 ++++++++ crates/logic-eval/src/parse/inner.rs | 88 +++++-- crates/logic-eval/src/parse/text.rs | 120 +++++++--- crates/logic-eval/src/prove/db.rs | 2 +- 9 files changed, 782 insertions(+), 248 deletions(-) delete mode 100644 crates/logic-eval/benches/candidate_filter.rs create mode 100644 crates/logic-eval/benches/common/mod.rs create mode 100644 crates/logic-eval/benches/database_build.rs create mode 100644 crates/logic-eval/benches/parse.rs create mode 100644 crates/logic-eval/benches/query.rs diff --git a/crates/logic-eval/Cargo.toml b/crates/logic-eval/Cargo.toml index 3e5cf50..9f12eb9 100644 --- a/crates/logic-eval/Cargo.toml +++ b/crates/logic-eval/Cargo.toml @@ -24,7 +24,15 @@ name = "query_threads" harness = false [[bench]] -name = "candidate_filter" +name = "query" +harness = false + +[[bench]] +name = "database_build" +harness = false + +[[bench]] +name = "parse" harness = false [[example]] diff --git a/crates/logic-eval/benches/candidate_filter.rs b/crates/logic-eval/benches/candidate_filter.rs deleted file mode 100644 index 01e3374..0000000 --- a/crates/logic-eval/benches/candidate_filter.rs +++ /dev/null @@ -1,189 +0,0 @@ -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use logic_eval::{parse_str, ClauseDataset, Database, Expr, InternedStr, Name, StrInterner}; - -type BenchName<'a> = Name>; - -const NOISE_CANDIDATES_PER_REQUEST: usize = 24; -const QUERY_BATCH_LEN: usize = 10; - -#[derive(Clone, Copy)] -struct CaseSize { - name: &'static str, - requests: usize, -} - -const CASES: &[CaseSize] = &[ - CaseSize { - name: "small", - requests: 32, - }, - CaseSize { - name: "medium", - requests: 128, - }, -]; - -fn type_name(index: usize) -> String { - format!("ty{index}") -} - -fn request_name(index: usize) -> String { - format!("req{index}") -} - -fn candidate_name(index: usize) -> String { - format!("candidate{index}") -} - -fn noise_candidate_name(request: usize, candidate: usize) -> String { - format!("candidate_noise_{request}_{candidate}") -} - -fn nested_type(name: &str) -> String { - format!("vec(vec({name}))") -} - -fn build_source(requests: usize) -> String { - let mut source = String::new(); - - for i in 0..requests { - let ty = type_name(i); - let expected = nested_type(&ty); - source.push_str(&format!( - "expected_value({}, {}).\n", - request_name(i), - expected - )); - source.push_str(&format!("enabled_candidate({}).\n", candidate_name(i))); - source.push_str(&format!( - "candidate_value({}, {}, {}).\n", - request_name(i), - candidate_name(i), - expected - )); - - for j in 0..NOISE_CANDIDATES_PER_REQUEST { - let noise_ty = type_name(requests + i * NOISE_CANDIDATES_PER_REQUEST + j); - source.push_str(&format!( - "candidate_value({}, {}, {}).\n", - request_name(i), - noise_candidate_name(i, j), - nested_type(&noise_ty) - )); - } - } - - source.push_str( - " - same_shape($Value, $Value). - same_shape(vec($A), vec($B)) :- same_shape($A, $B). - candidate_match($Request, $Candidate) :- - candidate_value($Request, $Candidate, $Value), - expected_value($Request, $Expected), - same_shape($Value, $Expected), - enabled_candidate($Candidate). - ", - ); - - source -} - -fn build_database<'a>(interner: &'a StrInterner, requests: usize) -> Database> { - let source = build_source(requests); - let dataset: ClauseDataset<_> = parse_str(&source, interner).unwrap(); - let mut db = Database::default(); - db.insert_dataset(dataset); - db -} - -fn build_query<'a>(interner: &'a StrInterner, request: usize) -> Expr> { - parse_str( - &format!("candidate_match({}, $Candidate)", request_name(request)), - interner, - ) - .unwrap() -} - -fn build_queries<'a>(interner: &'a StrInterner, start_request: usize) -> Vec>> { - (start_request..start_request + QUERY_BATCH_LEN) - .map(|request| build_query(interner, request)) - .collect() -} - -fn count_first_answer_for_query(db: &Database>, query: Expr>) -> usize { - db.query(query) - .prove_next() - .map_or(0, |answer| answer.count()) -} - -fn count_all_answers_for_query(db: &Database>, query: Expr>) -> usize { - let mut cx = db.query(query); - let mut count = 0; - while let Some(answer) = cx.prove_next() { - count += answer.count(); - } - count -} - -fn count_first_answers(db: &Database>, queries: &[Expr>]) -> usize { - queries - .iter() - .cloned() - .map(|query| count_first_answer_for_query(db, query)) - .sum() -} - -fn count_all_answers(db: &Database>, queries: &[Expr>]) -> usize { - queries - .iter() - .cloned() - .map(|query| count_all_answers_for_query(db, query)) - .sum() -} - -fn benchmark_candidate_filter(c: &mut Criterion) { - let mut group = c.benchmark_group("logic_eval_candidate_filter"); - group.sample_size(10); - - for case in CASES { - group.bench_with_input( - BenchmarkId::new("build_database", case.name), - case, - |b, case| { - b.iter(|| { - let interner = StrInterner::new(); - let db = build_database(&interner, case.requests); - black_box(db.clauses().count()) - }); - }, - ); - - let interner = StrInterner::new(); - let db = build_database(&interner, case.requests); - let query_start = case.requests / 2; - let queries = build_queries(&interner, query_start); - let expected_first = count_first_answers(&db, &queries); - let expected_all = count_all_answers(&db, &queries); - - group.bench_with_input(BenchmarkId::new("first_answer", case.name), case, |b, _| { - b.iter(|| { - let count = count_first_answers(&db, &queries); - assert_eq!(count, expected_first); - black_box(count) - }); - }); - - group.bench_with_input(BenchmarkId::new("all_answers", case.name), case, |b, _| { - b.iter(|| { - let count = count_all_answers(&db, &queries); - assert_eq!(count, expected_all); - black_box(count) - }); - }); - } - - group.finish(); -} - -criterion_group!(benches, benchmark_candidate_filter); -criterion_main!(benches); diff --git a/crates/logic-eval/benches/common/mod.rs b/crates/logic-eval/benches/common/mod.rs new file mode 100644 index 0000000..d634fe8 --- /dev/null +++ b/crates/logic-eval/benches/common/mod.rs @@ -0,0 +1,173 @@ +#![allow(dead_code)] + +pub(crate) const NOISE_CANDIDATES_PER_REQUEST: usize = 24; +pub(crate) const REQUESTS: usize = 128; + +pub(crate) fn type_name(index: usize) -> String { + format!("ty{index}") +} + +pub(crate) fn request_name(index: usize) -> String { + format!("req{index}") +} + +pub(crate) fn candidate_name(index: usize) -> String { + format!("candidate{index}") +} + +pub(crate) fn noise_candidate_name(request: usize, candidate: usize) -> String { + format!("candidate_noise_{request}_{candidate}") +} + +pub(crate) fn nested_type(name: &str) -> String { + format!("vec(vec({name}))") +} + +pub(crate) fn fact_count(requests: usize) -> usize { + requests * (3 + NOISE_CANDIDATES_PER_REQUEST) +} + +pub(crate) fn build_fact_source(requests: usize) -> String { + let mut source = String::new(); + append_fact_clauses(&mut source, requests); + source +} + +pub(crate) fn build_source(requests: usize) -> String { + let mut source = build_fact_source(requests); + + source.push_str( + " + same_shape($Value, $Value). + same_shape(vec($A), vec($B)) :- same_shape($A, $B). + candidate_match($Request, $Candidate) :- + candidate_value($Request, $Candidate, $Value), + expected_value($Request, $Expected), + same_shape($Value, $Expected), + enabled_candidate($Candidate). + ", + ); + + source +} + +pub(crate) fn representative_fact_clause(requests: usize) -> String { + let index = requests.saturating_sub(1); + format!( + "candidate_value({}, {}, {}).", + request_name(index), + candidate_name(index), + nested_type(&type_name(index)) + ) +} + +pub(crate) fn representative_rule_clause() -> &'static str { + " + candidate_match($Request, $Candidate) :- + candidate_value($Request, $Candidate, $Value), + expected_value($Request, $Expected), + same_shape($Value, $Expected), + enabled_candidate($Candidate). + " +} + +pub(crate) fn representative_rule_body_expr() -> &'static str { + " + candidate_value($Request, $Candidate, $Value), + expected_value($Request, $Expected), + same_shape($Value, $Expected), + enabled_candidate($Candidate) + " +} + +pub(crate) fn representative_nested_term(requests: usize) -> String { + let index = requests.saturating_sub(1); + format!( + "candidate_value({}, {}, {})", + request_name(index), + candidate_name(index), + nested_type(&type_name(index)) + ) +} + +pub(crate) fn delimiter_heavy_expr(terms: usize) -> String { + let mut expr = String::new(); + + for i in 0..terms { + if i > 0 { + if i % 4 == 0 { + expr.push_str("; "); + } else { + expr.push_str(", "); + } + } + + if i % 4 == 0 { + expr.push('('); + } + + expr.push_str(&format!( + "candidate_value({}, {}, {})", + request_name(i), + candidate_name(i), + nested_type(&type_name(i)) + )); + + if i % 4 == 3 || i + 1 == terms { + expr.push(')'); + } + } + + expr +} + +pub(crate) fn unique_names(count: usize) -> Vec { + (0..count).map(|i| format!("name{i}")).collect() +} + +pub(crate) fn repeated_names(count: usize) -> Vec<&'static str> { + const NAMES: &[&str] = &[ + "candidate_value", + "expected_value", + "enabled_candidate", + "same_shape", + "vec", + "req", + "candidate", + "$Request", + "$Candidate", + "$Value", + "$Expected", + ]; + + (0..count).map(|i| NAMES[i % NAMES.len()]).collect() +} + +fn append_fact_clauses(source: &mut String, requests: usize) { + for i in 0..requests { + let ty = type_name(i); + let expected = nested_type(&ty); + source.push_str(&format!( + "expected_value({}, {}).\n", + request_name(i), + expected + )); + source.push_str(&format!("enabled_candidate({}).\n", candidate_name(i))); + source.push_str(&format!( + "candidate_value({}, {}, {}).\n", + request_name(i), + candidate_name(i), + expected + )); + + for j in 0..NOISE_CANDIDATES_PER_REQUEST { + let noise_ty = type_name(requests + i * NOISE_CANDIDATES_PER_REQUEST + j); + source.push_str(&format!( + "candidate_value({}, {}, {}).\n", + request_name(i), + noise_candidate_name(i, j), + nested_type(&noise_ty) + )); + } + } +} diff --git a/crates/logic-eval/benches/database_build.rs b/crates/logic-eval/benches/database_build.rs new file mode 100644 index 0000000..95960f9 --- /dev/null +++ b/crates/logic-eval/benches/database_build.rs @@ -0,0 +1,223 @@ +mod common; + +use common::{build_source, NOISE_CANDIDATES_PER_REQUEST, REQUESTS}; +use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +use logic_eval::{ + parse_str, Atom, Clause, ClauseDataset, Database, InternedStr, Name, StrInterner, Term, +}; + +type BenchName<'a> = Name>; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum BenchAtom { + ExpectedValue, + EnabledCandidate, + CandidateValue, + SameShape, + CandidateMatch, + Vec, + Req(usize), + Candidate(usize), + NoiseCandidate(usize, usize), + Ty(usize), + RequestVar, + CandidateVar, + ValueVar, + ExpectedVar, + AVar, + BVar, +} + +impl Atom for BenchAtom { + fn is_variable(&self) -> bool { + matches!( + self, + Self::RequestVar + | Self::CandidateVar + | Self::ValueVar + | Self::ExpectedVar + | Self::AVar + | Self::BVar + ) + } +} + +fn atom(atom: BenchAtom) -> Term { + Term::atom(atom) +} + +fn term(functor: BenchAtom, args: impl IntoIterator>) -> Term { + Term::compound(functor, args) +} + +fn expr( + functor: BenchAtom, + args: impl IntoIterator>, +) -> logic_eval::Expr { + logic_eval::Expr::term_compound(functor, args) +} + +fn nested_atom_type(ty: usize) -> Term { + term( + BenchAtom::Vec, + [term(BenchAtom::Vec, [atom(BenchAtom::Ty(ty))])], + ) +} + +fn build_custom_atom_dataset(requests: usize) -> ClauseDataset { + let mut clauses = Vec::new(); + + for i in 0..requests { + let expected = nested_atom_type(i); + clauses.push(Clause::fact(term( + BenchAtom::ExpectedValue, + [atom(BenchAtom::Req(i)), expected.clone()], + ))); + clauses.push(Clause::fact(term( + BenchAtom::EnabledCandidate, + [atom(BenchAtom::Candidate(i))], + ))); + clauses.push(Clause::fact(term( + BenchAtom::CandidateValue, + [ + atom(BenchAtom::Req(i)), + atom(BenchAtom::Candidate(i)), + expected, + ], + ))); + + for j in 0..NOISE_CANDIDATES_PER_REQUEST { + let noise_ty = requests + i * NOISE_CANDIDATES_PER_REQUEST + j; + clauses.push(Clause::fact(term( + BenchAtom::CandidateValue, + [ + atom(BenchAtom::Req(i)), + atom(BenchAtom::NoiseCandidate(i, j)), + nested_atom_type(noise_ty), + ], + ))); + } + } + + clauses.push(Clause::fact(term( + BenchAtom::SameShape, + [atom(BenchAtom::ValueVar), atom(BenchAtom::ValueVar)], + ))); + clauses.push(Clause::rule( + term( + BenchAtom::SameShape, + [ + term(BenchAtom::Vec, [atom(BenchAtom::AVar)]), + term(BenchAtom::Vec, [atom(BenchAtom::BVar)]), + ], + ), + expr( + BenchAtom::SameShape, + [atom(BenchAtom::AVar), atom(BenchAtom::BVar)], + ), + )); + clauses.push(Clause::rule( + term( + BenchAtom::CandidateMatch, + [atom(BenchAtom::RequestVar), atom(BenchAtom::CandidateVar)], + ), + logic_eval::Expr::expr_and([ + expr( + BenchAtom::CandidateValue, + [ + atom(BenchAtom::RequestVar), + atom(BenchAtom::CandidateVar), + atom(BenchAtom::ValueVar), + ], + ), + expr( + BenchAtom::ExpectedValue, + [atom(BenchAtom::RequestVar), atom(BenchAtom::ExpectedVar)], + ), + expr( + BenchAtom::SameShape, + [atom(BenchAtom::ValueVar), atom(BenchAtom::ExpectedVar)], + ), + expr(BenchAtom::EnabledCandidate, [atom(BenchAtom::CandidateVar)]), + ]), + )); + + ClauseDataset(clauses) +} + +fn build_database<'a>(interner: &'a StrInterner, requests: usize) -> Database> { + let source = build_source(requests); + let dataset: ClauseDataset<_> = parse_str(&source, interner).unwrap(); + let mut db = Database::default(); + db.insert_dataset(dataset); + db +} + +fn build_custom_atom_database(requests: usize) -> Database { + let dataset = build_custom_atom_dataset(requests); + let mut db = Database::default(); + db.insert_dataset(dataset); + db +} + +fn benchmark_database_build(c: &mut Criterion) { + let mut group = c.benchmark_group("logic_eval_database_build"); + group.sample_size(10); + + group.bench_function("build_source", |b| { + b.iter(|| { + let source = build_source(REQUESTS); + black_box(source.len()) + }); + }); + + let source = build_source(REQUESTS); + group.bench_function("parse_dataset", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let dataset: ClauseDataset<_> = parse_str(black_box(&source), &interner).unwrap(); + black_box(dataset.len()) + }); + }); + + let interner = StrInterner::new(); + let dataset: ClauseDataset<_> = parse_str(&source, &interner).unwrap(); + group.bench_function("insert_dataset", |b| { + b.iter_batched( + || dataset.clone(), + |dataset| { + let mut db = Database::default(); + db.insert_dataset(dataset); + black_box(db.clauses().count()) + }, + BatchSize::SmallInput, + ); + }); + + group.bench_function("build_custom_atom_dataset", |b| { + b.iter(|| { + let dataset = build_custom_atom_dataset(REQUESTS); + black_box(dataset.len()) + }); + }); + + group.bench_function("build_custom_atom_database", |b| { + b.iter(|| { + let db = build_custom_atom_database(REQUESTS); + black_box(db.clauses().count()) + }); + }); + + group.bench_function("build_database", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let db = build_database(&interner, REQUESTS); + black_box(db.clauses().count()) + }); + }); + + group.finish(); +} + +criterion_group!(benches, benchmark_database_build); +criterion_main!(benches); diff --git a/crates/logic-eval/benches/parse.rs b/crates/logic-eval/benches/parse.rs new file mode 100644 index 0000000..fd55d77 --- /dev/null +++ b/crates/logic-eval/benches/parse.rs @@ -0,0 +1,130 @@ +mod common; + +use common::{ + build_fact_source, delimiter_heavy_expr, fact_count, repeated_names, + representative_fact_clause, representative_nested_term, representative_rule_body_expr, + representative_rule_clause, unique_names, REQUESTS, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use logic_eval::{parse_str, Clause, ClauseDataset, Intern, StrInterner, Term}; + +fn benchmark_parse(c: &mut Criterion) { + let mut group = c.benchmark_group("logic_eval_parse"); + group.sample_size(10); + + let fact_source = build_fact_source(REQUESTS); + group.bench_function("parse_fact_dataset", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let dataset: ClauseDataset<_> = parse_str(black_box(&fact_source), &interner).unwrap(); + black_box(dataset.len()) + }); + }); + + let fact_clause = representative_fact_clause(REQUESTS); + group.bench_function("parse_fact_clause", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let clause: Clause<_> = parse_str(black_box(&fact_clause), &interner).unwrap(); + black_box(&clause); + }); + }); + + let rule_clause = representative_rule_clause(); + group.bench_function("parse_rule_clause", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let clause: Clause<_> = parse_str(black_box(rule_clause), &interner).unwrap(); + black_box(&clause); + }); + }); + + let rule_body_expr = representative_rule_body_expr(); + group.bench_function("parse_rule_body_expr", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let expr: logic_eval::Expr<_> = + parse_str(black_box(rule_body_expr), &interner).unwrap(); + black_box(&expr); + }); + }); + + let nested_term = representative_nested_term(REQUESTS); + group.bench_function("parse_nested_term", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let term: Term<_> = parse_str(black_box(&nested_term), &interner).unwrap(); + black_box(&term); + }); + }); + + let delimiter_expr = delimiter_heavy_expr(REQUESTS); + group.bench_function("parse_delimiter_heavy_expr", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let expr: logic_eval::Expr<_> = + parse_str(black_box(&delimiter_expr), &interner).unwrap(); + black_box(&expr); + }); + }); + + let name_count = fact_count(REQUESTS) * 4; + let unique_names = unique_names(name_count); + group.bench_function("parse_unique_atom_terms", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let mut parsed = 0; + for name in &unique_names { + let term: Term<_> = parse_str(black_box(name.as_str()), &interner).unwrap(); + black_box(&term); + parsed += 1; + } + black_box(parsed) + }); + }); + + let repeated_names = repeated_names(name_count); + group.bench_function("parse_repeated_atom_terms", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let mut parsed = 0; + for name in &repeated_names { + let term: Term<_> = parse_str(black_box(*name), &interner).unwrap(); + black_box(&term); + parsed += 1; + } + black_box(parsed) + }); + }); + + group.bench_function("intern_unique_names", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let mut interned = 0; + for name in &unique_names { + let name = interner.intern_str(black_box(name.as_str())); + black_box(&name); + interned += 1; + } + black_box(interned) + }); + }); + + group.bench_function("intern_repeated_names", |b| { + b.iter(|| { + let interner = StrInterner::new(); + let mut interned = 0; + for name in &repeated_names { + let name = interner.intern_str(black_box(*name)); + black_box(&name); + interned += 1; + } + black_box(interned) + }); + }); + + group.finish(); +} + +criterion_group!(benches, benchmark_parse); +criterion_main!(benches); diff --git a/crates/logic-eval/benches/query.rs b/crates/logic-eval/benches/query.rs new file mode 100644 index 0000000..a0a6013 --- /dev/null +++ b/crates/logic-eval/benches/query.rs @@ -0,0 +1,95 @@ +mod common; + +use common::{build_source, request_name, REQUESTS}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use logic_eval::{parse_str, ClauseDataset, Database, Expr, InternedStr, Name, StrInterner}; + +type BenchName<'a> = Name>; + +const QUERY_BATCH_LEN: usize = 10; + +fn build_database<'a>(interner: &'a StrInterner, requests: usize) -> Database> { + let source = build_source(requests); + let dataset: ClauseDataset<_> = parse_str(&source, interner).unwrap(); + let mut db = Database::default(); + db.insert_dataset(dataset); + db +} + +fn build_query<'a>(interner: &'a StrInterner, request: usize) -> Expr> { + parse_str( + &format!("candidate_match({}, $Candidate)", request_name(request)), + interner, + ) + .unwrap() +} + +fn build_queries<'a>(interner: &'a StrInterner, start_request: usize) -> Vec>> { + (start_request..start_request + QUERY_BATCH_LEN) + .map(|request| build_query(interner, request)) + .collect() +} + +fn count_first_answer_for_query(db: &Database>, query: Expr>) -> usize { + db.query(query) + .prove_next() + .map_or(0, |answer| answer.count()) +} + +fn count_all_answers_for_query(db: &Database>, query: Expr>) -> usize { + let mut cx = db.query(query); + let mut count = 0; + while let Some(answer) = cx.prove_next() { + count += answer.count(); + } + count +} + +fn count_first_answers(db: &Database>, queries: &[Expr>]) -> usize { + queries + .iter() + .cloned() + .map(|query| count_first_answer_for_query(db, query)) + .sum() +} + +fn count_all_answers(db: &Database>, queries: &[Expr>]) -> usize { + queries + .iter() + .cloned() + .map(|query| count_all_answers_for_query(db, query)) + .sum() +} + +fn benchmark_candidate_filter(c: &mut Criterion) { + let mut group = c.benchmark_group("logic_eval_candidate_filter"); + group.sample_size(10); + + let interner = StrInterner::new(); + let db = build_database(&interner, REQUESTS); + let query_start = REQUESTS / 2; + let queries = build_queries(&interner, query_start); + let expected_first = count_first_answers(&db, &queries); + let expected_all = count_all_answers(&db, &queries); + + group.bench_function("first_answer", |b| { + b.iter(|| { + let count = count_first_answers(&db, &queries); + assert_eq!(count, expected_first); + black_box(count) + }); + }); + + group.bench_function("all_answers", |b| { + b.iter(|| { + let count = count_all_answers(&db, &queries); + assert_eq!(count, expected_all); + black_box(count) + }); + }); + + group.finish(); +} + +criterion_group!(benches, benchmark_candidate_filter); +criterion_main!(benches); diff --git a/crates/logic-eval/src/parse/inner.rs b/crates/logic-eval/src/parse/inner.rs index 3c200c1..e6d2421 100644 --- a/crates/logic-eval/src/parse/inner.rs +++ b/crates/logic-eval/src/parse/inner.rs @@ -23,8 +23,20 @@ where /// A parser for values that may borrow interned strings. pub trait Parse<'int, Int: Intern>: Sized + 'int { + /// Tries to parse a value without constructing an error on mismatch. + /// + /// Implementations must leave `buf` unchanged when returning `None`. + fn try_parse(_buf: &mut ParseBuffer<'_>, _interner: &'int Int) -> Option; + + /// Creates the error reported by [`Self::parse`] when parsing fails. + fn expected_error(buf: &ParseBuffer<'_>) -> Error { + format!("failed to parse from {}", buf.cur_text()).into() + } + /// Parses a value from `buf` using `interner`. - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result; + fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { + Self::try_parse(buf, interner).ok_or_else(|| Self::expected_error(buf)) + } } /// A cursor over a bounded slice of parser input. @@ -65,8 +77,7 @@ impl<'a> ParseBuffer<'a> { T: Parse<'int, Int>, { let mut peek = *self; - // FIXME: No need to create error messages, which may cause performance issue. - T::parse(&mut peek, interner).ok().map(|t| (t, peek)) + T::try_parse(&mut peek, interner).map(|t| (t, peek)) } } @@ -79,7 +90,7 @@ impl Ident { } impl Parse<'_, Int> for Ident { - fn parse(buf: &mut ParseBuffer<'_>, _: &'_ Int) -> Result { + fn try_parse(buf: &mut ParseBuffer<'_>, _: &'_ Int) -> Option { fn is_allowed_first(c: char) -> bool { c.is_alphabetic() || !(c.is_whitespace() || RESERVED.contains(&c)) } @@ -90,9 +101,7 @@ impl Parse<'_, Int> for Ident { let s = buf.cur_text(); - let Some(l) = s.find(|c: char| !c.is_whitespace()) else { - return Err("expected an ident, but input is empty".into()); - }; + let l = s.find(|c: char| !c.is_whitespace())?; let mut r = l; @@ -100,7 +109,7 @@ impl Parse<'_, Int> for Ident { if is_allowed_first(first) { r += first.len_utf8(); } else { - return Err(format!("expected an ident from {}", s).into()); + return None; } for rest in s[l..].chars().skip(1) { @@ -116,24 +125,29 @@ impl Parse<'_, Int> for Ident { right: buf.start + r, }; buf.start += r; - Ok(Ident(loc)) + Some(Ident(loc)) + } + + fn expected_error(buf: &ParseBuffer<'_>) -> Error { + let s = buf.cur_text(); + if s.chars().all(char::is_whitespace) { + "expected an ident, but input is empty".into() + } else { + format!("expected an ident from {s}").into() + } } } macro_rules! impl_parse_for_string { ($str:literal, $ty:ident) => { impl Parse<'_, Int> for $ty { - fn parse(buf: &mut ParseBuffer<'_>, _: &Int) -> Result { + fn try_parse(buf: &mut ParseBuffer<'_>, _: &Int) -> Option { let s = buf.cur_text(); - let Some(l) = s.find(|c: char| !c.is_whitespace()) else { - return Err(format!("expected `{}` from `{}`", $str, s).into()); - }; + let l = s.find(|c: char| !c.is_whitespace())?; let r = l + $str.len(); - let substr = s - .get(l..r) - .ok_or(Error::from(format!("expected `{}` from `{s}`", $str)))?; + let substr = s.get(l..r)?; if substr == $str { let loc = Location { @@ -141,11 +155,15 @@ macro_rules! impl_parse_for_string { right: buf.start + r, }; buf.start += r; - Ok($ty { _loc: loc }) + Some($ty { _loc: loc }) } else { - Err(format!("expected `{}` from `{s}`", $str).into()) + None } } + + fn expected_error(buf: &ParseBuffer<'_>) -> Error { + format!("expected `{}` from `{}`", $str, buf.cur_text()).into() + } } }; } @@ -213,7 +231,7 @@ pub(crate) struct Location { #[cfg(test)] mod tests { use super::*; - use crate::parse::repr::Clause; + use crate::{parse::repr::Clause, ClauseIn, TermIn}; type Interner = any_intern::DroplessInterner; @@ -234,4 +252,36 @@ mod tests { assert("f(a, b) :- f(a); f(b).", &interner); assert("f(a, b) :- f(a), (f(b); f(c)).", &interner); } + + #[test] + fn clause_requires_trailing_dot() { + let interner = Interner::new(); + let mut buf = ParseBuffer::new("f"); + let original = buf; + + assert!(ClauseIn::<'_, Interner>::try_parse(&mut buf, &interner).is_none()); + assert_eq!(buf.start, original.start); + assert_eq!(buf.end, original.end); + } + + #[test] + fn try_parse_failure_preserves_buffer() { + fn assert_unchanged<'int, T>(text: &str, interner: &'int Interner) + where + T: Parse<'int, Interner>, + { + let mut buf = ParseBuffer::new(text); + let original = buf; + assert!(T::try_parse(&mut buf, interner).is_none()); + assert_eq!(buf.start, original.start); + assert_eq!(buf.end, original.end); + } + + let interner = Interner::new(); + + assert_unchanged::("foo", &interner); + assert_unchanged::(",", &interner); + assert_unchanged::>("foo(", &interner); + assert_unchanged::>("foo(", &interner); + } } diff --git a/crates/logic-eval/src/parse/text.rs b/crates/logic-eval/src/parse/text.rs index 1fef56b..3c1889b 100644 --- a/crates/logic-eval/src/parse/text.rs +++ b/crates/logic-eval/src/parse/text.rs @@ -3,7 +3,7 @@ use super::{ CloseParenToken, CommaToken, DotToken, HornToken, Ident, NegationToken, OpenParenToken, Parse, ParseBuffer, }; -use crate::{ClauseDatasetIn, ClauseIn, Error, ExprIn, Intern, NameIn, Result, TermIn}; +use crate::{ClauseDatasetIn, ClauseIn, ExprIn, Intern, NameIn, Result, TermIn}; use std::{ borrow::Borrow, fmt::{self, Debug, Display}, @@ -12,57 +12,101 @@ use std::{ }; impl<'int, Int: Intern> Parse<'int, Int> for ClauseDatasetIn<'int, Int> { - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { + fn try_parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Option { + fn next_clause_buf<'a>(buf: &ParseBuffer<'a>) -> Option> { + let dot = buf.cur_text().find('.')?; + Some(ParseBuffer { + text: buf.text, + start: buf.start, + end: buf.start + dot + 1, + }) + } + let mut v = Vec::new(); - while let Some(moved_buf) = { - let mut peek = *buf; - ClauseIn::<'int, Int>::parse(&mut peek, interner) - .ok() - .map(|c| (c, peek)) - } { - let (clause, moved) = moved_buf; + + while let Some(mut clause_buf) = next_clause_buf(buf) { + let Some(clause) = ClauseIn::<'int, Int>::try_parse(&mut clause_buf, interner) else { + break; + }; v.push(clause); - *buf = moved; + buf.start = clause_buf.start; } - Ok(ClauseDataset(v)) + + Some(ClauseDataset(v)) } } impl<'int, Int: Intern> Parse<'int, Int> for ClauseIn<'int, Int> { - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { - let head = TermIn::<'int, Int>::parse(buf, interner)?; - - let body = if let Some((_, mut body_buf)) = buf.peek_parse::(interner) { - let dot = body_buf - .cur_text() - .find('.') - .ok_or(Error::from("clause must end with `.`"))?; - body_buf.end = body_buf.start + dot; - let body = ExprIn::<'int, Int>::parse(&mut body_buf, interner)?; - - buf.start = body_buf.end + 1; // Next to the dot - Some(body) - } else { - let _ = buf.parse::(interner)?; - None - }; + fn try_parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Option { + fn parse_inner<'int, Int: Intern>( + buf: &mut ParseBuffer<'_>, + interner: &'int Int, + ) -> Option> { + let head = TermIn::<'int, Int>::try_parse(buf, interner)?; + + let body = if let Some((_, mut body_buf)) = buf.peek_parse::(interner) { + let dot = body_buf.cur_text().find('.')?; + body_buf.end = body_buf.start + dot; + + let body = ExprIn::<'int, Int>::try_parse(&mut body_buf, interner)?; + + buf.start = body_buf.end + 1; // Next to the dot + Some(body) + } else { + DotToken::try_parse(buf, interner)?; + None + }; + + Some(Clause { head, body }) + } - Ok(Clause { head, body }) + let original = *buf; + let parsed = parse_inner(buf, interner); + if parsed.is_none() { + *buf = original; + } + parsed + } + + fn expected_error(buf: &ParseBuffer<'_>) -> crate::Error { + format!("expected a clause from {}", buf.cur_text()).into() } } impl<'int, Int: Intern> Parse<'int, Int> for ExprIn<'int, Int> { /// Caller is supposed to give the exact buffer for an Expr. - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { - Expr::>::parse_or(*buf, interner) + fn try_parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Option { + let original = *buf; + match Expr::>::parse_or(*buf, interner) { + Ok(expr) => Some(expr), + Err(_) => { + *buf = original; + None + } + } + } + + fn expected_error(buf: &ParseBuffer<'_>) -> crate::Error { + format!("expected an expression from {}", buf.cur_text()).into() } } impl<'int, Int: Intern> Parse<'int, Int> for TermIn<'int, Int> { - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { - let functor = Name::parse(buf, interner)?; - let args = Term::parse_args(buf, interner)?; - Ok(Term { functor, args }) + fn try_parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Option { + let original = *buf; + let functor = Name::try_parse(buf, interner)?; + let args = match Term::parse_args(buf, interner) { + Ok(args) => args, + Err(_) => { + *buf = original; + return None; + } + }; + Some(Term { functor, args }) + } + + fn expected_error(buf: &ParseBuffer<'_>) -> crate::Error { + format!("expected a term from {}", buf.cur_text()).into() } } @@ -252,10 +296,10 @@ impl Name<()> { } impl<'int, Int: Intern> Parse<'int, Int> for NameIn<'int, Int> { - fn parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Result { - let ident = buf.parse::(interner)?; + fn try_parse(buf: &mut ParseBuffer<'_>, interner: &'int Int) -> Option { + let ident = Ident::try_parse(buf, interner)?; let interned = interner.intern_str(ident.to_text(buf.text)); - Ok(Self(interned)) + Some(Self(interned)) } } diff --git a/crates/logic-eval/src/prove/db.rs b/crates/logic-eval/src/prove/db.rs index ca6e9a0..e0e2e52 100644 --- a/crates/logic-eval/src/prove/db.rs +++ b/crates/logic-eval/src/prove/db.rs @@ -411,7 +411,7 @@ pub(crate) struct PredicateClauses { impl PredicateClauses { fn insert(&mut self, clause: ClauseId, storage: &TermStorage) { - if self.all.contains(&clause) { + if self.positions.contains_key(&clause) { if cfg!(debug_assertions) { panic!("duplicate clause inserted into PredicateClauses: {clause:?}") } else {