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::{Grammar, GrammarExpr, GrammarRule, GRAMMAR_CONCEPTS};
9use crate::{
10    LinkMetadata, LinkNetwork, LinkQuery, LinkType, ParseConfiguration, TranslationRule,
11    TranslationRuleSet, TranslationTemplate,
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        {
163            if previous_rule != rule.name() {
164                return Err(GrammarTranslateError::NameCollision {
165                    language: target_language.to_string(),
166                    name: translated_name,
167                });
168            }
169        }
170        rename_map.insert(rule.name().to_string(), translated_name);
171    }
172
173    Ok(rename_map)
174}
175
176#[derive(Clone, Debug, PartialEq, Eq)]
177struct ResolvedConcept {
178    concept: String,
179    explicit: bool,
180}
181
182fn resolve_rule_concept(rule: &GrammarRule, rules: &TranslationRuleSet) -> Option<ResolvedConcept> {
183    rule.concept()
184        .map(|concept| ResolvedConcept {
185            concept: concept.to_string(),
186            explicit: true,
187        })
188        .or_else(|| infer_concept_from_surface(rule.name(), rules))
189}
190
191fn infer_concept_from_surface(name: &str, rules: &TranslationRuleSet) -> Option<ResolvedConcept> {
192    for rule in rules.rules() {
193        let Some(concept) = concept_id_for_translation_rule(rule) else {
194            continue;
195        };
196        if concept == name
197            || rule
198                .templates()
199                .values()
200                .any(|template| template.source() == name)
201        {
202            return Some(ResolvedConcept {
203                concept: concept.to_string(),
204                explicit: false,
205            });
206        }
207    }
208
209    None
210}
211
212fn concept_id_for_translation_rule(rule: &TranslationRule) -> Option<&str> {
213    rule.query()
214        .term_filter()
215        .or_else(|| concept_like_rule_name(rule.name()))
216}
217
218fn concept_like_rule_name(name: &str) -> Option<&str> {
219    (name.starts_with("grammar.") || name.starts_with("grammar::concept::")).then_some(name)
220}
221
222fn translate_concept_surface(
223    concept: &str,
224    target_language: &str,
225    rules: &TranslationRuleSet,
226) -> Option<String> {
227    render_concept_with_rules(concept, target_language, rules).or_else(|| {
228        rules
229            .rules()
230            .iter()
231            .find(|rule| concept_id_for_translation_rule(rule) == Some(concept))
232            .and_then(|rule| template_for_language(rule.templates(), target_language))
233            .map(ToString::to_string)
234    })
235}
236
237fn render_concept_with_rules(
238    concept: &str,
239    target_language: &str,
240    rules: &TranslationRuleSet,
241) -> Option<String> {
242    let mut network = LinkNetwork::new();
243    let concept_link = network.intern_concept(concept, None);
244    network.insert_link(
245        [concept_link],
246        LinkMetadata::new()
247            .with_link_type(LinkType::Semantic)
248            .with_named(true)
249            .with_term(concept),
250    );
251
252    for language in language_lookup_order(target_language) {
253        let rendered =
254            network.reconstruct_text_as_with_rules(&language, ParseConfiguration::default(), rules);
255        if !rendered.is_empty() {
256            return Some(rendered);
257        }
258    }
259
260    None
261}
262
263fn template_for_language<'a>(
264    templates: &'a BTreeMap<String, TranslationTemplate>,
265    language: &str,
266) -> Option<&'a str> {
267    language_lookup_order(language)
268        .into_iter()
269        .find_map(|candidate| templates.get(&candidate).map(TranslationTemplate::source))
270}
271
272fn language_lookup_order(language: &str) -> Vec<String> {
273    match canonical_language(language) {
274        Some(("English", "en")) => vec!["English".to_string(), "en".to_string()],
275        Some(("Russian", "ru")) => vec!["Russian".to_string(), "ru".to_string()],
276        _ => vec![language.to_string()],
277    }
278}
279
280fn canonical_language(language: &str) -> Option<(&'static str, &'static str)> {
281    match language.to_ascii_lowercase().as_str() {
282        "english" | "en" => Some(("English", "en")),
283        "russian" | "ru" => Some(("Russian", "ru")),
284        _ => None,
285    }
286}
287
288fn renamed_name(name: &str, rename_map: &BTreeMap<String, String>) -> String {
289    rename_map
290        .get(name)
291        .cloned()
292        .unwrap_or_else(|| name.to_string())
293}
294
295fn rename_expr(expr: &GrammarExpr, rename_map: &BTreeMap<String, String>) -> GrammarExpr {
296    match expr {
297        GrammarExpr::NonTerminal(name) => GrammarExpr::NonTerminal(renamed_name(name, rename_map)),
298        GrammarExpr::Choice {
299            ordered,
300            alternatives,
301        } => GrammarExpr::Choice {
302            ordered: *ordered,
303            alternatives: alternatives
304                .iter()
305                .map(|expr| rename_expr(expr, rename_map))
306                .collect(),
307        },
308        GrammarExpr::Sequence(items) => GrammarExpr::Sequence(
309            items
310                .iter()
311                .map(|expr| rename_expr(expr, rename_map))
312                .collect(),
313        ),
314        GrammarExpr::Optional(expr) => {
315            GrammarExpr::Optional(Box::new(rename_expr(expr, rename_map)))
316        }
317        GrammarExpr::ZeroOrMore(expr) => {
318            GrammarExpr::ZeroOrMore(Box::new(rename_expr(expr, rename_map)))
319        }
320        GrammarExpr::OneOrMore(expr) => {
321            GrammarExpr::OneOrMore(Box::new(rename_expr(expr, rename_map)))
322        }
323        GrammarExpr::Repeat { expr, min, max } => GrammarExpr::Repeat {
324            expr: Box::new(rename_expr(expr, rename_map)),
325            min: *min,
326            max: *max,
327        },
328        GrammarExpr::And(expr) => GrammarExpr::And(Box::new(rename_expr(expr, rename_map))),
329        GrammarExpr::Not(expr) => GrammarExpr::Not(Box::new(rename_expr(expr, rename_map))),
330        GrammarExpr::Capture { label, expr } => GrammarExpr::Capture {
331            label: label.clone(),
332            expr: Box::new(rename_expr(expr, rename_map)),
333        },
334        GrammarExpr::Empty
335        | GrammarExpr::Terminal(_)
336        | GrammarExpr::TerminalInsensitive(_)
337        | GrammarExpr::CharRange(_, _)
338        | GrammarExpr::CharClass { .. }
339        | GrammarExpr::AnyChar => expr.clone(),
340    }
341}
342
343fn translate_doc_comment(doc: &str, target_language: &str, rules: &TranslationRuleSet) -> String {
344    for source_language in doc_source_languages(target_language) {
345        let network = LinkNetwork::parse(doc, source_language, ParseConfiguration::default());
346        let rendered = network.reconstruct_text_as_with_rules(
347            target_language,
348            ParseConfiguration::default(),
349            rules,
350        );
351        if rendered != doc {
352            return rendered;
353        }
354    }
355
356    let replaced = replace_known_concept_surfaces(doc, target_language, rules);
357    if replaced == doc {
358        doc.to_string()
359    } else {
360        replaced
361    }
362}
363
364fn doc_source_languages(target_language: &str) -> Vec<&'static str> {
365    match canonical_language(target_language) {
366        Some(("Russian", _)) => vec!["English"],
367        Some(("English", _)) => vec!["Russian"],
368        _ => vec!["English", "Russian"],
369    }
370}
371
372fn replace_known_concept_surfaces(
373    text: &str,
374    target_language: &str,
375    rules: &TranslationRuleSet,
376) -> String {
377    let mut replacements = rules
378        .rules()
379        .iter()
380        .filter_map(|rule| {
381            template_for_language(rule.templates(), target_language).map(|target| (rule, target))
382        })
383        .flat_map(|(rule, target)| {
384            rule.templates()
385                .values()
386                .filter_map(move |template| replacement_pair(template.source(), target))
387        })
388        .collect::<Vec<_>>();
389
390    replacements.sort_by_key(|replacement| Reverse(replacement.source.len()));
391    replacements.dedup();
392
393    replacements
394        .into_iter()
395        .fold(text.to_string(), |current, pair| {
396            replace_surface(&current, &pair.source, &pair.target)
397        })
398}
399
400fn replacement_pair(source: &str, target: &str) -> Option<ReplacementPair> {
401    (!source.is_empty() && source != target && !source.contains('{') && !target.contains('{')).then(
402        || ReplacementPair {
403            source: source.to_string(),
404            target: target.to_string(),
405        },
406    )
407}
408
409#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
410struct ReplacementPair {
411    source: String,
412    target: String,
413}
414
415fn replace_surface(text: &str, source: &str, target: &str) -> String {
416    let mut output = String::with_capacity(text.len());
417    let mut index = 0;
418
419    while index < text.len() {
420        if text[index..].starts_with(source)
421            && text.is_char_boundary(index + source.len())
422            && has_surface_boundaries(text, source, index, index + source.len())
423        {
424            output.push_str(target);
425            index += source.len();
426            continue;
427        }
428
429        let character = text[index..]
430            .chars()
431            .next()
432            .expect("index should be inside the string");
433        output.push(character);
434        index += character.len_utf8();
435    }
436
437    output
438}
439
440fn has_surface_boundaries(text: &str, source: &str, start: usize, end: usize) -> bool {
441    let first = source.chars().next();
442    let last = source.chars().next_back();
443    let before = text[..start].chars().next_back();
444    let after = text[end..].chars().next();
445
446    let start_ok = first.map_or(true, |character| !is_word_character(character))
447        || !before.is_some_and(is_word_character);
448    let end_ok = last.map_or(true, |character| !is_word_character(character))
449        || !after.is_some_and(is_word_character);
450
451    start_ok && end_ok
452}
453
454fn is_word_character(character: char) -> bool {
455    character.is_alphanumeric() || character == '_'
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459struct SurfaceTranslation {
460    concept: &'static str,
461    english: &'static str,
462    russian: &'static str,
463}
464
465const GRAMMAR_SURFACE_TRANSLATIONS: &[SurfaceTranslation] = &[
466    SurfaceTranslation {
467        concept: "grammar.rule",
468        english: "rule",
469        russian: "правило",
470    },
471    SurfaceTranslation {
472        concept: "grammar.sequence",
473        english: "sequence",
474        russian: "последовательность",
475    },
476    SurfaceTranslation {
477        concept: "grammar.ordered-choice",
478        english: "ordered choice",
479        russian: "упорядоченный выбор",
480    },
481    SurfaceTranslation {
482        concept: "grammar.unordered-choice",
483        english: "choice",
484        russian: "выбор",
485    },
486    SurfaceTranslation {
487        concept: "grammar.repetition",
488        english: "repetition",
489        russian: "повторение",
490    },
491    SurfaceTranslation {
492        concept: "grammar.zero-or-more",
493        english: "zero or more",
494        russian: "ноль или более",
495    },
496    SurfaceTranslation {
497        concept: "grammar.one-or-more",
498        english: "one or more",
499        russian: "один или более",
500    },
501    SurfaceTranslation {
502        concept: "grammar.optional",
503        english: "optional",
504        russian: "необязательный",
505    },
506    SurfaceTranslation {
507        concept: "grammar.terminal",
508        english: "terminal",
509        russian: "терминал",
510    },
511    SurfaceTranslation {
512        concept: "grammar.non-terminal",
513        english: "non-terminal",
514        russian: "нетерминал",
515    },
516    SurfaceTranslation {
517        concept: "grammar.char-class",
518        english: "character class",
519        russian: "класс символов",
520    },
521    SurfaceTranslation {
522        concept: "grammar.char-range",
523        english: "character range",
524        russian: "диапазон символов",
525    },
526    SurfaceTranslation {
527        concept: "grammar.any-char",
528        english: "any character",
529        russian: "любой символ",
530    },
531    SurfaceTranslation {
532        concept: "grammar.positive-predicate",
533        english: "positive predicate",
534        russian: "положительный предикат",
535    },
536    SurfaceTranslation {
537        concept: "grammar.negative-predicate",
538        english: "negative predicate",
539        russian: "отрицательный предикат",
540    },
541    SurfaceTranslation {
542        concept: "grammar.capture",
543        english: "capture",
544        russian: "захват",
545    },
546    SurfaceTranslation {
547        concept: "grammar.empty",
548        english: "empty",
549        russian: "пусто",
550    },
551    SurfaceTranslation {
552        concept: "grammar.expression",
553        english: "expression",
554        russian: "выражение",
555    },
556    SurfaceTranslation {
557        concept: "grammar.term",
558        english: "term",
559        russian: "слагаемое",
560    },
561    SurfaceTranslation {
562        concept: "grammar.factor",
563        english: "factor",
564        russian: "множитель",
565    },
566    SurfaceTranslation {
567        concept: "grammar.number",
568        english: "number",
569        russian: "число",
570    },
571    SurfaceTranslation {
572        concept: "grammar.digit",
573        english: "digit",
574        russian: "цифра",
575    },
576    SurfaceTranslation {
577        concept: "grammar.letter",
578        english: "letter",
579        russian: "буква",
580    },
581    SurfaceTranslation {
582        concept: "grammar.identifier",
583        english: "identifier",
584        russian: "идентификатор",
585    },
586    SurfaceTranslation {
587        concept: "grammar.statement",
588        english: "statement",
589        russian: "оператор",
590    },
591    SurfaceTranslation {
592        concept: "grammar.item",
593        english: "item",
594        russian: "элемент",
595    },
596    SurfaceTranslation {
597        concept: "grammar.list",
598        english: "list",
599        russian: "список",
600    },
601    SurfaceTranslation {
602        concept: "grammar.value",
603        english: "value",
604        russian: "значение",
605    },
606    SurfaceTranslation {
607        concept: "grammar.name",
608        english: "name",
609        russian: "имя",
610    },
611    SurfaceTranslation {
612        concept: "grammar.object",
613        english: "object",
614        russian: "объект",
615    },
616    SurfaceTranslation {
617        concept: "grammar.member",
618        english: "member",
619        russian: "член",
620    },
621    SurfaceTranslation {
622        concept: "grammar.string",
623        english: "string",
624        russian: "строка",
625    },
626    SurfaceTranslation {
627        concept: "grammar.boolean",
628        english: "boolean",
629        russian: "логическое",
630    },
631    SurfaceTranslation {
632        concept: "grammar.null",
633        english: "null",
634        russian: "нуль",
635    },
636    SurfaceTranslation {
637        concept: "grammar::concept::expression",
638        english: "expression",
639        russian: "выражение",
640    },
641    SurfaceTranslation {
642        concept: "grammar::concept::term",
643        english: "term",
644        russian: "слагаемое",
645    },
646    SurfaceTranslation {
647        concept: "grammar::concept::factor",
648        english: "factor",
649        russian: "множитель",
650    },
651];