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 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}