Skip to main content

meta_language/grammar/
translate.rs

1//! Concept-aligned translation of a grammar's human-facing surface.
2
3use std::cmp::Reverse;
4use std::collections::{BTreeMap, BTreeSet};
5use std::error::Error;
6use std::fmt;
7
8use super::{FeatureExpr, GRAMMAR_CONCEPTS, Grammar, GrammarExpr, GrammarRule};
9use crate::{
10    LinkMetadata, LinkNetwork, LinkQuery, LinkType, ParseConfiguration, TranslationRule,
11    TranslationRuleSet, TranslationTemplate, current_concept_id,
12};
13
14/// Error raised while translating grammar rule names and documentation.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum GrammarTranslateError {
17    /// A rule references a concept absent from the provided rule set.
18    UnknownConcept {
19        /// Rule whose explicit concept could not be translated.
20        rule: String,
21        /// Missing concept id.
22        concept: String,
23    },
24    /// Two distinct source rules translate to the same target name.
25    NameCollision {
26        /// Target language requested by the caller.
27        language: String,
28        /// Colliding translated rule name.
29        name: String,
30    },
31}
32
33impl fmt::Display for GrammarTranslateError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::UnknownConcept { rule, concept } => {
37                write!(
38                    formatter,
39                    "rule `{rule}` references unknown grammar concept `{concept}`"
40                )
41            }
42            Self::NameCollision { language, name } => write!(
43                formatter,
44                "translating grammar surface to `{language}` produced duplicate rule name `{name}`"
45            ),
46        }
47    }
48}
49
50impl Error for GrammarTranslateError {}
51
52/// Translates a grammar's rule names and documentation into `target_language`.
53///
54/// The grammar algebra is preserved: expressions are cloned with only
55/// `NonTerminal` references rewritten to match renamed rules. Rules without an
56/// explicit concept and without a known source-language surface are left
57/// unchanged.
58///
59/// # Errors
60///
61/// Returns [`GrammarTranslateError::UnknownConcept`] when an explicitly aligned
62/// rule concept cannot be rendered by `rules`, or
63/// [`GrammarTranslateError::NameCollision`] when distinct rules would share one
64/// translated name.
65pub fn translate_grammar_surface(
66    grammar: &Grammar,
67    target_language: &str,
68    rules: &TranslationRuleSet,
69) -> Result<Grammar, GrammarTranslateError> {
70    let rename_map = translated_rule_names(grammar, target_language, rules)?;
71    let mut translated = Grammar::new();
72
73    if let Some(source_format) = grammar.source_format() {
74        translated.set_source_format(source_format);
75    }
76    if let Some(start) = grammar.start() {
77        translated.set_start(renamed_name(start, &rename_map));
78    }
79
80    for rule in grammar.rules() {
81        let mut translated_rule = GrammarRule::new(
82            renamed_name(rule.name(), &rename_map),
83            rename_expr(rule.expr(), &rename_map),
84        )
85        .with_kind(rule.kind());
86
87        if let Some(concept) = rule.concept() {
88            translated_rule = translated_rule.with_concept(concept);
89        }
90        if let Some(doc) = rule.doc() {
91            translated_rule =
92                translated_rule.with_doc(translate_doc_comment(doc, target_language, rules));
93        }
94
95        translated.add_rule(translated_rule);
96    }
97
98    Ok(translated)
99}
100
101/// Builds a default rule set for grammar-construct and common grammar-surface
102/// concepts.
103#[must_use]
104pub fn grammar_concept_translation_rules() -> TranslationRuleSet {
105    let grammar_concepts = GRAMMAR_CONCEPTS
106        .iter()
107        .map(|concept| concept.id)
108        .collect::<BTreeSet<_>>();
109    let mut rules = TranslationRuleSet::new("grammar-concepts");
110
111    for surface in GRAMMAR_SURFACE_TRANSLATIONS {
112        debug_assert!(
113            grammar_concepts.contains(surface.concept)
114                || surface.concept.starts_with("grammar.")
115                || surface.concept.starts_with("grammar::concept::"),
116            "grammar translation concept should use a grammar concept namespace"
117        );
118        rules.add_rule(
119            TranslationRule::new(
120                surface.concept,
121                LinkQuery::by_type(LinkType::Concept).with_term(surface.concept),
122            )
123            .with_template("English", surface.english)
124            .with_template("en", surface.english)
125            .with_template("Russian", surface.russian)
126            .with_template("ru", surface.russian),
127        );
128    }
129
130    rules
131}
132
133fn translated_rule_names(
134    grammar: &Grammar,
135    target_language: &str,
136    rules: &TranslationRuleSet,
137) -> Result<BTreeMap<String, String>, GrammarTranslateError> {
138    let mut rename_map = BTreeMap::new();
139    let mut used_names = BTreeMap::<String, String>::new();
140
141    for rule in grammar.rules() {
142        let resolved = resolve_rule_concept(rule, rules);
143        let translated_name = match resolved {
144            Some(ResolvedConcept {
145                concept,
146                explicit: true,
147            }) => translate_concept_surface(&concept, target_language, rules).ok_or_else(|| {
148                GrammarTranslateError::UnknownConcept {
149                    rule: rule.name().to_string(),
150                    concept,
151                }
152            })?,
153            Some(ResolvedConcept {
154                concept,
155                explicit: false,
156            }) => translate_concept_surface(&concept, target_language, rules)
157                .unwrap_or_else(|| rule.name().to_string()),
158            None => rule.name().to_string(),
159        };
160
161        if let Some(previous_rule) = used_names.insert(translated_name.clone(), rule.name().into())
162            && previous_rule != rule.name()
163        {
164            return Err(GrammarTranslateError::NameCollision {
165                language: target_language.to_string(),
166                name: translated_name,
167            });
168        }
169        rename_map.insert(rule.name().to_string(), translated_name);
170    }
171
172    Ok(rename_map)
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
176struct ResolvedConcept {
177    concept: String,
178    explicit: bool,
179}
180
181fn resolve_rule_concept(rule: &GrammarRule, rules: &TranslationRuleSet) -> Option<ResolvedConcept> {
182    rule.concept()
183        .map(|concept| ResolvedConcept {
184            concept: current_concept_id(concept).to_string(),
185            explicit: true,
186        })
187        .or_else(|| infer_concept_from_surface(rule.name(), rules))
188}
189
190fn infer_concept_from_surface(name: &str, rules: &TranslationRuleSet) -> Option<ResolvedConcept> {
191    for rule in rules.rules() {
192        let Some(concept) = concept_id_for_translation_rule(rule) else {
193            continue;
194        };
195        if concept == name
196            || rule
197                .templates()
198                .values()
199                .any(|template| template.source() == name)
200        {
201            return Some(ResolvedConcept {
202                concept: concept.to_string(),
203                explicit: false,
204            });
205        }
206    }
207
208    None
209}
210
211fn concept_id_for_translation_rule(rule: &TranslationRule) -> Option<&str> {
212    rule.query()
213        .term_filter()
214        .or_else(|| concept_like_rule_name(rule.name()))
215}
216
217fn concept_like_rule_name(name: &str) -> Option<&str> {
218    (name.starts_with("grammar.") || name.starts_with("grammar::concept::")).then_some(name)
219}
220
221fn translate_concept_surface(
222    concept: &str,
223    target_language: &str,
224    rules: &TranslationRuleSet,
225) -> Option<String> {
226    render_concept_with_rules(concept, target_language, rules).or_else(|| {
227        rules
228            .rules()
229            .iter()
230            .find(|rule| concept_id_for_translation_rule(rule) == Some(concept))
231            .and_then(|rule| template_for_language(rule.templates(), target_language))
232            .map(ToString::to_string)
233    })
234}
235
236fn render_concept_with_rules(
237    concept: &str,
238    target_language: &str,
239    rules: &TranslationRuleSet,
240) -> Option<String> {
241    let mut network = LinkNetwork::new();
242    let concept_link = network.intern_concept(concept, None);
243    network.insert_link(
244        [concept_link],
245        LinkMetadata::new()
246            .with_link_type(LinkType::Semantic)
247            .with_named(true)
248            .with_term(concept),
249    );
250
251    for language in language_lookup_order(target_language) {
252        let rendered =
253            network.reconstruct_text_as_with_rules(&language, ParseConfiguration::default(), rules);
254        if !rendered.is_empty() {
255            return Some(rendered);
256        }
257    }
258
259    None
260}
261
262fn template_for_language<'a>(
263    templates: &'a BTreeMap<String, TranslationTemplate>,
264    language: &str,
265) -> Option<&'a str> {
266    language_lookup_order(language)
267        .into_iter()
268        .find_map(|candidate| templates.get(&candidate).map(TranslationTemplate::source))
269}
270
271fn language_lookup_order(language: &str) -> Vec<String> {
272    match canonical_language(language) {
273        Some(("English", "en")) => vec!["English".to_string(), "en".to_string()],
274        Some(("Russian", "ru")) => vec!["Russian".to_string(), "ru".to_string()],
275        _ => vec![language.to_string()],
276    }
277}
278
279fn canonical_language(language: &str) -> Option<(&'static str, &'static str)> {
280    match language.to_ascii_lowercase().as_str() {
281        "english" | "en" => Some(("English", "en")),
282        "russian" | "ru" => Some(("Russian", "ru")),
283        _ => None,
284    }
285}
286
287fn renamed_name(name: &str, rename_map: &BTreeMap<String, String>) -> String {
288    rename_map
289        .get(name)
290        .cloned()
291        .unwrap_or_else(|| name.to_string())
292}
293
294fn rename_expr(expr: &GrammarExpr, rename_map: &BTreeMap<String, String>) -> GrammarExpr {
295    match expr {
296        GrammarExpr::NonTerminal(name) => GrammarExpr::NonTerminal(renamed_name(name, rename_map)),
297        GrammarExpr::Choice {
298            ordered,
299            alternatives,
300        } => GrammarExpr::Choice {
301            ordered: *ordered,
302            alternatives: alternatives
303                .iter()
304                .map(|expr| rename_expr(expr, rename_map))
305                .collect(),
306        },
307        GrammarExpr::Sequence(items) => GrammarExpr::Sequence(
308            items
309                .iter()
310                .map(|expr| rename_expr(expr, rename_map))
311                .collect(),
312        ),
313        GrammarExpr::Optional(expr) => {
314            GrammarExpr::Optional(Box::new(rename_expr(expr, rename_map)))
315        }
316        GrammarExpr::ZeroOrMore(expr) => {
317            GrammarExpr::ZeroOrMore(Box::new(rename_expr(expr, rename_map)))
318        }
319        GrammarExpr::OneOrMore(expr) => {
320            GrammarExpr::OneOrMore(Box::new(rename_expr(expr, rename_map)))
321        }
322        GrammarExpr::Repeat { expr, min, max } => GrammarExpr::Repeat {
323            expr: Box::new(rename_expr(expr, rename_map)),
324            min: *min,
325            max: *max,
326        },
327        GrammarExpr::And(expr) => GrammarExpr::And(Box::new(rename_expr(expr, rename_map))),
328        GrammarExpr::Not(expr) => GrammarExpr::Not(Box::new(rename_expr(expr, rename_map))),
329        GrammarExpr::Capture { label, expr } => GrammarExpr::Capture {
330            label: label.clone(),
331            expr: Box::new(rename_expr(expr, rename_map)),
332        },
333        GrammarExpr::Feature(feature) => {
334            let mut copy =
335                GrammarExpr::rewrite_feature(feature, |inner| rename_expr(inner, rename_map));
336            if let GrammarExpr::Feature(renamed) = &mut copy
337                && let FeatureExpr::Call { name, .. } = renamed.as_mut()
338            {
339                *name = renamed_name(name, rename_map);
340            }
341            copy
342        }
343        GrammarExpr::Empty
344        | GrammarExpr::Terminal(_)
345        | GrammarExpr::TerminalInsensitive(_)
346        | GrammarExpr::CharRange(_, _)
347        | GrammarExpr::CharClass { .. }
348        | GrammarExpr::AnyChar => expr.clone(),
349    }
350}
351
352fn translate_doc_comment(doc: &str, target_language: &str, rules: &TranslationRuleSet) -> String {
353    for source_language in doc_source_languages(target_language) {
354        let network = LinkNetwork::parse(doc, source_language, ParseConfiguration::default());
355        let rendered = network.reconstruct_text_as_with_rules(
356            target_language,
357            ParseConfiguration::default(),
358            rules,
359        );
360        if rendered != doc {
361            return rendered;
362        }
363    }
364
365    let replaced = replace_known_concept_surfaces(doc, target_language, rules);
366    if replaced == doc {
367        doc.to_string()
368    } else {
369        replaced
370    }
371}
372
373fn doc_source_languages(target_language: &str) -> Vec<&'static str> {
374    match canonical_language(target_language) {
375        Some(("Russian", _)) => vec!["English"],
376        Some(("English", _)) => vec!["Russian"],
377        _ => vec!["English", "Russian"],
378    }
379}
380
381fn replace_known_concept_surfaces(
382    text: &str,
383    target_language: &str,
384    rules: &TranslationRuleSet,
385) -> String {
386    let mut replacements = rules
387        .rules()
388        .iter()
389        .filter_map(|rule| {
390            template_for_language(rule.templates(), target_language).map(|target| (rule, target))
391        })
392        .flat_map(|(rule, target)| {
393            rule.templates()
394                .values()
395                .filter_map(move |template| replacement_pair(template.source(), target))
396        })
397        .collect::<Vec<_>>();
398
399    replacements.sort_by_key(|replacement| Reverse(replacement.source.len()));
400    replacements.dedup();
401
402    replacements
403        .into_iter()
404        .fold(text.to_string(), |current, pair| {
405            replace_surface(&current, &pair.source, &pair.target)
406        })
407}
408
409fn replacement_pair(source: &str, target: &str) -> Option<ReplacementPair> {
410    (!source.is_empty() && source != target && !source.contains('{') && !target.contains('{')).then(
411        || ReplacementPair {
412            source: source.to_string(),
413            target: target.to_string(),
414        },
415    )
416}
417
418#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
419struct ReplacementPair {
420    source: String,
421    target: String,
422}
423
424fn replace_surface(text: &str, source: &str, target: &str) -> String {
425    let mut output = String::with_capacity(text.len());
426    let mut index = 0;
427
428    while index < text.len() {
429        if text[index..].starts_with(source)
430            && text.is_char_boundary(index + source.len())
431            && has_surface_boundaries(text, source, index, index + source.len())
432        {
433            output.push_str(target);
434            index += source.len();
435            continue;
436        }
437
438        let character = text[index..]
439            .chars()
440            .next()
441            .expect("index should be inside the string");
442        output.push(character);
443        index += character.len_utf8();
444    }
445
446    output
447}
448
449fn has_surface_boundaries(text: &str, source: &str, start: usize, end: usize) -> bool {
450    let first = source.chars().next();
451    let last = source.chars().next_back();
452    let before = text[..start].chars().next_back();
453    let after = text[end..].chars().next();
454
455    let start_ok = first.is_none_or(|character| !is_word_character(character))
456        || !before.is_some_and(is_word_character);
457    let end_ok = last.is_none_or(|character| !is_word_character(character))
458        || !after.is_some_and(is_word_character);
459
460    start_ok && end_ok
461}
462
463fn is_word_character(character: char) -> bool {
464    character.is_alphanumeric() || character == '_'
465}
466
467#[derive(Clone, Copy, Debug, PartialEq, Eq)]
468struct SurfaceTranslation {
469    concept: &'static str,
470    english: &'static str,
471    russian: &'static str,
472}
473
474const GRAMMAR_SURFACE_TRANSLATIONS: &[SurfaceTranslation] = &[
475    SurfaceTranslation {
476        concept: "grammar.rule",
477        english: "rule",
478        russian: "правило",
479    },
480    SurfaceTranslation {
481        concept: "grammar.sequence",
482        english: "sequence",
483        russian: "последовательность",
484    },
485    SurfaceTranslation {
486        concept: "grammar.ordered-choice",
487        english: "ordered choice",
488        russian: "упорядоченный выбор",
489    },
490    SurfaceTranslation {
491        concept: "grammar.unordered-choice",
492        english: "choice",
493        russian: "выбор",
494    },
495    SurfaceTranslation {
496        concept: "grammar.counted-repetition",
497        english: "repetition",
498        russian: "повторение",
499    },
500    SurfaceTranslation {
501        concept: "grammar.zero-or-more-repetition",
502        english: "zero or more",
503        russian: "ноль или более",
504    },
505    SurfaceTranslation {
506        concept: "grammar.one-or-more-repetition",
507        english: "one or more",
508        russian: "один или более",
509    },
510    SurfaceTranslation {
511        concept: "grammar.optional-expression",
512        english: "optional",
513        russian: "необязательный",
514    },
515    SurfaceTranslation {
516        concept: "grammar.terminal",
517        english: "terminal",
518        russian: "терминал",
519    },
520    SurfaceTranslation {
521        concept: "grammar.nonterminal",
522        english: "non-terminal",
523        russian: "нетерминал",
524    },
525    SurfaceTranslation {
526        concept: "grammar.character-class",
527        english: "character class",
528        russian: "класс символов",
529    },
530    SurfaceTranslation {
531        concept: "grammar.character-range",
532        english: "character range",
533        russian: "диапазон символов",
534    },
535    SurfaceTranslation {
536        concept: "grammar.any-character",
537        english: "any character",
538        russian: "любой символ",
539    },
540    SurfaceTranslation {
541        concept: "grammar.positive-predicate",
542        english: "positive predicate",
543        russian: "положительный предикат",
544    },
545    SurfaceTranslation {
546        concept: "grammar.negative-predicate",
547        english: "negative predicate",
548        russian: "отрицательный предикат",
549    },
550    SurfaceTranslation {
551        concept: "grammar.capture",
552        english: "capture",
553        russian: "захват",
554    },
555    SurfaceTranslation {
556        concept: "grammar.empty-expression",
557        english: "empty",
558        russian: "пусто",
559    },
560    SurfaceTranslation {
561        concept: "grammar.expression",
562        english: "expression",
563        russian: "выражение",
564    },
565    SurfaceTranslation {
566        concept: "grammar.term",
567        english: "term",
568        russian: "слагаемое",
569    },
570    SurfaceTranslation {
571        concept: "grammar.factor",
572        english: "factor",
573        russian: "множитель",
574    },
575    SurfaceTranslation {
576        concept: "grammar.number",
577        english: "number",
578        russian: "число",
579    },
580    SurfaceTranslation {
581        concept: "grammar.digit",
582        english: "digit",
583        russian: "цифра",
584    },
585    SurfaceTranslation {
586        concept: "grammar.letter",
587        english: "letter",
588        russian: "буква",
589    },
590    SurfaceTranslation {
591        concept: "grammar.identifier",
592        english: "identifier",
593        russian: "идентификатор",
594    },
595    SurfaceTranslation {
596        concept: "grammar.statement",
597        english: "statement",
598        russian: "оператор",
599    },
600    SurfaceTranslation {
601        concept: "grammar.item",
602        english: "item",
603        russian: "элемент",
604    },
605    SurfaceTranslation {
606        concept: "grammar.list",
607        english: "list",
608        russian: "список",
609    },
610    SurfaceTranslation {
611        concept: "grammar.value",
612        english: "value",
613        russian: "значение",
614    },
615    SurfaceTranslation {
616        concept: "grammar.name",
617        english: "name",
618        russian: "имя",
619    },
620    SurfaceTranslation {
621        concept: "grammar.object",
622        english: "object",
623        russian: "объект",
624    },
625    SurfaceTranslation {
626        concept: "grammar.member",
627        english: "member",
628        russian: "член",
629    },
630    SurfaceTranslation {
631        concept: "grammar.string",
632        english: "string",
633        russian: "строка",
634    },
635    SurfaceTranslation {
636        concept: "grammar.boolean-value",
637        english: "boolean",
638        russian: "логическое",
639    },
640    SurfaceTranslation {
641        concept: "grammar.null",
642        english: "null",
643        russian: "нуль",
644    },
645    SurfaceTranslation {
646        concept: "grammar::concept::expression",
647        english: "expression",
648        russian: "выражение",
649    },
650    SurfaceTranslation {
651        concept: "grammar::concept::term",
652        english: "term",
653        russian: "слагаемое",
654    },
655    SurfaceTranslation {
656        concept: "grammar::concept::factor",
657        english: "factor",
658        russian: "множитель",
659    },
660];