1use std::collections::BTreeMap;
2use std::error::Error;
3use std::fmt;
4use std::sync::OnceLock;
5
6use crate::{
7 FormalizationLevel, Link, LinkId, LinkMetadata, LinkNetwork, LinkQuery, LinkType,
8 LinoSerializationError, NaturalizationDirection, ParseConfiguration, QueryParseError,
9};
10
11mod renderer;
12
13const RULE_SET_TERM: &str = "translation-rule-set";
14const RULE_TERM: &str = "translation-rule";
15const MATCH_TERM: &str = "translation-rule-match";
16const REFERENCE_CAPTURE_LANGUAGE: &str = "translation-rule-reference-capture";
17const LANGUAGE_FALLBACK_LANGUAGE: &str = "translation-rule-language-fallback";
18const TEMPLATE_DEFINITION: &str = "translation-rule-template";
19const FORMAL_LEXICAL_TARGET: &str = "formal:lexical";
20const FORMAL_CONCEPT_TARGET: &str = "formal:concept";
21const FORMAL_LOGICAL_TARGET: &str = "formal:logical";
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct TranslationRuleSet {
26 name: String,
27 rules: Vec<TranslationRule>,
28 language_fallbacks: BTreeMap<String, Vec<String>>,
29}
30
31impl TranslationRuleSet {
32 #[must_use]
34 pub fn new(name: impl Into<String>) -> Self {
35 Self {
36 name: name.into(),
37 rules: Vec::new(),
38 language_fallbacks: BTreeMap::new(),
39 }
40 }
41
42 #[must_use]
44 pub fn name(&self) -> &str {
45 &self.name
46 }
47
48 #[must_use]
50 pub fn rules(&self) -> &[TranslationRule] {
51 &self.rules
52 }
53
54 #[must_use]
56 pub const fn language_fallbacks(&self) -> &BTreeMap<String, Vec<String>> {
57 &self.language_fallbacks
58 }
59
60 #[must_use]
62 pub fn with_rule(mut self, rule: TranslationRule) -> Self {
63 self.add_rule(rule);
64 self
65 }
66
67 pub fn add_rule(&mut self, rule: TranslationRule) {
69 self.rules.push(rule);
70 }
71
72 #[must_use]
74 pub fn with_language_fallback(
75 mut self,
76 target_language: impl Into<String>,
77 fallback_language: impl Into<String>,
78 ) -> Self {
79 let fallbacks = self
80 .language_fallbacks
81 .entry(target_language.into())
82 .or_default();
83 let fallback_language = fallback_language.into();
84 if !fallbacks.contains(&fallback_language) {
85 fallbacks.push(fallback_language);
86 }
87 self
88 }
89
90 #[must_use]
92 pub fn to_lino(&self) -> String {
93 let mut network = LinkNetwork::new();
94 let root = network.insert_link(
95 [],
96 LinkMetadata::new()
97 .with_link_type(LinkType::Semantic)
98 .with_named(true)
99 .with_term(RULE_SET_TERM)
100 .with_definition(&self.name),
101 );
102
103 for (target, fallbacks) in &self.language_fallbacks {
104 for fallback in fallbacks {
105 network.insert_link(
106 [root],
107 LinkMetadata::new()
108 .with_link_type(LinkType::Semantic)
109 .with_named(true)
110 .with_term(target)
111 .with_language(LANGUAGE_FALLBACK_LANGUAGE)
112 .with_definition(fallback),
113 );
114 }
115 }
116
117 for rule in &self.rules {
118 let rule_link = network.insert_link(
119 [root],
120 LinkMetadata::new()
121 .with_link_type(LinkType::Semantic)
122 .with_named(true)
123 .with_term(RULE_TERM)
124 .with_definition(rule.name()),
125 );
126 network.insert_link(
127 [rule_link],
128 LinkMetadata::new()
129 .with_link_type(LinkType::Semantic)
130 .with_named(true)
131 .with_term(MATCH_TERM)
132 .with_definition(query_to_rule_spec(&rule.query)),
133 );
134 for (capture, reference_index) in &rule.reference_captures {
135 network.insert_link(
136 [rule_link],
137 LinkMetadata::new()
138 .with_link_type(LinkType::Semantic)
139 .with_named(true)
140 .with_term(capture)
141 .with_language(REFERENCE_CAPTURE_LANGUAGE)
142 .with_definition(reference_index.to_string()),
143 );
144 }
145 for (target, template) in &rule.templates {
146 network.insert_link(
147 [rule_link],
148 LinkMetadata::new()
149 .with_link_type(LinkType::Semantic)
150 .with_named(true)
151 .with_term(template.source())
152 .with_language(target)
153 .with_definition(TEMPLATE_DEFINITION),
154 );
155 }
156 }
157
158 network.to_lino()
159 }
160
161 pub fn from_lino(text: &str) -> Result<Self, TranslationRuleSetLoadError> {
163 let network = LinkNetwork::from_lino(text)?;
164 let root = network
165 .links()
166 .find(|link| {
167 link.metadata().link_type() == Some(LinkType::Semantic)
168 && link.metadata().term() == Some(RULE_SET_TERM)
169 })
170 .ok_or_else(|| {
171 TranslationRuleSetLoadError::Structure(
172 "missing translation-rule-set root".to_string(),
173 )
174 })?;
175 let mut rules = Vec::new();
176 let mut rule_links = network
177 .links()
178 .filter(|link| {
179 link.references().first().copied() == Some(root.id())
180 && link.metadata().term() == Some(RULE_TERM)
181 })
182 .collect::<Vec<_>>();
183 rule_links.sort_by_key(|link| link.id());
184
185 for rule_link in rule_links {
186 rules.push(load_rule(&network, rule_link)?);
187 }
188
189 let mut language_fallbacks = BTreeMap::<String, Vec<String>>::new();
190 for link in network.links().filter(|link| {
191 link.references().first().copied() == Some(root.id())
192 && link.metadata().language() == Some(LANGUAGE_FALLBACK_LANGUAGE)
193 }) {
194 let target = link.metadata().term().ok_or_else(|| {
195 TranslationRuleSetLoadError::Structure(
196 "language fallback is missing a target".to_string(),
197 )
198 })?;
199 let fallback = link.metadata().definition().ok_or_else(|| {
200 TranslationRuleSetLoadError::Structure(
201 "language fallback is missing a fallback target".to_string(),
202 )
203 })?;
204 language_fallbacks
205 .entry(target.to_string())
206 .or_default()
207 .push(fallback.to_string());
208 }
209
210 Ok(Self {
211 name: root
212 .metadata()
213 .definition()
214 .unwrap_or(RULE_SET_TERM)
215 .to_string(),
216 rules,
217 language_fallbacks,
218 })
219 }
220
221 #[must_use]
223 pub fn statehood_demo_lino() -> &'static str {
224 static LINO: OnceLock<String> = OnceLock::new();
225 LINO.get_or_init(|| statehood_demo_rule_set().to_lino())
226 }
227
228 #[must_use]
230 pub fn statehood_demo() -> Self {
231 Self::from_lino(Self::statehood_demo_lino())
232 .expect("statehood demo translation rule set must load")
233 }
234
235 pub(crate) fn render(
236 &self,
237 network: &LinkNetwork,
238 target_language: &str,
239 configuration: ParseConfiguration,
240 ) -> Option<String> {
241 let source = network.reconstruct_text();
242 renderer::render_roots(self, network, target_language, configuration)
243 .map(|rendered| with_source_trailing_newline(rendered, &source))
244 }
245
246 #[must_use]
248 pub fn render_link(
249 &self,
250 network: &LinkNetwork,
251 link_id: LinkId,
252 target_language: &str,
253 configuration: ParseConfiguration,
254 ) -> String {
255 renderer::render_link(self, network, link_id, target_language, configuration)
256 }
257}
258
259#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct TranslationRule {
262 name: String,
263 query: LinkQuery,
264 reference_captures: BTreeMap<String, usize>,
265 templates: BTreeMap<String, TranslationTemplate>,
266}
267
268impl TranslationRule {
269 #[must_use]
271 pub fn new(name: impl Into<String>, query: LinkQuery) -> Self {
272 Self {
273 name: name.into(),
274 query,
275 reference_captures: BTreeMap::new(),
276 templates: BTreeMap::new(),
277 }
278 }
279
280 #[must_use]
282 pub fn name(&self) -> &str {
283 &self.name
284 }
285
286 #[must_use]
288 pub const fn query(&self) -> &LinkQuery {
289 &self.query
290 }
291
292 #[must_use]
294 pub const fn reference_captures(&self) -> &BTreeMap<String, usize> {
295 &self.reference_captures
296 }
297
298 #[must_use]
300 pub const fn templates(&self) -> &BTreeMap<String, TranslationTemplate> {
301 &self.templates
302 }
303
304 #[must_use]
306 pub fn with_reference_capture(
307 mut self,
308 name: impl Into<String>,
309 reference_index: usize,
310 ) -> Self {
311 self.reference_captures.insert(name.into(), reference_index);
312 self
313 }
314
315 #[must_use]
317 pub fn with_template(
318 mut self,
319 target_language: impl Into<String>,
320 template: impl Into<String>,
321 ) -> Self {
322 self.templates.insert(
323 target_language.into(),
324 TranslationTemplate::new(template.into()),
325 );
326 self
327 }
328
329 #[must_use]
331 pub fn with_formal_template(
332 mut self,
333 level: FormalizationLevel,
334 template: impl Into<String>,
335 ) -> Self {
336 self.templates.insert(
337 formal_template_target(level).to_string(),
338 TranslationTemplate::new(template.into()),
339 );
340 self
341 }
342
343 fn template_for(
344 &self,
345 target_language: &str,
346 configuration: ParseConfiguration,
347 ) -> Option<&TranslationTemplate> {
348 let level = effective_formalization_level(configuration);
349 if level != FormalizationLevel::Natural {
350 return self.templates.get(formal_template_target(level));
351 }
352
353 self.templates.get(target_language).or_else(|| {
354 canonical_reconstruction_language(target_language)
355 .and_then(|language| self.templates.get(language))
356 })
357 }
358}
359
360#[derive(Clone, Debug, PartialEq, Eq)]
362pub struct TranslationTemplate {
363 source: String,
364}
365
366impl TranslationTemplate {
367 #[must_use]
369 pub fn new(source: impl Into<String>) -> Self {
370 Self {
371 source: source.into(),
372 }
373 }
374
375 #[must_use]
377 pub fn source(&self) -> &str {
378 &self.source
379 }
380}
381
382#[derive(Clone, Debug, Default, PartialEq, Eq)]
384pub struct TranslationRuleRegistry {
385 rule_sets: BTreeMap<String, TranslationRuleSet>,
386 active_rule_set: Option<String>,
387}
388
389impl TranslationRuleRegistry {
390 #[must_use]
392 pub fn new() -> Self {
393 Self::default()
394 }
395
396 #[must_use]
398 pub fn with_statehood_demo() -> Self {
399 Self::new().with_rule_set(TranslationRuleSet::statehood_demo())
400 }
401
402 #[must_use]
404 pub fn with_rule_set(mut self, rule_set: TranslationRuleSet) -> Self {
405 self.replace_rule_set(rule_set);
406 self
407 }
408
409 pub fn replace_rule_set(&mut self, rule_set: TranslationRuleSet) {
411 let name = rule_set.name().to_string();
412 if self.active_rule_set.is_none() {
413 self.active_rule_set = Some(name.clone());
414 }
415 self.rule_sets.insert(name, rule_set);
416 }
417
418 pub fn set_active_rule_set(&mut self, name: &str) -> bool {
420 if self.rule_sets.contains_key(name) {
421 self.active_rule_set = Some(name.to_string());
422 true
423 } else {
424 false
425 }
426 }
427
428 #[must_use]
430 pub fn active_rule_set(&self) -> Option<&TranslationRuleSet> {
431 self.active_rule_set
432 .as_deref()
433 .and_then(|name| self.rule_sets.get(name))
434 }
435
436 #[must_use]
438 pub fn rule_set(&self, name: &str) -> Option<&TranslationRuleSet> {
439 self.rule_sets.get(name)
440 }
441
442 #[must_use]
444 pub fn len(&self) -> usize {
445 self.rule_sets.len()
446 }
447
448 #[must_use]
450 pub fn is_empty(&self) -> bool {
451 self.rule_sets.is_empty()
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq)]
457pub enum TranslationRuleSetLoadError {
458 Lino(LinoSerializationError),
460 Structure(String),
462 Query(QueryParseError),
464}
465
466impl fmt::Display for TranslationRuleSetLoadError {
467 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
468 match self {
469 Self::Lino(error) => write!(formatter, "{error}"),
470 Self::Structure(message) => {
471 write!(formatter, "translation rule structure error: {message}")
472 }
473 Self::Query(error) => write!(formatter, "translation rule query error: {error}"),
474 }
475 }
476}
477
478impl Error for TranslationRuleSetLoadError {}
479
480impl From<LinoSerializationError> for TranslationRuleSetLoadError {
481 fn from(error: LinoSerializationError) -> Self {
482 Self::Lino(error)
483 }
484}
485
486impl From<QueryParseError> for TranslationRuleSetLoadError {
487 fn from(error: QueryParseError) -> Self {
488 Self::Query(error)
489 }
490}
491
492fn load_rule(
493 network: &LinkNetwork,
494 rule_link: &Link,
495) -> Result<TranslationRule, TranslationRuleSetLoadError> {
496 let name = rule_link.metadata().definition().ok_or_else(|| {
497 TranslationRuleSetLoadError::Structure("rule is missing a name".to_string())
498 })?;
499 let query_source = network
500 .links()
501 .find(|link| {
502 link.references().first().copied() == Some(rule_link.id())
503 && link.metadata().term() == Some(MATCH_TERM)
504 })
505 .and_then(|link| link.metadata().definition())
506 .ok_or_else(|| {
507 TranslationRuleSetLoadError::Structure("rule is missing a match query".to_string())
508 })?;
509 let mut rule = TranslationRule::new(name, query_from_rule_spec(query_source)?);
510 let mut children = network
511 .links()
512 .filter(|link| link.references().first().copied() == Some(rule_link.id()))
513 .collect::<Vec<_>>();
514 children.sort_by_key(|link| link.id());
515
516 for child in children {
517 let metadata = child.metadata();
518 if metadata.term() == Some(MATCH_TERM) {
519 continue;
520 }
521 if metadata.language() == Some(REFERENCE_CAPTURE_LANGUAGE) {
522 let capture = metadata.term().ok_or_else(|| {
523 TranslationRuleSetLoadError::Structure(
524 "reference capture is missing a capture name".to_string(),
525 )
526 })?;
527 let index = metadata
528 .definition()
529 .ok_or_else(|| {
530 TranslationRuleSetLoadError::Structure(
531 "reference capture is missing an index".to_string(),
532 )
533 })?
534 .parse::<usize>()
535 .map_err(|error| {
536 TranslationRuleSetLoadError::Structure(format!(
537 "invalid reference capture index: {error}"
538 ))
539 })?;
540 rule = rule.with_reference_capture(capture, index);
541 } else if metadata.definition() == Some(TEMPLATE_DEFINITION) {
542 let target = metadata.language().ok_or_else(|| {
543 TranslationRuleSetLoadError::Structure("template is missing a target".to_string())
544 })?;
545 let template = metadata.term().ok_or_else(|| {
546 TranslationRuleSetLoadError::Structure(
547 "template is missing source text".to_string(),
548 )
549 })?;
550 rule = rule.with_template(target, template);
551 }
552 }
553
554 Ok(rule)
555}
556
557fn query_to_rule_spec(query: &LinkQuery) -> String {
558 let mut object = serde_json::Map::new();
559 if let Some(link_type) = query.link_type_filter() {
560 object.insert("link_type".to_string(), link_type.to_string().into());
561 }
562 if let Some(term) = query.term_filter() {
563 object.insert("term".to_string(), term.into());
564 }
565 if let Some(language) = query.language_filter() {
566 object.insert("language".to_string(), language.into());
567 }
568 if let Some(named) = query.named_filter() {
569 object.insert("named".to_string(), named.into());
570 }
571 if let Some(pattern_source) = query.pattern_source() {
572 object.insert("sexpression".to_string(), pattern_source.into());
573 }
574
575 serde_json::Value::Object(object).to_string()
576}
577
578fn query_from_rule_spec(source: &str) -> Result<LinkQuery, QueryParseError> {
579 let value = serde_json::from_str::<serde_json::Value>(source)
580 .map_err(|error| QueryParseError::new(format!("invalid query spec: {error}")))?;
581 let object = value
582 .as_object()
583 .ok_or_else(|| QueryParseError::new("query spec must be a JSON object"))?;
584
585 let mut query =
586 if let Some(sexpression) = object.get("sexpression").and_then(|value| value.as_str()) {
587 LinkQuery::from_sexpression(sexpression)?
588 } else {
589 LinkQuery::new()
590 };
591
592 if let Some(link_type) = object.get("link_type").and_then(|value| value.as_str()) {
593 query = query.with_link_type(parse_query_link_type(link_type)?);
594 }
595 if let Some(term) = object.get("term").and_then(|value| value.as_str()) {
596 query = query.with_term(term);
597 }
598 if let Some(language) = object.get("language").and_then(|value| value.as_str()) {
599 query = query.with_language(language);
600 }
601 if let Some(named) = object.get("named").and_then(serde_json::Value::as_bool) {
602 query = query.with_named(named);
603 }
604
605 Ok(query)
606}
607
608fn parse_query_link_type(token: &str) -> Result<LinkType, QueryParseError> {
609 Ok(match token {
610 "link" => LinkType::Link,
611 "reference" => LinkType::Reference,
612 "relation" => LinkType::Relation,
613 "language" => LinkType::Language,
614 "grammar" => LinkType::Grammar,
615 "type" => LinkType::Type,
616 "concept" => LinkType::Concept,
617 "syntax" => LinkType::Syntax,
618 "field" => LinkType::Field,
619 "trivia" => LinkType::Trivia,
620 "token" => LinkType::Token,
621 "document" => LinkType::Document,
622 "semantic" => LinkType::Semantic,
623 "region" => LinkType::Region,
624 "object" => LinkType::Object,
625 other => {
626 return Err(QueryParseError::new(format!(
627 "unknown query link type `{other}`"
628 )))
629 }
630 })
631}
632
633fn statehood_demo_rule_set() -> TranslationRuleSet {
634 TranslationRuleSet::new("statehood-demo").with_rule(
635 TranslationRule::new(
636 "statehood proposition",
637 LinkQuery::by_type(LinkType::Semantic).with_term("proposition:statehood"),
638 )
639 .with_reference_capture("subject", 2)
640 .with_reference_capture("object", 3)
641 .with_template("English", "{subject} is a {object}.")
642 .with_template("en", "{subject} is a {object}.")
643 .with_template("Russian", "{subject} это {object}.")
644 .with_template("ru", "{subject} это {object}.")
645 .with_formal_template(
646 FormalizationLevel::Lexical,
647 "statehood({subject}, {object})",
648 )
649 .with_formal_template(
650 FormalizationLevel::Concept,
651 [
652 "statehood(",
653 "{subject:concept}",
654 ", ",
655 "{object:concept}",
656 ")",
657 ]
658 .concat(),
659 )
660 .with_formal_template(
661 FormalizationLevel::Logical,
662 [
663 "(proposition: statehood (subject: ",
664 "{subject:concept}",
665 ") (object: ",
666 "{object:concept}",
667 ") (truth: true))",
668 ]
669 .concat(),
670 ),
671 )
672}
673
674const fn effective_formalization_level(configuration: ParseConfiguration) -> FormalizationLevel {
675 match (
676 configuration.naturalization_direction(),
677 configuration.formalization_level(),
678 ) {
679 (NaturalizationDirection::Formalize, FormalizationLevel::Natural) => {
680 FormalizationLevel::Lexical
681 }
682 (_, level) => level,
683 }
684}
685
686const fn formal_template_target(level: FormalizationLevel) -> &'static str {
687 match level {
688 FormalizationLevel::Natural => "",
689 FormalizationLevel::Lexical => FORMAL_LEXICAL_TARGET,
690 FormalizationLevel::Concept => FORMAL_CONCEPT_TARGET,
691 FormalizationLevel::Logical => FORMAL_LOGICAL_TARGET,
692 }
693}
694
695fn canonical_reconstruction_language(language: &str) -> Option<&'static str> {
696 match language.to_ascii_lowercase().as_str() {
697 "english" | "en" => Some("English"),
698 "russian" | "ru" => Some("Russian"),
699 _ => None,
700 }
701}
702
703fn with_source_trailing_newline(mut body: String, source: &str) -> String {
704 if source.ends_with('\n') {
705 body.push('\n');
706 }
707 body
708}