1use crate::grammar::{
8 CharClassItem, GRAMMAR_CONCEPTS, Grammar, GrammarExpr, GrammarRule, RuleKind,
9 grammar_expr_concept_id,
10};
11
12use super::minimize::mdl_cost;
13
14const COST_EPSILON: f64 = 1e-9;
15
16const INFERENCE_NAMING_CONCEPTS: &[InferenceNamingConcept] = &[
17 InferenceNamingConcept {
18 id: "grammar.number",
19 name: "number",
20 },
21 InferenceNamingConcept {
22 id: "grammar.digit",
23 name: "digit",
24 },
25 InferenceNamingConcept {
26 id: "grammar.letter",
27 name: "letter",
28 },
29 InferenceNamingConcept {
30 id: "grammar.identifier",
31 name: "identifier",
32 },
33 InferenceNamingConcept {
34 id: "grammar.string",
35 name: "string",
36 },
37 InferenceNamingConcept {
38 id: "grammar.boolean-value",
39 name: "boolean",
40 },
41 InferenceNamingConcept {
42 id: "grammar.null",
43 name: "null",
44 },
45 InferenceNamingConcept {
46 id: "grammar.value",
47 name: "value",
48 },
49 InferenceNamingConcept {
50 id: "grammar.item",
51 name: "item",
52 },
53 InferenceNamingConcept {
54 id: "grammar.list",
55 name: "list",
56 },
57 InferenceNamingConcept {
58 id: "grammar.name",
59 name: "name",
60 },
61 InferenceNamingConcept {
62 id: "grammar.object",
63 name: "object",
64 },
65 InferenceNamingConcept {
66 id: "grammar.member",
67 name: "member",
68 },
69];
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72struct InferenceNamingConcept {
73 id: &'static str,
74 name: &'static str,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum AdviceSource {
80 Deterministic,
82 Llm,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum AdviceDecisionKind {
89 Naming,
91 Merge,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct AdviceDecision {
98 pub kind: AdviceDecisionKind,
100 pub target: String,
102 pub source: AdviceSource,
104}
105
106impl AdviceDecision {
107 #[must_use]
109 pub fn new(kind: AdviceDecisionKind, target: impl Into<String>, source: AdviceSource) -> Self {
110 Self {
111 kind,
112 target: target.into(),
113 source,
114 }
115 }
116}
117
118#[derive(Clone, Copy, Debug)]
120pub struct NamingRequest<'a> {
121 pub grammar: &'a Grammar,
123 pub rule_expr: &'a GrammarExpr,
125 pub sample_yields: &'a [String],
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct NameCandidate {
132 pub name: String,
134 pub concept: Option<String>,
136 pub source: AdviceSource,
138}
139
140#[derive(Clone, Copy, Debug)]
142pub struct MergeRequest<'a> {
143 pub grammar: &'a Grammar,
145 pub candidates: &'a [MergeCandidate],
147 pub examples: &'a [String],
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct MergeCandidate {
156 pub winner: String,
158 pub loser: String,
160}
161
162impl MergeCandidate {
163 #[must_use]
165 pub fn new(winner: impl Into<String>, loser: impl Into<String>) -> Self {
166 Self {
167 winner: winner.into(),
168 loser: loser.into(),
169 }
170 }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq)]
175pub struct MergeScore {
176 pub score: f64,
178 pub source: AdviceSource,
180}
181
182pub trait NamingAdvisor {
184 fn propose_names(&self, request: &NamingRequest<'_>) -> Vec<NameCandidate>;
186}
187
188pub trait MergeAdvisor {
190 fn rank_merges(&self, request: &MergeRequest<'_>) -> Vec<MergeScore>;
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
196pub struct ConceptNamingAdvisor;
197
198impl NamingAdvisor for ConceptNamingAdvisor {
199 fn propose_names(&self, request: &NamingRequest<'_>) -> Vec<NameCandidate> {
200 let (base_name, concept) = concept_name_for_request(request).map_or_else(
201 || (structural_name(request.rule_expr), None),
202 |concept| (concept.name.to_string(), Some(concept.id.to_string())),
203 );
204 let name = unique_rule_name(&sanitize_identifier(&base_name), request.grammar);
205
206 vec![NameCandidate {
207 name,
208 concept,
209 source: AdviceSource::Deterministic,
210 }]
211 }
212}
213
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub struct MdlMergeAdvisor;
217
218impl MergeAdvisor for MdlMergeAdvisor {
219 fn rank_merges(&self, request: &MergeRequest<'_>) -> Vec<MergeScore> {
220 let baseline = mdl_cost(request.grammar, request.examples).total();
221
222 request
223 .candidates
224 .iter()
225 .map(|candidate| {
226 let score =
227 merge_candidate_grammar(request.grammar, candidate).map_or(0.0, |trial| {
228 let delta = mdl_cost(&trial, request.examples).total() - baseline;
229 score_from_delta(delta)
230 });
231 MergeScore {
232 score,
233 source: AdviceSource::Deterministic,
234 }
235 })
236 .collect()
237 }
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct FallbackAdvisor<A, D> {
243 accelerator: Option<A>,
244 deterministic: D,
245}
246
247impl<A, D> FallbackAdvisor<A, D> {
248 #[must_use]
250 pub const fn new(accelerator: Option<A>, deterministic: D) -> Self {
251 Self {
252 accelerator,
253 deterministic,
254 }
255 }
256
257 #[must_use]
259 pub const fn deterministic(deterministic: D) -> Self {
260 Self {
261 accelerator: None,
262 deterministic,
263 }
264 }
265}
266
267impl<A, D> NamingAdvisor for FallbackAdvisor<A, D>
268where
269 A: NamingAdvisor,
270 D: NamingAdvisor,
271{
272 fn propose_names(&self, request: &NamingRequest<'_>) -> Vec<NameCandidate> {
273 let deterministic = self.deterministic.propose_names(request);
274 let Some(accelerator) = &self.accelerator else {
275 return deterministic;
276 };
277
278 let accelerated = accelerator
279 .propose_names(request)
280 .into_iter()
281 .filter(|candidate| validate_name_candidate(request, candidate))
282 .collect::<Vec<_>>();
283
284 if accelerated.is_empty() {
285 deterministic
286 } else {
287 accelerated
288 }
289 }
290}
291
292impl<A, D> MergeAdvisor for FallbackAdvisor<A, D>
293where
294 A: MergeAdvisor,
295 D: MergeAdvisor,
296{
297 fn rank_merges(&self, request: &MergeRequest<'_>) -> Vec<MergeScore> {
298 let deterministic = self.deterministic.rank_merges(request);
299 let Some(accelerator) = &self.accelerator else {
300 return deterministic;
301 };
302
303 validate_merge_scores(accelerator.rank_merges(request), request.candidates.len())
304 .unwrap_or(deterministic)
305 }
306}
307
308#[cfg(feature = "llm-assist")]
309mod llm;
310#[cfg(feature = "llm-assist")]
311pub use llm::{LlmClient, LlmError, LlmMergeAdvisor, LlmNamingAdvisor};
312
313fn concept_name_for_request(request: &NamingRequest<'_>) -> Option<InferenceNamingConcept> {
314 concept_from_samples(request.sample_yields).or_else(|| concept_from_expr(request.rule_expr))
315}
316
317fn concept_from_samples(samples: &[String]) -> Option<InferenceNamingConcept> {
318 if samples.is_empty() || samples.iter().any(String::is_empty) {
319 return None;
320 }
321
322 if samples.iter().all(|sample| is_integer_text(sample)) {
323 return inference_concept("grammar.number");
324 }
325 if samples.iter().all(|sample| is_digit_text(sample)) {
326 return inference_concept("grammar.digit");
327 }
328 if samples.iter().all(|sample| is_identifier_text(sample)) {
329 return inference_concept("grammar.identifier");
330 }
331 if samples.iter().all(|sample| is_letter_text(sample)) {
332 return inference_concept("grammar.letter");
333 }
334
335 None
336}
337
338fn concept_from_expr(expr: &GrammarExpr) -> Option<InferenceNamingConcept> {
339 if is_number_expr(expr) {
340 return inference_concept("grammar.number");
341 }
342 if is_digit_expr(expr) {
343 return inference_concept("grammar.digit");
344 }
345 if is_identifier_expr(expr) {
346 return inference_concept("grammar.identifier");
347 }
348 if is_letter_expr(expr) {
349 return inference_concept("grammar.letter");
350 }
351
352 None
353}
354
355fn inference_concept(id: &str) -> Option<InferenceNamingConcept> {
356 INFERENCE_NAMING_CONCEPTS
357 .iter()
358 .copied()
359 .find(|concept| concept.id == id)
360}
361
362fn structural_name(expr: &GrammarExpr) -> String {
363 match expr {
364 GrammarExpr::Empty => "empty_expression".to_string(),
365 GrammarExpr::Terminal(_) | GrammarExpr::TerminalInsensitive(_) => "literal".to_string(),
366 GrammarExpr::CharRange(_, _) => "character_range".to_string(),
367 GrammarExpr::CharClass { .. } => "character_class".to_string(),
368 GrammarExpr::AnyChar => "any_character".to_string(),
369 GrammarExpr::NonTerminal(name) => sanitize_identifier(name),
370 GrammarExpr::Choice {
371 ordered,
372 alternatives,
373 } => {
374 let prefix = if *ordered {
375 "ordered_choice"
376 } else {
377 "unordered_choice"
378 };
379 format!("{prefix}_{}", alternatives.len())
380 }
381 GrammarExpr::Sequence(items) => format!("sequence_{}", items.len()),
382 GrammarExpr::Optional(inner) => format!("optional_{}", structural_stem(inner)),
383 GrammarExpr::ZeroOrMore(inner) => {
384 format!("zero_or_more_{}", structural_stem(inner))
385 }
386 GrammarExpr::OneOrMore(inner) => format!("one_or_more_{}", structural_stem(inner)),
387 GrammarExpr::Repeat { expr, .. } => format!("repeated_{}", structural_stem(expr)),
388 GrammarExpr::And(inner) => format!("{}_positive_predicate", structural_stem(inner)),
389 GrammarExpr::Not(inner) => format!("{}_negative_predicate", structural_stem(inner)),
390 GrammarExpr::Capture { label, expr } => label.as_deref().map_or_else(
391 || format!("{}_capture", structural_stem(expr)),
392 sanitize_identifier,
393 ),
394 GrammarExpr::Feature(feature) => sanitize_identifier(feature.head()),
395 }
396}
397
398fn structural_stem(expr: &GrammarExpr) -> String {
399 concept_from_expr(expr).map_or_else(
400 || match expr {
401 GrammarExpr::NonTerminal(name) => sanitize_identifier(name),
402 _ => sanitize_identifier(
403 grammar_expr_concept_id(expr)
404 .rsplit('.')
405 .next()
406 .unwrap_or("rule"),
407 ),
408 },
409 |concept| concept.name.to_string(),
410 )
411}
412
413fn is_number_expr(expr: &GrammarExpr) -> bool {
414 match expr {
415 GrammarExpr::OneOrMore(inner) => is_digit_expr(inner),
416 GrammarExpr::Repeat { expr, min, max } => *min >= 1 && max.is_none() && is_digit_expr(expr),
417 GrammarExpr::Sequence(items) if items.len() == 2 => {
418 is_optional_sign(&items[0]) && is_number_expr(&items[1])
419 }
420 _ => false,
421 }
422}
423
424fn is_optional_sign(expr: &GrammarExpr) -> bool {
425 match expr {
426 GrammarExpr::Optional(inner) => matches!(
427 inner.as_ref(),
428 GrammarExpr::Terminal(value) if value == "-" || value == "+"
429 ),
430 _ => false,
431 }
432}
433
434fn is_digit_expr(expr: &GrammarExpr) -> bool {
435 match expr {
436 GrammarExpr::CharRange('0', '9') => true,
437 GrammarExpr::Terminal(value) => is_digit_text(value),
438 GrammarExpr::CharClass { negated, items } => {
439 !*negated
440 && items.iter().all(|item| {
441 matches!(
442 item,
443 CharClassItem::Range('0', '9') | CharClassItem::Char('0'..='9')
444 )
445 })
446 }
447 _ => false,
448 }
449}
450
451fn is_letter_expr(expr: &GrammarExpr) -> bool {
452 match expr {
453 GrammarExpr::CharRange('a', 'z') | GrammarExpr::CharRange('A', 'Z') => true,
454 GrammarExpr::Terminal(value) => is_letter_text(value),
455 GrammarExpr::CharClass { negated, items } => {
456 !*negated
457 && items.iter().all(|item| {
458 matches!(
459 item,
460 CharClassItem::Range('a', 'z')
461 | CharClassItem::Range('A', 'Z')
462 | CharClassItem::Char('a'..='z' | 'A'..='Z' | '_')
463 )
464 })
465 }
466 _ => false,
467 }
468}
469
470fn is_identifier_expr(expr: &GrammarExpr) -> bool {
471 match expr {
472 GrammarExpr::Sequence(items) if items.len() == 2 => {
473 is_letter_expr(&items[0])
474 && matches!(
475 &items[1],
476 GrammarExpr::ZeroOrMore(inner) if is_identifier_tail_expr(inner)
477 )
478 }
479 _ => false,
480 }
481}
482
483fn is_identifier_tail_expr(expr: &GrammarExpr) -> bool {
484 is_letter_expr(expr)
485 || is_digit_expr(expr)
486 || matches!(
487 expr,
488 GrammarExpr::Choice { alternatives, .. }
489 if alternatives
490 .iter()
491 .all(|alternative| is_letter_expr(alternative) || is_digit_expr(alternative))
492 )
493}
494
495fn is_integer_text(text: &str) -> bool {
496 let digits = text
497 .strip_prefix(['-', '+'])
498 .filter(|rest| !rest.is_empty())
499 .unwrap_or(text);
500 digits.chars().all(|character| character.is_ascii_digit())
501}
502
503fn is_digit_text(text: &str) -> bool {
504 let mut chars = text.chars();
505 chars
506 .next()
507 .is_some_and(|character| character.is_ascii_digit())
508 && chars.next().is_none()
509}
510
511fn is_letter_text(text: &str) -> bool {
512 let mut chars = text.chars();
513 chars
514 .next()
515 .is_some_and(|character| character.is_ascii_alphabetic())
516 && chars.next().is_none()
517}
518
519fn is_identifier_text(text: &str) -> bool {
520 let mut chars = text.chars();
521 let Some(first) = chars.next() else {
522 return false;
523 };
524 (first.is_ascii_alphabetic() || first == '_')
525 && chars.all(|character| character.is_ascii_alphanumeric() || character == '_')
526}
527
528fn sanitize_identifier(value: &str) -> String {
529 let mut output = String::new();
530 for character in value.chars() {
531 let normalized = if character.is_ascii_alphanumeric() || character == '_' {
532 character.to_ascii_lowercase()
533 } else {
534 '_'
535 };
536
537 if output.is_empty() && normalized.is_ascii_digit() {
538 output.push('_');
539 }
540 if normalized == '_' && output.ends_with('_') {
541 continue;
542 }
543 output.push(normalized);
544 }
545
546 let trimmed = output.trim_matches('_');
547 if trimmed.is_empty() {
548 "rule".to_string()
549 } else if trimmed
550 .chars()
551 .next()
552 .is_some_and(|character| character.is_ascii_digit())
553 {
554 format!("_{trimmed}")
555 } else {
556 trimmed.to_string()
557 }
558}
559
560fn unique_rule_name(base: &str, grammar: &Grammar) -> String {
561 if grammar.rule(base).is_none() {
562 return base.to_string();
563 }
564
565 let mut suffix = 2usize;
566 loop {
567 let candidate = format!("{base}_{suffix}");
568 if grammar.rule(&candidate).is_none() {
569 return candidate;
570 }
571 suffix = suffix.saturating_add(1);
572 }
573}
574
575fn validate_name_candidate(request: &NamingRequest<'_>, candidate: &NameCandidate) -> bool {
576 is_valid_identifier(&candidate.name)
577 && request.grammar.rule(&candidate.name).is_none()
578 && candidate.concept.as_deref().is_none_or(known_concept_id)
579}
580
581fn is_valid_identifier(value: &str) -> bool {
582 let mut chars = value.chars();
583 let Some(first) = chars.next() else {
584 return false;
585 };
586 (first.is_ascii_alphabetic() || first == '_')
587 && chars.all(|character| character.is_ascii_alphanumeric() || character == '_')
588}
589
590fn known_concept_id(concept: &str) -> bool {
591 GRAMMAR_CONCEPTS.iter().any(|known| known.id == concept)
592 || INFERENCE_NAMING_CONCEPTS
593 .iter()
594 .any(|known| known.id == concept)
595}
596
597fn validate_merge_scores(scores: Vec<MergeScore>, expected_len: usize) -> Option<Vec<MergeScore>> {
598 if scores.len() != expected_len || scores.iter().any(|score| !score.score.is_finite()) {
599 return None;
600 }
601
602 Some(
603 scores
604 .into_iter()
605 .map(|score| MergeScore {
606 score: score.score.clamp(0.0, 1.0),
607 source: score.source,
608 })
609 .collect(),
610 )
611}
612
613fn merge_candidate_grammar(grammar: &Grammar, candidate: &MergeCandidate) -> Option<Grammar> {
614 if candidate.winner == candidate.loser {
615 return Some(grammar.clone());
616 }
617
618 let winner_rule = grammar.rule(&candidate.winner)?;
619 let loser_rule = grammar.rule(&candidate.loser)?;
620 if grammar.start() == Some(loser_rule.name())
621 || !merge_metadata_compatible(winner_rule, loser_rule)
622 {
623 return None;
624 }
625
626 let winner_name = winner_rule.name().to_string();
627 let loser_name = loser_rule.name().to_string();
628 let replacement = GrammarExpr::non_terminal(&winner_name);
629 let merged_expr = merge_exprs(winner_rule.expr(), loser_rule.expr());
630
631 let mut next = Grammar::new();
632 if let Some(source_format) = grammar.source_format() {
633 next.set_source_format(source_format);
634 }
635
636 for rule in grammar.rules() {
637 if rule.name() == loser_name {
638 continue;
639 }
640
641 let mut next_rule = rule.clone();
642 if next_rule.name() == winner_name {
643 next_rule.expr = merged_expr.clone();
644 }
645 next_rule.expr = rewrite_nonterminal_refs(&next_rule.expr, &loser_name, &replacement);
646 next.add_rule(next_rule);
647 }
648
649 if let Some(start) = grammar.start() {
650 next.set_start(start);
651 }
652 Some(next)
653}
654
655fn merge_metadata_compatible(winner: &GrammarRule, loser: &GrammarRule) -> bool {
656 winner.kind() == loser.kind()
657 && winner.concept() == loser.concept()
658 && winner.doc() == loser.doc()
659 && winner.kind() == RuleKind::Normal
660}
661
662fn merge_exprs(winner: &GrammarExpr, loser: &GrammarExpr) -> GrammarExpr {
663 if winner == loser {
664 return winner.clone();
665 }
666
667 let mut alternatives = Vec::new();
668 push_merge_alternative(&mut alternatives, winner);
669 push_merge_alternative(&mut alternatives, loser);
670
671 if alternatives.len() == 1 {
672 alternatives.remove(0)
673 } else {
674 GrammarExpr::choice(false, alternatives)
675 }
676}
677
678fn push_merge_alternative(alternatives: &mut Vec<GrammarExpr>, expr: &GrammarExpr) {
679 if let GrammarExpr::Choice {
680 ordered: false,
681 alternatives: nested,
682 } = expr
683 {
684 for alternative in nested {
685 push_merge_alternative(alternatives, alternative);
686 }
687 return;
688 }
689
690 if !alternatives.contains(expr) {
691 alternatives.push(expr.clone());
692 }
693}
694
695fn rewrite_nonterminal_refs(
696 expr: &GrammarExpr,
697 loser_name: &str,
698 replacement: &GrammarExpr,
699) -> GrammarExpr {
700 match expr {
701 GrammarExpr::NonTerminal(name) if name == loser_name => replacement.clone(),
702 GrammarExpr::Choice {
703 ordered,
704 alternatives,
705 } => GrammarExpr::choice(
706 *ordered,
707 alternatives
708 .iter()
709 .map(|expr| rewrite_nonterminal_refs(expr, loser_name, replacement)),
710 ),
711 GrammarExpr::Sequence(items) => GrammarExpr::sequence(
712 items
713 .iter()
714 .map(|expr| rewrite_nonterminal_refs(expr, loser_name, replacement)),
715 ),
716 GrammarExpr::Optional(inner) => {
717 GrammarExpr::optional(rewrite_nonterminal_refs(inner, loser_name, replacement))
718 }
719 GrammarExpr::ZeroOrMore(inner) => {
720 GrammarExpr::zero_or_more(rewrite_nonterminal_refs(inner, loser_name, replacement))
721 }
722 GrammarExpr::OneOrMore(inner) => {
723 GrammarExpr::one_or_more(rewrite_nonterminal_refs(inner, loser_name, replacement))
724 }
725 GrammarExpr::Repeat { expr, min, max } => GrammarExpr::repeat(
726 rewrite_nonterminal_refs(expr, loser_name, replacement),
727 *min,
728 *max,
729 ),
730 GrammarExpr::And(inner) => {
731 GrammarExpr::and(rewrite_nonterminal_refs(inner, loser_name, replacement))
732 }
733 GrammarExpr::Not(inner) => {
734 GrammarExpr::not(rewrite_nonterminal_refs(inner, loser_name, replacement))
735 }
736 GrammarExpr::Capture { label, expr } => label.as_deref().map_or_else(
737 || {
738 GrammarExpr::capture_unlabeled(rewrite_nonterminal_refs(
739 expr,
740 loser_name,
741 replacement,
742 ))
743 },
744 |label| {
745 GrammarExpr::capture(
746 label,
747 rewrite_nonterminal_refs(expr, loser_name, replacement),
748 )
749 },
750 ),
751 GrammarExpr::Feature(feature) => GrammarExpr::rewrite_feature(feature, |inner| {
752 rewrite_nonterminal_refs(inner, loser_name, replacement)
753 }),
754 GrammarExpr::Empty
755 | GrammarExpr::Terminal(_)
756 | GrammarExpr::TerminalInsensitive(_)
757 | GrammarExpr::CharRange(_, _)
758 | GrammarExpr::CharClass { .. }
759 | GrammarExpr::AnyChar
760 | GrammarExpr::NonTerminal(_) => expr.clone(),
761 }
762}
763
764fn score_from_delta(delta: f64) -> f64 {
765 if !delta.is_finite() {
766 return 0.0;
767 }
768
769 let magnitude = delta.abs() / (1.0 + delta.abs());
770 if delta < -COST_EPSILON {
771 f64::midpoint(1.0, magnitude)
772 } else if delta > COST_EPSILON {
773 f64::midpoint(1.0, -magnitude)
774 } else {
775 0.5
776 }
777}