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 rule-level applicability: unguarded rules always apply; a
65 /// guarded rule applies only when the selection matches one of its
66 /// `when` languages. The §5.1 declaration gate (the selection must
67 /// also be in `Document.languages`) is applied per document in
68 /// `execute_stage` before this is consulted.
69 pub fn guard_applies(&self, language: Option<&str>) -> bool {
70 self.when.is_empty()
71 || language.is_some_and(|l| self.when.iter().any(|w| w == l))
72 }
73}
74
75#[derive(Debug, Clone)]
76pub enum StageItem {
77 /// parallel { ... } — single simultaneous pass.
78 Parallel(Vec<Sub>),
79 /// sequence { ... } rules and bare rules — sequential gsub passes
80 /// (the Ruby adapter flattens sequence blocks into the stage).
81 Sequential(Vec<Sub>),
82 /// run stage.x / run map.<dep>.stage.x
83 Run {
84 dep: Option<String>,
85 stage: String,
86 reverse_run: Option<bool>,
87 },
88 /// A stdlib function application.
89 Funcall(Funcall),
90}
91
92#[derive(Debug, Clone)]
93pub struct Stage {
94 pub name: String,
95 pub items: Vec<StageItem>,
96 /// Ruby `reverse_run:` at stage level (Group#reverse toggles it).
97 pub reverse_run: Option<bool>,
98}
99
100#[derive(Debug, Clone)]
101pub struct AliasDef {
102 pub name: String,
103 pub value: Item,
104}
105
106#[derive(Debug, Clone)]
107pub struct Dependency {
108 /// The located map name (`dependency "x"`).
109 pub target: String,
110 /// The `as` alias name; None = library import.
111 pub alias: Option<String>,
112}
113
114#[derive(Debug, Clone, Default)]
115pub struct Tests {
116 pub data: Vec<(String, String)>,
117}
118
119/// A parsed, dependency-resolved map document.
120pub struct Document {
121 /// The map id (file stem; how `Interscript.transliterate` addresses it).
122 pub name: String,
123 pub system_code: String,
124 pub aliases: BTreeMap<String, Item>,
125 pub stages: BTreeMap<String, Stage>,
126 /// ISC-WF-1 W1: the map's named rulesets, fully use-expanded at
127 /// load time. Never executed directly (§3.2 — a ruleset is not a
128 /// stage); only `use` splices their items into a stage.
129 pub rulesets: BTreeMap<String, Stage>,
130 /// Aliased dependencies (stage-run targets), keyed by alias.
131 pub dep_aliases: BTreeMap<String, Rc<Document>>,
132 /// Library imports (unaliased dependencies), loaded documents.
133 pub imports: Vec<Rc<Document>>,
134 pub tests: Tests,
135 /// Raw metadata key/value text pairs (inert for transliteration).
136 pub metadata: Vec<(String, String)>,
137 /// ISC-WF-1 W3: the languages the map declares (`languages a b`).
138 pub languages: Vec<String>,
139}
140
141impl Document {
142 /// `Interscript::Node::Document#imported_aliases`: each import
143 /// dependency's own aliases merged UNDER the doc's own aliases.
144 pub fn imported_aliases(&self) -> BTreeMap<String, Item> {
145 let mut merged: BTreeMap<String, Item> = BTreeMap::new();
146 for dep in &self.imports {
147 for (k, v) in &dep.aliases {
148 merged.entry(k.clone()).or_insert_with(|| v.clone());
149 }
150 }
151 for (k, v) in &self.aliases {
152 merged.insert(k.clone(), v.clone());
153 }
154 merged
155 }
156
157 /// `imported_stages`: import deps' stages merged under own stages.
158 pub fn imported_stages(&self) -> BTreeMap<String, Stage> {
159 let mut merged: BTreeMap<String, Stage> = BTreeMap::new();
160 for dep in &self.imports {
161 for (k, v) in &dep.stages {
162 merged.entry(k.clone()).or_insert_with(|| v.clone());
163 }
164 }
165 for (k, v) in &self.stages {
166 merged.insert(k.clone(), v.clone());
167 }
168 merged
169 }
170
171 /// Look up an alias through the import chain (word context).
172 pub fn lookup_alias(&self, name: &str) -> Option<Item> {
173 self.imported_aliases().get(name).cloned()
174 }
175}