1mod metrics;
4mod recognizer;
5mod sampler;
6
7use std::error::Error;
8use std::fmt;
9
10use crate::grammar::Grammar;
11
12pub trait MembershipOracle {
14 fn accepts(&self, text: &str) -> bool;
16}
17
18#[derive(Clone, Copy, Debug)]
20pub struct GrammarOracle<'g>(pub &'g Grammar);
21
22impl<'g> GrammarOracle<'g> {
23 #[must_use]
25 pub const fn new(grammar: &'g Grammar) -> Self {
26 Self(grammar)
27 }
28
29 #[must_use]
31 pub fn accepts(&self, text: &str) -> bool {
32 <Self as MembershipOracle>::accepts(self, text)
33 }
34}
35
36impl MembershipOracle for GrammarOracle<'_> {
37 fn accepts(&self, text: &str) -> bool {
38 recognizer::accepts(self.0, text)
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct SampleConfig {
45 pub seed: u64,
47 pub count: usize,
49 pub max_depth: usize,
51 pub repeat_cap: usize,
53}
54
55impl Default for SampleConfig {
56 fn default() -> Self {
57 Self {
58 seed: 0xD1E5_EED5_17A7_E001,
59 count: 256,
60 max_depth: 16,
61 repeat_cap: 4,
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct MetricScores {
69 pub precision: f64,
71 pub recall: f64,
73 pub f1: f64,
75 pub size_symbols: usize,
77 pub mdl_bits: f64,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum ScoringMode {
84 GoldenGrammar,
86 Corpus,
88}
89
90#[derive(Clone, Debug, PartialEq)]
92pub struct BenchmarkReport {
93 pub corpus: &'static str,
95 pub scores: MetricScores,
97 pub samples_drawn: usize,
99 pub seed: u64,
101 pub scoring_mode: ScoringMode,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub enum EvalError {
108 EmptyGrammar,
110 EmptyCorpus,
112 EmptySample {
114 source: &'static str,
116 },
117 NonTerminating {
119 rule: String,
121 },
122 UnknownRule {
124 rule: String,
126 },
127 InvalidCharRange {
129 start: char,
131 end: char,
133 },
134 EmptyCharClass,
136 InvalidRepeat {
138 min: usize,
140 max: usize,
142 },
143 CorpusNotFound {
145 corpus: String,
147 },
148 UnsupportedExpression {
151 construct: String,
153 },
154}
155
156impl fmt::Display for EvalError {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::EmptyGrammar => formatter.write_str("grammar has no start rule"),
160 Self::EmptyCorpus => formatter.write_str("corpus-mode recall requires positives"),
161 Self::EmptySample { source } => write!(formatter, "{source} sampling produced no text"),
162 Self::NonTerminating { rule } => write!(formatter, "rule `{rule}` cannot terminate"),
163 Self::UnknownRule { rule } => write!(formatter, "unknown grammar rule `{rule}`"),
164 Self::InvalidCharRange { start, end } => {
165 write!(formatter, "invalid character range {start:?}..={end:?}")
166 }
167 Self::EmptyCharClass => formatter.write_str("character class has no sampleable member"),
168 Self::InvalidRepeat { min, max } => {
169 write!(formatter, "invalid repetition bounds {min}..={max}")
170 }
171 Self::CorpusNotFound { corpus } => write!(formatter, "unknown corpus `{corpus}`"),
172 Self::UnsupportedExpression { construct } => {
173 write!(
174 formatter,
175 "sampling does not evaluate `{construct}` expressions"
176 )
177 }
178 }
179 }
180}
181
182impl Error for EvalError {}
183
184#[derive(Clone, Copy, Debug)]
186pub struct GoldenCorpus {
187 name: &'static str,
188 positives: &'static [&'static str],
189 golden_grammar: fn() -> Grammar,
190}
191
192impl GoldenCorpus {
193 #[must_use]
195 pub const fn new(
196 name: &'static str,
197 positives: &'static [&'static str],
198 golden_grammar: fn() -> Grammar,
199 ) -> Self {
200 Self {
201 name,
202 positives,
203 golden_grammar,
204 }
205 }
206
207 #[must_use]
209 pub const fn name(&self) -> &'static str {
210 self.name
211 }
212
213 #[must_use]
215 pub const fn positives(&self) -> &'static [&'static str] {
216 self.positives
217 }
218
219 #[must_use]
221 pub fn golden_grammar(&self) -> Grammar {
222 (self.golden_grammar)()
223 }
224}
225
226const LIST_POSITIVES: &[&str] = &["a", "b", "a,b", "b,a"];
227const ASSIGNMENT_POSITIVES: &[&str] = &["let a=1;", "let b=2;", "let x=9;"];
228
229pub const GOLDEN_CORPORA: &[GoldenCorpus] = &[
231 GoldenCorpus::new("inference-eval:list", LIST_POSITIVES, list_corpus_grammar),
232 GoldenCorpus::new(
233 "inference-eval:assignment",
234 ASSIGNMENT_POSITIVES,
235 assignment_corpus_grammar,
236 ),
237];
238
239pub fn sample(grammar: &Grammar, config: &SampleConfig) -> Result<Vec<String>, EvalError> {
246 sampler::sample(grammar, config)
247}
248
249pub fn evaluate(
256 inferred: &Grammar,
257 golden: &dyn MembershipOracle,
258 golden_sampler: Option<&Grammar>,
259 positives: &[&str],
260 config: &SampleConfig,
261) -> Result<MetricScores, EvalError> {
262 evaluate_outcome(inferred, golden, golden_sampler, positives, config)
263 .map(|outcome| outcome.scores)
264}
265
266pub fn run_corpus(
268 corpus: &GoldenCorpus,
269 inferred: &Grammar,
270 config: &SampleConfig,
271) -> Result<BenchmarkReport, EvalError> {
272 let golden = corpus.golden_grammar();
273 let oracle = GrammarOracle(&golden);
274 let outcome = evaluate_outcome(inferred, &oracle, Some(&golden), corpus.positives(), config)?;
275
276 Ok(BenchmarkReport {
277 corpus: corpus.name(),
278 scores: outcome.scores,
279 samples_drawn: outcome.samples_drawn,
280 seed: config.seed,
281 scoring_mode: outcome.scoring_mode,
282 })
283}
284
285pub fn run_named_corpus(
287 corpus: &str,
288 inferred: &Grammar,
289 config: &SampleConfig,
290) -> Result<BenchmarkReport, EvalError> {
291 let descriptor = GOLDEN_CORPORA
292 .iter()
293 .find(|candidate| candidate.name() == corpus)
294 .ok_or_else(|| EvalError::CorpusNotFound {
295 corpus: corpus.to_string(),
296 })?;
297 run_corpus(descriptor, inferred, config)
298}
299
300#[must_use]
302pub fn size_symbols(grammar: &Grammar) -> usize {
303 metrics::size_symbols(grammar)
304}
305
306#[must_use]
314pub fn mdl(grammar: &Grammar, data: &[&str]) -> f64 {
315 metrics::mdl(grammar, data)
316}
317
318#[derive(Clone, Debug)]
319struct EvaluationOutcome {
320 scores: MetricScores,
321 samples_drawn: usize,
322 scoring_mode: ScoringMode,
323}
324
325fn evaluate_outcome(
326 inferred: &Grammar,
327 golden: &dyn MembershipOracle,
328 golden_sampler: Option<&Grammar>,
329 positives: &[&str],
330 config: &SampleConfig,
331) -> Result<EvaluationOutcome, EvalError> {
332 let inferred_samples = sample(inferred, config)?;
333 if inferred_samples.is_empty() {
334 return Err(EvalError::EmptySample { source: "inferred" });
335 }
336
337 let precision_hits = inferred_samples
338 .iter()
339 .filter(|text| golden.accepts(text))
340 .count();
341 let precision = metrics::ratio(precision_hits, inferred_samples.len());
342 let inferred_oracle = GrammarOracle(inferred);
343
344 let (recall, mdl_bits, samples_drawn, scoring_mode) =
345 if let Some(golden_grammar) = golden_sampler {
346 let reference_samples = sample(golden_grammar, config)?;
347 if reference_samples.is_empty() {
348 return Err(EvalError::EmptySample { source: "golden" });
349 }
350 let recall_hits = reference_samples
351 .iter()
352 .filter(|text| inferred_oracle.accepts(text))
353 .count();
354 let data = reference_samples
355 .iter()
356 .map(String::as_str)
357 .collect::<Vec<_>>();
358 (
359 metrics::ratio(recall_hits, reference_samples.len()),
360 mdl(inferred, &data),
361 inferred_samples
362 .len()
363 .saturating_add(reference_samples.len()),
364 ScoringMode::GoldenGrammar,
365 )
366 } else {
367 if positives.is_empty() {
368 return Err(EvalError::EmptyCorpus);
369 }
370 let recall_hits = positives
371 .iter()
372 .filter(|text| inferred_oracle.accepts(text))
373 .count();
374 (
375 metrics::ratio(recall_hits, positives.len()),
376 mdl(inferred, positives),
377 inferred_samples.len().saturating_add(positives.len()),
378 ScoringMode::Corpus,
379 )
380 };
381
382 let f1 = if precision + recall == 0.0 {
383 0.0
384 } else {
385 2.0 * precision * recall / (precision + recall)
386 };
387
388 Ok(EvaluationOutcome {
389 scores: MetricScores {
390 precision,
391 recall,
392 f1,
393 size_symbols: size_symbols(inferred),
394 mdl_bits,
395 },
396 samples_drawn,
397 scoring_mode,
398 })
399}
400
401fn list_corpus_grammar() -> Grammar {
402 let expr = Grammar::expr();
403 Grammar::builder()
404 .start("list")
405 .rule(
406 "list",
407 expr.seq([
408 expr.nt("item"),
409 expr.rep0(expr.seq([expr.term(","), expr.nt("item")])),
410 ]),
411 )
412 .rule(
413 "item",
414 expr.choice_unordered([expr.term("a"), expr.term("b")]),
415 )
416 .build()
417}
418
419fn assignment_corpus_grammar() -> Grammar {
420 let expr = Grammar::expr();
421 Grammar::builder()
422 .start("assignment")
423 .rule(
424 "assignment",
425 expr.seq([
426 expr.term("let "),
427 expr.nt("letter"),
428 expr.term("="),
429 expr.nt("digit"),
430 expr.term(";"),
431 ]),
432 )
433 .rule("letter", expr.char_range('a', 'z'))
434 .rule("digit", expr.char_range('0', '9'))
435 .build()
436}