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}