Skip to main content

interscript_rs/
document.rs

1//! The document model — mirrors `Interscript::Node::Document` as
2//! produced by `Interscript::Isc::NodeAdapter`, plus the pieces the
3//! interpreter needs (resolved dependency documents, embedded tests).
4
5use crate::item::Item;
6use crate::stdlib::Funcall;
7use std::collections::BTreeMap;
8use std::rc::Rc;
9
10#[derive(Debug, Clone)]
11pub struct Sub {
12    pub from: Item,
13    pub to: SubTo,
14    pub before: Option<Item>,
15    pub not_before: Option<Item>,
16    pub after: Option<Item>,
17    pub not_after: Option<Item>,
18    /// Number of capture groups in regex order
19    /// (before, not_before, from, not_after, after).
20    pub group_count: usize,
21    /// Ruby `reverse_run:`: `Some(true)` skips the rule in the forward
22    /// direction and runs it in the reversed document (and vice versa).
23    /// The ISC grammar cannot express it; only `Document::reverse`
24    /// produces it.
25    pub reverse_run: Option<bool>,
26    /// ISC-WF-1 W3: the `when` language guards (§5). Empty = unguarded
27    /// (always applicable). A guarded rule applies only when the caller
28    /// selects one of these languages.
29    pub when: Vec<String>,
30}
31
32#[derive(Debug, Clone)]
33pub enum SubTo {
34    /// `to upcase` — upcase the whole match.
35    Upcase,
36    /// `to downcase` — downcase the whole match.
37    Downcase,
38    /// A replacement item.
39    Item(Item),
40}
41
42impl Sub {
43    /// `Interscript::Node::Rule::Sub#max_length`.
44    pub fn max_length(&self) -> usize {
45        let mut len = self.from.max_length();
46        for c in [&self.before, &self.not_before, &self.after, &self.not_after] {
47            if let Some(item) = c {
48                len += item.max_length();
49            }
50        }
51        len
52    }
53
54    /// ISC-WF-1 W2/W3 canonical from-key: the structural rendering of
55    /// the EXPANDED from item. For literal froms this is byte-exact
56    /// string equality (the corpus convention — no Unicode
57    /// normalization); non-literal froms (any()/alias trees) compare
58    /// structurally, which is the exact-from generalization the spec's
59    /// "from(o) == from(r)" needs on resolved items.
60    pub fn from_key(&self) -> String {
61        format!("{:?}", self.from)
62    }
63
64    /// W3 applicability: unguarded rules always apply; a guarded rule
65    /// applies only when the caller selected one of its languages
66    /// (§5.1 — no selection, or an undeclared selection, leaves every
67    /// guard inert).
68    pub fn guard_applies(&self, language: Option<&str>) -> bool {
69        self.when.is_empty()
70            || language.is_some_and(|l| self.when.iter().any(|w| w == l))
71    }
72}
73
74#[derive(Debug, Clone)]
75pub enum StageItem {
76    /// parallel { ... } — single simultaneous pass.
77    Parallel(Vec<Sub>),
78    /// sequence { ... } rules and bare rules — sequential gsub passes
79    /// (the Ruby adapter flattens sequence blocks into the stage).
80    Sequential(Vec<Sub>),
81    /// run stage.x / run map.<dep>.stage.x
82    Run {
83        dep: Option<String>,
84        stage: String,
85        reverse_run: Option<bool>,
86    },
87    /// A stdlib function application.
88    Funcall(Funcall),
89}
90
91#[derive(Debug, Clone)]
92pub struct Stage {
93    pub name: String,
94    pub items: Vec<StageItem>,
95    /// Ruby `reverse_run:` at stage level (Group#reverse toggles it).
96    pub reverse_run: Option<bool>,
97}
98
99#[derive(Debug, Clone)]
100pub struct AliasDef {
101    pub name: String,
102    pub value: Item,
103}
104
105#[derive(Debug, Clone)]
106pub struct Dependency {
107    /// The located map name (`dependency "x"`).
108    pub target: String,
109    /// The `as` alias name; None = library import.
110    pub alias: Option<String>,
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct Tests {
115    pub data: Vec<(String, String)>,
116}
117
118/// A parsed, dependency-resolved map document.
119pub struct Document {
120    /// The map id (file stem; how `Interscript.transliterate` addresses it).
121    pub name: String,
122    pub system_code: String,
123    pub aliases: BTreeMap<String, Item>,
124    pub stages: BTreeMap<String, Stage>,
125    /// ISC-WF-1 W1: the map's named rulesets, fully use-expanded at
126    /// load time. Never executed directly (§3.2 — a ruleset is not a
127    /// stage); only `use` splices their items into a stage.
128    pub rulesets: BTreeMap<String, Stage>,
129    /// Aliased dependencies (stage-run targets), keyed by alias.
130    pub dep_aliases: BTreeMap<String, Rc<Document>>,
131    /// Library imports (unaliased dependencies), loaded documents.
132    pub imports: Vec<Rc<Document>>,
133    pub tests: Tests,
134    /// Raw metadata key/value text pairs (inert for transliteration).
135    pub metadata: Vec<(String, String)>,
136    /// ISC-WF-1 W3: the languages the map declares (`languages a b`).
137    pub languages: Vec<String>,
138}
139
140impl Document {
141    /// `Interscript::Node::Document#imported_aliases`: each import
142    /// dependency's own aliases merged UNDER the doc's own aliases.
143    pub fn imported_aliases(&self) -> BTreeMap<String, Item> {
144        let mut merged: BTreeMap<String, Item> = BTreeMap::new();
145        for dep in &self.imports {
146            for (k, v) in &dep.aliases {
147                merged.entry(k.clone()).or_insert_with(|| v.clone());
148            }
149        }
150        for (k, v) in &self.aliases {
151            merged.insert(k.clone(), v.clone());
152        }
153        merged
154    }
155
156    /// `imported_stages`: import deps' stages merged under own stages.
157    pub fn imported_stages(&self) -> BTreeMap<String, Stage> {
158        let mut merged: BTreeMap<String, Stage> = BTreeMap::new();
159        for dep in &self.imports {
160            for (k, v) in &dep.stages {
161                merged.entry(k.clone()).or_insert_with(|| v.clone());
162            }
163        }
164        for (k, v) in &self.stages {
165            merged.insert(k.clone(), v.clone());
166        }
167        merged
168    }
169
170    /// Look up an alias through the import chain (word context).
171    pub fn lookup_alias(&self, name: &str) -> Option<Item> {
172        self.imported_aliases().get(name).cloned()
173    }
174}