1use crate::grammar::{
8 grammar_expr_concept_id, CharClassItem, Grammar, GrammarExpr, GrammarRule, RuleKind,
9 GRAMMAR_CONCEPTS,
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",
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".to_string(),
365 GrammarExpr::Terminal(_) | GrammarExpr::TerminalInsensitive(_) => "literal".to_string(),
366 GrammarExpr::CharRange(_, _) => "char_range".to_string(),
367 GrammarExpr::CharClass { .. } => "char_class".to_string(),
368 GrammarExpr::AnyChar => "any_char".to_string(),
369 GrammarExpr::NonTerminal(name) => sanitize_identifier(name),
370 GrammarExpr::Choice {
371 ordered,
372 alternatives,
373 } => {
374 let prefix = if *ordered { "ordered_choice" } else { "choice" };
375 format!("{prefix}_{}", alternatives.len())
376 }
377 GrammarExpr::Sequence(items) => format!("seq_{}", items.len()),
378 GrammarExpr::Optional(inner) => format!("{}_opt", structural_stem(inner)),
379 GrammarExpr::ZeroOrMore(inner) => format!("{}_star", structural_stem(inner)),
380 GrammarExpr::OneOrMore(inner) => format!("{}_plus", structural_stem(inner)),
381 GrammarExpr::Repeat { expr, .. } => format!("{}_repeat", structural_stem(expr)),
382 GrammarExpr::And(inner) => format!("{}_and", structural_stem(inner)),
383 GrammarExpr::Not(inner) => format!("{}_not", structural_stem(inner)),
384 GrammarExpr::Capture { label, expr } => label.as_deref().map_or_else(
385 || format!("{}_capture", structural_stem(expr)),
386 sanitize_identifier,
387 ),
388 }
389}
390
391fn structural_stem(expr: &GrammarExpr) -> String {
392 concept_from_expr(expr).map_or_else(
393 || match expr {
394 GrammarExpr::NonTerminal(name) => sanitize_identifier(name),
395 _ => sanitize_identifier(
396 grammar_expr_concept_id(expr)
397 .rsplit('.')
398 .next()
399 .unwrap_or("rule"),
400 ),
401 },
402 |concept| concept.name.to_string(),
403 )
404}
405
406fn is_number_expr(expr: &GrammarExpr) -> bool {
407 match expr {
408 GrammarExpr::OneOrMore(inner) => is_digit_expr(inner),
409 GrammarExpr::Repeat { expr, min, max } => *min >= 1 && max.is_none() && is_digit_expr(expr),
410 GrammarExpr::Sequence(items) if items.len() == 2 => {
411 is_optional_sign(&items[0]) && is_number_expr(&items[1])
412 }
413 _ => false,
414 }
415}
416
417fn is_optional_sign(expr: &GrammarExpr) -> bool {
418 match expr {
419 GrammarExpr::Optional(inner) => matches!(
420 inner.as_ref(),
421 GrammarExpr::Terminal(value) if value == "-" || value == "+"
422 ),
423 _ => false,
424 }
425}
426
427fn is_digit_expr(expr: &GrammarExpr) -> bool {
428 match expr {
429 GrammarExpr::CharRange('0', '9') => true,
430 GrammarExpr::Terminal(value) => is_digit_text(value),
431 GrammarExpr::CharClass { negated, items } => {
432 !*negated
433 && items.iter().all(|item| {
434 matches!(
435 item,
436 CharClassItem::Range('0', '9') | CharClassItem::Char('0'..='9')
437 )
438 })
439 }
440 _ => false,
441 }
442}
443
444fn is_letter_expr(expr: &GrammarExpr) -> bool {
445 match expr {
446 GrammarExpr::CharRange('a', 'z') | GrammarExpr::CharRange('A', 'Z') => true,
447 GrammarExpr::Terminal(value) => is_letter_text(value),
448 GrammarExpr::CharClass { negated, items } => {
449 !*negated
450 && items.iter().all(|item| {
451 matches!(
452 item,
453 CharClassItem::Range('a', 'z')
454 | CharClassItem::Range('A', 'Z')
455 | CharClassItem::Char('a'..='z' | 'A'..='Z' | '_')
456 )
457 })
458 }
459 _ => false,
460 }
461}
462
463fn is_identifier_expr(expr: &GrammarExpr) -> bool {
464 match expr {
465 GrammarExpr::Sequence(items) if items.len() == 2 => {
466 is_letter_expr(&items[0])
467 && matches!(
468 &items[1],
469 GrammarExpr::ZeroOrMore(inner) if is_identifier_tail_expr(inner)
470 )
471 }
472 _ => false,
473 }
474}
475
476fn is_identifier_tail_expr(expr: &GrammarExpr) -> bool {
477 is_letter_expr(expr)
478 || is_digit_expr(expr)
479 || matches!(
480 expr,
481 GrammarExpr::Choice { alternatives, .. }
482 if alternatives
483 .iter()
484 .all(|alternative| is_letter_expr(alternative) || is_digit_expr(alternative))
485 )
486}
487
488fn is_integer_text(text: &str) -> bool {
489 let digits = text
490 .strip_prefix(['-', '+'])
491 .filter(|rest| !rest.is_empty())
492 .unwrap_or(text);
493 digits.chars().all(|character| character.is_ascii_digit())
494}
495
496fn is_digit_text(text: &str) -> bool {
497 let mut chars = text.chars();
498 chars
499 .next()
500 .is_some_and(|character| character.is_ascii_digit())
501 && chars.next().is_none()
502}
503
504fn is_letter_text(text: &str) -> bool {
505 let mut chars = text.chars();
506 chars
507 .next()
508 .is_some_and(|character| character.is_ascii_alphabetic())
509 && chars.next().is_none()
510}
511
512fn is_identifier_text(text: &str) -> bool {
513 let mut chars = text.chars();
514 let Some(first) = chars.next() else {
515 return false;
516 };
517 (first.is_ascii_alphabetic() || first == '_')
518 && chars.all(|character| character.is_ascii_alphanumeric() || character == '_')
519}
520
521fn sanitize_identifier(value: &str) -> String {
522 let mut output = String::new();
523 for character in value.chars() {
524 let normalized = if character.is_ascii_alphanumeric() || character == '_' {
525 character.to_ascii_lowercase()
526 } else {
527 '_'
528 };
529
530 if output.is_empty() && normalized.is_ascii_digit() {
531 output.push('_');
532 }
533 if normalized == '_' && output.ends_with('_') {
534 continue;
535 }
536 output.push(normalized);
537 }
538
539 let trimmed = output.trim_matches('_');
540 if trimmed.is_empty() {
541 "rule".to_string()
542 } else if trimmed
543 .chars()
544 .next()
545 .is_some_and(|character| character.is_ascii_digit())
546 {
547 format!("_{trimmed}")
548 } else {
549 trimmed.to_string()
550 }
551}
552
553fn unique_rule_name(base: &str, grammar: &Grammar) -> String {
554 if grammar.rule(base).is_none() {
555 return base.to_string();
556 }
557
558 let mut suffix = 2usize;
559 loop {
560 let candidate = format!("{base}_{suffix}");
561 if grammar.rule(&candidate).is_none() {
562 return candidate;
563 }
564 suffix = suffix.saturating_add(1);
565 }
566}
567
568fn validate_name_candidate(request: &NamingRequest<'_>, candidate: &NameCandidate) -> bool {
569 is_valid_identifier(&candidate.name)
570 && request.grammar.rule(&candidate.name).is_none()
571 && candidate.concept.as_deref().map_or(true, known_concept_id)
572}
573
574fn is_valid_identifier(value: &str) -> bool {
575 let mut chars = value.chars();
576 let Some(first) = chars.next() else {
577 return false;
578 };
579 (first.is_ascii_alphabetic() || first == '_')
580 && chars.all(|character| character.is_ascii_alphanumeric() || character == '_')
581}
582
583fn known_concept_id(concept: &str) -> bool {
584 GRAMMAR_CONCEPTS.iter().any(|known| known.id == concept)
585 || INFERENCE_NAMING_CONCEPTS
586 .iter()
587 .any(|known| known.id == concept)
588}
589
590fn validate_merge_scores(scores: Vec<MergeScore>, expected_len: usize) -> Option<Vec<MergeScore>> {
591 if scores.len() != expected_len || scores.iter().any(|score| !score.score.is_finite()) {
592 return None;
593 }
594
595 Some(
596 scores
597 .into_iter()
598 .map(|score| MergeScore {
599 score: score.score.clamp(0.0, 1.0),
600 source: score.source,
601 })
602 .collect(),
603 )
604}
605
606fn merge_candidate_grammar(grammar: &Grammar, candidate: &MergeCandidate) -> Option<Grammar> {
607 if candidate.winner == candidate.loser {
608 return Some(grammar.clone());
609 }
610
611 let winner_rule = grammar.rule(&candidate.winner)?;
612 let loser_rule = grammar.rule(&candidate.loser)?;
613 if grammar.start() == Some(loser_rule.name())
614 || !merge_metadata_compatible(winner_rule, loser_rule)
615 {
616 return None;
617 }
618
619 let winner_name = winner_rule.name().to_string();
620 let loser_name = loser_rule.name().to_string();
621 let replacement = GrammarExpr::non_terminal(&winner_name);
622 let merged_expr = merge_exprs(winner_rule.expr(), loser_rule.expr());
623
624 let mut next = Grammar::new();
625 if let Some(source_format) = grammar.source_format() {
626 next.set_source_format(source_format);
627 }
628
629 for rule in grammar.rules() {
630 if rule.name() == loser_name {
631 continue;
632 }
633
634 let mut next_rule = rule.clone();
635 if next_rule.name() == winner_name {
636 next_rule.expr = merged_expr.clone();
637 }
638 next_rule.expr = rewrite_nonterminal_refs(&next_rule.expr, &loser_name, &replacement);
639 next.add_rule(next_rule);
640 }
641
642 if let Some(start) = grammar.start() {
643 next.set_start(start);
644 }
645 Some(next)
646}
647
648fn merge_metadata_compatible(winner: &GrammarRule, loser: &GrammarRule) -> bool {
649 winner.kind() == loser.kind()
650 && winner.concept() == loser.concept()
651 && winner.doc() == loser.doc()
652 && winner.kind() == RuleKind::Normal
653}
654
655fn merge_exprs(winner: &GrammarExpr, loser: &GrammarExpr) -> GrammarExpr {
656 if winner == loser {
657 return winner.clone();
658 }
659
660 let mut alternatives = Vec::new();
661 push_merge_alternative(&mut alternatives, winner);
662 push_merge_alternative(&mut alternatives, loser);
663
664 if alternatives.len() == 1 {
665 alternatives.remove(0)
666 } else {
667 GrammarExpr::choice(false, alternatives)
668 }
669}
670
671fn push_merge_alternative(alternatives: &mut Vec<GrammarExpr>, expr: &GrammarExpr) {
672 if let GrammarExpr::Choice {
673 ordered: false,
674 alternatives: nested,
675 } = expr
676 {
677 for alternative in nested {
678 push_merge_alternative(alternatives, alternative);
679 }
680 return;
681 }
682
683 if !alternatives.contains(expr) {
684 alternatives.push(expr.clone());
685 }
686}
687
688fn rewrite_nonterminal_refs(
689 expr: &GrammarExpr,
690 loser_name: &str,
691 replacement: &GrammarExpr,
692) -> GrammarExpr {
693 match expr {
694 GrammarExpr::NonTerminal(name) if name == loser_name => replacement.clone(),
695 GrammarExpr::Choice {
696 ordered,
697 alternatives,
698 } => GrammarExpr::choice(
699 *ordered,
700 alternatives
701 .iter()
702 .map(|expr| rewrite_nonterminal_refs(expr, loser_name, replacement)),
703 ),
704 GrammarExpr::Sequence(items) => GrammarExpr::sequence(
705 items
706 .iter()
707 .map(|expr| rewrite_nonterminal_refs(expr, loser_name, replacement)),
708 ),
709 GrammarExpr::Optional(inner) => {
710 GrammarExpr::optional(rewrite_nonterminal_refs(inner, loser_name, replacement))
711 }
712 GrammarExpr::ZeroOrMore(inner) => {
713 GrammarExpr::zero_or_more(rewrite_nonterminal_refs(inner, loser_name, replacement))
714 }
715 GrammarExpr::OneOrMore(inner) => {
716 GrammarExpr::one_or_more(rewrite_nonterminal_refs(inner, loser_name, replacement))
717 }
718 GrammarExpr::Repeat { expr, min, max } => GrammarExpr::repeat(
719 rewrite_nonterminal_refs(expr, loser_name, replacement),
720 *min,
721 *max,
722 ),
723 GrammarExpr::And(inner) => {
724 GrammarExpr::and(rewrite_nonterminal_refs(inner, loser_name, replacement))
725 }
726 GrammarExpr::Not(inner) => {
727 GrammarExpr::not(rewrite_nonterminal_refs(inner, loser_name, replacement))
728 }
729 GrammarExpr::Capture { label, expr } => label.as_deref().map_or_else(
730 || {
731 GrammarExpr::capture_unlabeled(rewrite_nonterminal_refs(
732 expr,
733 loser_name,
734 replacement,
735 ))
736 },
737 |label| {
738 GrammarExpr::capture(
739 label,
740 rewrite_nonterminal_refs(expr, loser_name, replacement),
741 )
742 },
743 ),
744 GrammarExpr::Empty
745 | GrammarExpr::Terminal(_)
746 | GrammarExpr::TerminalInsensitive(_)
747 | GrammarExpr::CharRange(_, _)
748 | GrammarExpr::CharClass { .. }
749 | GrammarExpr::AnyChar
750 | GrammarExpr::NonTerminal(_) => expr.clone(),
751 }
752}
753
754fn score_from_delta(delta: f64) -> f64 {
755 if !delta.is_finite() {
756 return 0.0;
757 }
758
759 let magnitude = delta.abs() / (1.0 + delta.abs());
760 if delta < -COST_EPSILON {
761 0.5 + magnitude * 0.5
762 } else if delta > COST_EPSILON {
763 0.5 - magnitude * 0.5
764 } else {
765 0.5
766 }
767}