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