use std::collections::{BTreeMap, BTreeSet};
use crate::decorators::{
DecoratorError, DecoratorLevel, DecoratorSet, decorator_record, record_field,
};
use crate::language_catalog::{native_grammar, native_grammars};
use crate::native_grammar_parser::{native_grammar_text, native_parser};
use crate::{ConceptRecord, FeatureParseOptions, SyntaxTree, concept_records, parse_grammar_links};
const NATIVE_SOURCE_PREFIX: &str = "native:";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeRuleConcept {
pub rule: String,
pub concept: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeConceptProblemKind {
RuleWithoutConcept,
ConceptWithoutRecord,
RuleWithoutAlias,
AliasWithoutRule,
NamespaceMismatch,
}
impl NativeConceptProblemKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RuleWithoutConcept => "rule-without-concept",
Self::ConceptWithoutRecord => "concept-without-record",
Self::RuleWithoutAlias => "rule-without-alias",
Self::AliasWithoutRule => "alias-without-rule",
Self::NamespaceMismatch => "namespace-mismatch",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeConceptProblem {
pub kind: NativeConceptProblemKind,
pub grammar: String,
pub rule: String,
pub concept: Option<String>,
}
#[must_use]
pub fn native_grammar_ids() -> Vec<&'static str> {
let mut ids: Vec<&'static str> = native_grammars()
.iter()
.map(|grammar| grammar.id.as_str())
.collect();
ids.sort_unstable();
ids
}
#[must_use]
pub fn native_grammar_language(id: &str) -> Option<&'static str> {
let file = native_grammar(id)?.file.as_str();
let name = file.rsplit('/').next().unwrap_or(file);
Some(name.strip_suffix(".lino").unwrap_or(name))
}
#[must_use]
pub fn native_grammar_source(id: &str) -> Option<String> {
native_grammar_language(id).map(|language| format!("{NATIVE_SOURCE_PREFIX}{language}"))
}
#[must_use]
pub fn native_grammar_rule_concepts(id: &str) -> Vec<NativeRuleConcept> {
native_grammar_text(id)
.and_then(|text| parse_grammar_links(text).ok())
.map(|grammar| {
grammar
.rules()
.iter()
.map(|rule| NativeRuleConcept {
rule: rule.name.clone(),
concept: rule.concept.clone(),
})
.collect()
})
.unwrap_or_default()
}
fn namespace_language<'a>(concept: &'a str, languages: &BTreeSet<&str>) -> Option<&'a str> {
let parts: Vec<&str> = concept.split('.').collect();
(parts.len() > 2 && languages.contains(parts[1])).then(|| parts[1])
}
#[must_use]
pub fn check_native_grammar_concepts() -> Vec<NativeConceptProblem> {
check_native_grammar_concepts_in(
concept_records(),
&native_grammar_ids(),
&native_grammar_rule_concepts,
)
}
#[must_use]
pub fn check_native_grammar_concepts_in(
records: &[ConceptRecord],
grammars: &[&str],
rule_concepts: &dyn Fn(&str) -> Vec<NativeRuleConcept>,
) -> Vec<NativeConceptProblem> {
let by_id: BTreeMap<&str, &ConceptRecord> = records
.iter()
.map(|record| (record.id.as_str(), record))
.collect();
let languages: BTreeSet<&str> = grammars
.iter()
.filter_map(|id| native_grammar_language(id))
.collect();
let mut problems = Vec::new();
for &grammar in grammars {
let (Some(source), Some(language)) = (
native_grammar_source(grammar),
native_grammar_language(grammar),
) else {
continue;
};
let problem = |kind, rule: &str, concept: Option<&str>| NativeConceptProblem {
kind,
grammar: grammar.to_owned(),
rule: rule.to_owned(),
concept: concept.map(str::to_owned),
};
let mut named = BTreeMap::new();
for NativeRuleConcept { rule, concept } in rule_concepts(grammar) {
let Some(concept) = concept else {
problems.push(problem(
NativeConceptProblemKind::RuleWithoutConcept,
&rule,
None,
));
continue;
};
let Some(record) = by_id.get(concept.as_str()) else {
problems.push(problem(
NativeConceptProblemKind::ConceptWithoutRecord,
&rule,
Some(&concept),
));
named.insert(rule, concept);
continue;
};
if !record
.source_aliases
.iter()
.any(|alias| alias.source == source && alias.name == rule)
{
problems.push(problem(
NativeConceptProblemKind::RuleWithoutAlias,
&rule,
Some(&concept),
));
}
if namespace_language(&concept, &languages).is_some_and(|owner| owner != language) {
problems.push(problem(
NativeConceptProblemKind::NamespaceMismatch,
&rule,
Some(&concept),
));
}
named.insert(rule, concept);
}
for record in records {
for alias in &record.source_aliases {
if alias.source == source && named.get(&alias.name) != Some(&record.id) {
problems.push(problem(
NativeConceptProblemKind::AliasWithoutRule,
&alias.name,
Some(&record.id),
));
}
}
}
}
problems
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedRule {
pub rule: String,
pub concept: String,
pub languages: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpecificRule {
pub rule: String,
pub concept: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrammarConceptReuse {
pub grammar: String,
pub language: String,
pub rules: usize,
pub shared: Vec<SharedRule>,
pub specific: Vec<SpecificRule>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConceptReuse {
pub concept: String,
pub languages: Vec<String>,
pub shared: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeConceptReuse {
pub grammars: Vec<GrammarConceptReuse>,
pub concepts: Vec<ConceptReuse>,
}
#[must_use]
pub fn native_grammar_concept_reuse() -> NativeConceptReuse {
let grammars = native_grammar_ids();
let language_of: BTreeMap<String, &str> = grammars
.iter()
.filter_map(|id| Some((native_grammar_source(id)?, native_grammar_language(id)?)))
.collect();
let mut languages: BTreeMap<&str, Vec<String>> = BTreeMap::new();
for record in concept_records() {
let named: BTreeSet<&str> = record
.source_aliases
.iter()
.filter_map(|alias| language_of.get(&alias.source).copied())
.collect();
if !named.is_empty() {
languages.insert(&record.id, named.into_iter().map(str::to_owned).collect());
}
}
let report = grammars
.iter()
.map(|&grammar| {
let mut shared = Vec::new();
let mut specific = Vec::new();
for NativeRuleConcept { rule, concept } in native_grammar_rule_concepts(grammar) {
let concept = concept.unwrap_or_default();
match languages.get(concept.as_str()) {
Some(named) if named.len() > 1 => shared.push(SharedRule {
rule,
concept,
languages: named.clone(),
}),
_ => specific.push(SpecificRule { rule, concept }),
}
}
GrammarConceptReuse {
grammar: grammar.to_owned(),
language: native_grammar_language(grammar)
.unwrap_or_default()
.to_owned(),
rules: shared.len() + specific.len(),
shared,
specific,
}
})
.collect();
let concepts = languages
.into_iter()
.map(|(concept, named)| ConceptReuse {
concept: concept.to_owned(),
shared: named.len() > 1,
languages: named,
})
.collect();
NativeConceptReuse {
grammars: report,
concepts,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstructTranslationRelation {
Translated,
Untranslatable,
Ambiguous,
Unknown,
}
impl ConstructTranslationRelation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Translated => "translated",
Self::Untranslatable => "untranslatable",
Self::Ambiguous => "ambiguous",
Self::Unknown => "unknown",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
[
Self::Translated,
Self::Untranslatable,
Self::Ambiguous,
Self::Unknown,
]
.into_iter()
.find(|relation| relation.as_str() == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructTranslation {
pub relation: ConstructTranslationRelation,
pub concept: Option<String>,
pub rules: Vec<String>,
}
#[must_use]
pub fn translate_native_construct(from: &str, rule: &str, to: &str) -> ConstructTranslation {
translate_native_construct_in(concept_records(), from, rule, to)
}
#[must_use]
pub fn translate_native_construct_in(
records: &[ConceptRecord],
from: &str,
rule: &str,
to: &str,
) -> ConstructTranslation {
let unknown = ConstructTranslation {
relation: ConstructTranslationRelation::Unknown,
concept: None,
rules: Vec::new(),
};
let (Some(source), Some(target)) = (native_grammar_source(from), native_grammar_source(to))
else {
return unknown;
};
let meanings: Vec<&ConceptRecord> = records
.iter()
.filter(|record| {
record
.source_aliases
.iter()
.any(|alias| alias.source == source && alias.name == rule)
})
.collect();
let record = match meanings.as_slice() {
[] => return unknown,
[record] => record,
_ => {
return ConstructTranslation {
relation: ConstructTranslationRelation::Ambiguous,
..unknown
};
}
};
let rules: Vec<String> = record
.source_aliases
.iter()
.filter(|alias| alias.source == target)
.map(|alias| alias.name.clone())
.collect();
ConstructTranslation {
relation: relation_of(&rules),
concept: Some(record.id.clone()),
rules,
}
}
const fn relation_of(rules: &[String]) -> ConstructTranslationRelation {
match rules.len() {
1 => ConstructTranslationRelation::Translated,
0 => ConstructTranslationRelation::Untranslatable,
_ => ConstructTranslationRelation::Ambiguous,
}
}
pub fn translate_native_construct_decorated(
from: &str,
rule: &str,
to: &str,
decorators: &DecoratorSet,
) -> Result<ConstructTranslation, DecoratorError> {
let found = translate_native_construct(from, rule, to);
if !decorators.has(DecoratorLevel::ConceptMapping) {
return Ok(found);
}
let joined = found.rules.join(" ");
let record = decorator_record([
("from", from),
("to", to),
("rule", rule),
("relation", found.relation.as_str()),
("concept", found.concept.as_deref().unwrap_or_default()),
("rules", &joined),
]);
let Some(decorated) = decorators.decorate(DecoratorLevel::ConceptMapping, &record) else {
return Ok(ConstructTranslation {
relation: ConstructTranslationRelation::Unknown,
concept: None,
rules: Vec::new(),
});
};
let rules: Vec<String> = record_field(&decorated, "rules")
.split(' ')
.filter(|name| !name.is_empty())
.map(str::to_owned)
.collect();
let relation = record_field(&decorated, "relation");
let relation =
if relation != found.relation.as_str() || record_field(&decorated, "rules") == joined {
ConstructTranslationRelation::parse(relation).ok_or_else(|| {
DecoratorError::new(format!("a decorator set the unknown relation {relation:?}"))
})?
} else {
relation_of(&rules)
};
let concept = record_field(&decorated, "concept");
Ok(ConstructTranslation {
relation,
concept: (!concept.is_empty()).then(|| concept.to_owned()),
rules,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructTree {
pub kind: String,
pub concept: String,
pub children: Vec<Self>,
}
#[must_use]
pub fn native_construct_tree(id: &str, source: &str) -> Option<ConstructTree> {
let concepts: BTreeMap<String, String> = native_grammar_rule_concepts(id)
.into_iter()
.filter_map(|NativeRuleConcept { rule, concept }| Some((rule, concept?)))
.collect();
let outcome = native_parser(id)?
.parse_tree(source.as_bytes(), &FeatureParseOptions::default())
.ok()?;
let tree = outcome.tree?;
if !matches!(tree, SyntaxTree::Node { .. }) {
return None;
}
constructs(&tree, &concepts)?.into_iter().next()
}
fn constructs(
node: &SyntaxTree,
concepts: &BTreeMap<String, String>,
) -> Option<Vec<ConstructTree>> {
let (kind, children) = match node {
SyntaxTree::Node { kind, children, .. } => {
let mut inner = Vec::new();
for child in children {
inner.extend(constructs(child, concepts)?);
}
(Some(kind), inner)
}
SyntaxTree::Token {
kind,
trivia: false,
..
} => (kind.as_ref(), Vec::new()),
SyntaxTree::Token { .. } | SyntaxTree::Embed { .. } => (None, Vec::new()),
SyntaxTree::Error { .. } | SyntaxTree::Missing { .. } => return None,
};
match kind.and_then(|kind| Some((kind, concepts.get(kind)?))) {
Some((kind, concept)) => Some(vec![ConstructTree {
kind: kind.clone(),
concept: concept.clone(),
children,
}]),
None => Some(children),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructTranslationProblem {
pub kind: String,
pub concept: Option<String>,
pub relation: ConstructTranslationRelation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructTreeTranslation {
pub tree: Option<ConstructTree>,
pub problems: Vec<ConstructTranslationProblem>,
}
#[must_use]
pub fn translate_native_construct_tree(
tree: &ConstructTree,
from: &str,
to: &str,
) -> ConstructTreeTranslation {
fn translate(
node: &ConstructTree,
from: &str,
to: &str,
problems: &mut Vec<ConstructTranslationProblem>,
) -> Option<ConstructTree> {
let translation = translate_native_construct(from, &node.kind, to);
let children: Vec<Option<ConstructTree>> = node
.children
.iter()
.map(|child| translate(child, from, to, problems))
.collect();
if translation.relation != ConstructTranslationRelation::Translated {
problems.push(ConstructTranslationProblem {
kind: node.kind.clone(),
concept: translation.concept,
relation: translation.relation,
});
return None;
}
Some(ConstructTree {
kind: translation.rules[0].clone(),
concept: translation.concept.unwrap_or_default(),
children: children.into_iter().collect::<Option<Vec<_>>>()?,
})
}
let mut problems = Vec::new();
let translated = translate(tree, from, to, &mut problems);
ConstructTreeTranslation {
tree: if problems.is_empty() {
translated
} else {
None
},
problems,
}
}