Skip to main content

interscript_rs/
interpreter.rs

1//! The interpreter: `Interscript::Interpreter::Stage` parity. A
2//! backtracking item matcher (the Ruby runtime's regex semantics
3//! without compiling to regex — Rust's regex crate has no lookaround,
4//! which the constraint semantics require) plus the stage executor
5//! over parallel groups, sequential sub rules, run rules and funcalls.
6//!
7//! Semantics contract (from interpreter.rb / stdlib.rb):
8//! * `before`     -> lookbehind  (?<=…)
9//! * `not_before` -> (?<!…)
10//! * `after`      -> lookahead   (?=…)
11//! * `not_after`  -> (?!…)
12//! * an EMPTY constraint item behaves like the empty regex in that
13//!   position: positive constraints become no-ops, negative ones
14//!   disable the rule (Ruby `(?<=)` always true / `(?<!)` never true).
15//! * parallel groups: single left-to-right pass. Ruby's two paths are
16//!   both mirrored (they disagree on duplicate `from` keys): the
17//!   tree path (`parallel_replace_tree`, constraint-free groups) is a
18//!   last-wins char trie with longest-match scanning; the megaregexp
19//!   fallback tries rules in `deterministic_sort_by_max_length`
20//!   order (-max_length*100000+idx), first match wins.
21//! * replacement `to` items collapse through `nth_string`
22//!   (Any -> first string) before building, exactly like build_item.
23
24use crate::document::{Document, StageItem, Sub, SubTo};
25use crate::error::{Error, Result};
26use crate::item::Item;
27use crate::stdlib::Prim;
28use std::cell::RefCell;
29use std::collections::{BTreeMap, BTreeSet};
30use std::path::PathBuf;
31use std::rc::Rc;
32
33/// A loaded-map cache bound to a load path. `Interscript.transliterate`
34/// parity: `transliterate(map_id, string)` runs stage `main`.
35pub struct Runtime {
36    load_path: Vec<PathBuf>,
37    cache: RefCell<BTreeMap<String, Rc<Document>>>,
38    /// Dependency-cycle guard.
39    loading: RefCell<Vec<String>>,
40}
41
42impl Runtime {
43    pub fn new(load_path: Vec<PathBuf>) -> Runtime {
44        Runtime {
45            load_path,
46            cache: RefCell::new(BTreeMap::new()),
47            loading: RefCell::new(Vec::new()),
48        }
49    }
50
51    /// Load (and dependency-close) a map document.
52    pub fn load(&self, map_id: &str) -> Result<Rc<Document>> {
53        if let Some(doc) = self.cache.borrow().get(map_id) {
54            return Ok(doc.clone());
55        }
56        {
57            let loading = self.loading.borrow();
58            if loading.iter().any(|m| m == map_id) {
59                let mut chain = loading.clone();
60                chain.push(map_id.to_string());
61                return Err(Error::DependencyCycle(chain));
62            }
63        }
64        self.loading.borrow_mut().push(map_id.to_string());
65        let result = self.load_uncached(map_id);
66        self.loading.borrow_mut().pop();
67        result
68    }
69
70    fn load_uncached(&self, map_id: &str) -> Result<Rc<Document>> {
71        let path = crate::locate::locate(&self.load_path, map_id)?;
72        let source = crate::locate::read_map(&path)?;
73        let stem = path
74            .file_stem()
75            .and_then(|s| s.to_str())
76            .unwrap_or(map_id)
77            .to_string();
78        let tree = crate::grammar::parse_isc(&source).map_err(|(offset, expected)| Error::Parse {
79            map: stem.clone(),
80            offset,
81            expected,
82        })?;
83
84        // Dependency documents load FIRST (NodeAdapter ordering).
85        let targets = crate::builder::dependency_targets(&tree);
86        let mut imports: Vec<Rc<Document>> = Vec::new();
87        let mut dep_docs: BTreeMap<String, Rc<Document>> = BTreeMap::new();
88        for (target, alias) in targets {
89            let dep = self.load(&target)?;
90            match alias {
91                Some(name) => {
92                    dep_docs.insert(name, dep);
93                }
94                None => imports.push(dep),
95            }
96        }
97
98        let doc = Rc::new(crate::builder::build(&tree, &stem, &imports, &dep_docs)?);
99        self.cache
100            .borrow_mut()
101            .insert(map_id.to_string(), doc.clone());
102        Ok(doc)
103    }
104
105    /// `Interscript.transliterate(map_id, string)`.
106    pub fn transliterate(&self, map_id: &str, input: &str) -> Result<String> {
107        self.transliterate_stage(map_id, "main", input, None)
108    }
109
110    /// ISC-WF-1 W3: transliterate with a language selection (§5.1).
111    /// Selecting a language the map does not declare behaves like no
112    /// selection (every `when` guard inert).
113    pub fn transliterate_lang(
114        &self,
115        map_id: &str,
116        input: &str,
117        language: Option<&str>,
118    ) -> Result<String> {
119        self.transliterate_stage(map_id, "main", input, language)
120    }
121
122    pub fn transliterate_stage(
123        &self,
124        map_id: &str,
125        stage: &str,
126        input: &str,
127        language: Option<&str>,
128    ) -> Result<String> {
129        let doc = self.load(map_id)?;
130        execute_stage(&doc, stage, input, language)
131    }
132
133    /// Run a document's embedded tests; returns (passed, failed, failures).
134    /// Embedded tests run WITHOUT a language selection (§5.1 default).
135    pub fn run_tests(&self, map_id: &str) -> Result<(usize, usize, Vec<String>)> {
136        let doc = self.load(map_id)?;
137        let mut passed = 0usize;
138        let mut failed = 0usize;
139        let mut failures = Vec::new();
140        for (input, expected) in &doc.tests.data {
141            match self.transliterate(map_id, input) {
142                Ok(actual) if &actual == expected => passed += 1,
143                Ok(actual) => {
144                    failed += 1;
145                    failures.push(format!("{input:?} -> {actual:?} (expected {expected:?})"));
146                }
147                Err(e) => {
148                    failed += 1;
149                    failures.push(format!("{input:?} -> ERROR {e}"));
150                }
151            }
152        }
153        Ok((passed, failed, failures))
154    }
155}
156
157// ----------------------------------------------------------------------
158// Stage execution
159// ----------------------------------------------------------------------
160
161/// Public alias for diagnostics and tests.
162pub fn execute_stage_pub(
163    doc: &Document,
164    stage_name: &str,
165    input: &str,
166    language: Option<&str>,
167) -> Result<String> {
168    execute_stage(doc, stage_name, input, language)
169}
170
171/// W3 (§5.2) pass filtering: the applicable rule set = unguarded rules
172/// ∪ guarded rules whose `when` matches the selected language — and a
173/// guard-applicable rule BEATS an unguarded rule with the same from
174/// (a language-specific value overrides the generic value without
175/// restating the generic row). With no selection every guard is inert,
176/// so this reduces to the unguarded set (§5.1).
177fn guard_filter<'a>(rules: &[&'a Sub], language: Option<&str>) -> Vec<&'a Sub> {
178    let applicable: Vec<&Sub> = rules
179        .iter()
180        .copied()
181        .filter(|r| r.guard_applies(language))
182        .collect();
183    let guarded_froms: BTreeSet<String> = applicable
184        .iter()
185        .filter(|r| !r.when.is_empty())
186        .map(|r| r.from_key())
187        .collect();
188    if guarded_froms.is_empty() {
189        return applicable;
190    }
191    // Guarded rows always stay; an UNGUARDED row drops when an
192    // applicable guarded row shares its from (§5.2 precedence).
193    applicable
194        .into_iter()
195        .filter(|r| !r.when.is_empty() || !guarded_froms.contains(&r.from_key()))
196        .collect()
197}
198
199fn execute_stage(
200    doc: &Document,
201    stage_name: &str,
202    input: &str,
203    language: Option<&str>,
204) -> Result<String> {
205    let stage = doc
206        .imported_stages()
207        .remove(stage_name)
208        .ok_or_else(|| Error::MapLogic(format!("stage {stage_name:?} not found in {}", doc.name)))?;
209    // Ruby execute_rule: `return if r.reverse_run == true` — a rule or
210    // stage carrying reverse_run == true is skipped entirely.
211    if stage.reverse_run == Some(true) {
212        return Ok(input.to_string());
213    }
214    let mut s = input.to_string();
215    for item in &stage.items {
216        // Ruby execute_rule top: reverse_run == true skips the rule.
217        let skipped = match item {
218            StageItem::Funcall(f) => f.reverse_run == Some(true),
219            StageItem::Run { reverse_run, .. } => *reverse_run == Some(true),
220            _ => false,
221        };
222        if skipped {
223            continue;
224        }
225        s = match item {
226            StageItem::Parallel(rules) => apply_parallel(doc, rules, &s, language)?,
227            StageItem::Sequential(rules) => {
228                let mut out = s;
229                for rule in rules {
230                    if rule.reverse_run == Some(true) {
231                        continue;
232                    }
233                    // W3 §5.2: a guarded rule whose guard is inert is
234                    // skipped entirely (behaves as if absent).
235                    if !rule.guard_applies(language) {
236                        continue;
237                    }
238                    out = apply_sub(doc, rule, &out)?;
239                }
240                out
241            }
242            StageItem::Run { dep, stage, .. } => match dep {
243                Some(alias) => {
244                    let dep_doc = doc.dep_aliases.get(alias).ok_or_else(|| {
245                        Error::MapLogic(format!("dep alias {alias:?} not found in {}", doc.name))
246                    })?;
247                    // W3 §5.3: the selection threads into dependency
248                    // stage runs (each document's own declaration
249                    // gates its guards).
250                    execute_stage(dep_doc, stage, &s, language)?
251                }
252                None => execute_stage(doc, stage, &s, language)?,
253            },
254            StageItem::Funcall(funcall) => funcall.name.apply(s, &funcall.kwargs)?,
255        };
256    }
257    Ok(s)
258}
259
260/// Single left-to-right simultaneous pass. Ruby has TWO paths and
261/// they disagree on duplicate `from` keys, so both are mirrored:
262///
263/// * tree path (`Stdlib.parallel_replace_tree`), used when every rule
264///   is constraint-free and builds to plain from-strings: a
265///   last-wins char-trie, longest match at each position;
266/// * megaregexp path (the rescue fallback): rules tried in
267///   `deterministic_sort_by_max_length` order (-max_length*100000+idx),
268///   first match wins.
269fn apply_parallel(doc: &Document, rules: &[Sub], s: &str, language: Option<&str>) -> Result<String> {
270    // Ruby: `next if i.reverse_run == true` in both the tree and the
271    // megaregexp branch — skipped rules join neither array.
272    let live: Vec<&Sub> = rules
273        .iter()
274        .filter(|r| r.reverse_run != Some(true))
275        .collect();
276    // W3 §5.2: the pass runs over the language-applicable set, with
277    // guard-applicable beating unguarded same-from.
278    let applicable = guard_filter(&live, language);
279    if let Some(tree) = build_tree(&applicable, doc)? {
280        return tree_replace(&tree, s);
281    }
282
283    let chars: Vec<char> = s.chars().collect();
284    // deterministic_sort_by_max_length: -max_length * 100000 + idx.
285    let mut order: Vec<usize> = (0..applicable.len()).collect();
286    order.sort_by_key(|&i| (std::cmp::Reverse(applicable[i].max_length()), i));
287
288    let mut out = String::with_capacity(s.len());
289    let mut copied = 0usize; // chars[..copied] are already emitted
290    let mut pos = 0usize;
291    while pos < chars.len() {
292        let mut matched: Option<(usize, String)> = None;
293        for &i in &order {
294            let rule = applicable[i];
295            let m = Matcher::new(&chars, doc);
296            if let Some((end, replacement)) = m.match_rule(rule, pos)? {
297                matched = Some((end, replacement));
298                break;
299            }
300        }
301        match matched {
302            Some((end, replacement)) => {
303                out.extend(&chars[copied..pos]);
304                out.push_str(&replacement);
305                copied = end;
306                // Ruby gsub: a zero-width match cannot loop — advance
307                // at least one char.
308                pos = std::cmp::max(end, pos + 1);
309            }
310            None => pos += 1,
311        }
312    }
313    out.extend(&chars[copied..]);
314    Ok(out)
315}
316
317/// A char-trie node with an optional terminal replacement.
318type Trie = std::collections::BTreeMap<char, TrieNode>;
319#[derive(Default)]
320struct TrieNode {
321    children: Trie,
322    replacement: Option<String>,
323}
324
325/// `Interscript::Stdlib.parallel_replace_compile_tree` parity: build
326/// the trie when the group is tree-eligible (no constraints; every
327/// from builds via build_item(:par) without raising; every to builds
328/// via build_item(:parstr)). Duplicate from-strings: later rules
329/// overwrite earlier terminals (hash assignment semantics).
330fn build_tree(rules: &[&Sub], doc: &Document) -> Result<Option<TrieNode>> {
331    let m = Matcher::new(&[], doc);
332    let mut map: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
333    for rule in rules {
334        if rule.before.is_some()
335            || rule.not_before.is_some()
336            || rule.after.is_some()
337            || rule.not_after.is_some()
338        {
339            return Ok(None);
340        }
341        let to = match &rule.to {
342            SubTo::Item(item) => match m.build_par_str(item) {
343                Ok(s) => s,
344                Err(_) => return Ok(None),
345            },
346            // to upcase/downcase inside a parallel group has no
347            // build_item case (nil replacement) — Ruby silently
348            // skips nothing here; the tree hash gets nil, matching
349            // `Array(nil)` → no keys. Treat as skip.
350            SubTo::Upcase | SubTo::Downcase => String::new(),
351        };
352        let _ = to;
353        match m.build_par_from(&rule.from) {
354            Ok(keys) => {
355                for key in keys {
356                    map.insert(key, to.clone());
357                }
358            }
359            Err(_) => return Ok(None),
360        }
361    }
362    let mut root = TrieNode::default();
363    for (from, to) in map {
364        let chars: Vec<char> = from.chars().collect();
365        if chars.is_empty() {
366            // The Ruby trie cannot represent an empty key (the scan
367            // only follows char edges), so it is unreachable there.
368            continue;
369        }
370        let mut node = &mut root;
371        for c in chars {
372            node = node.children.entry(c).or_default();
373        }
374        node.replacement = Some(to);
375    }
376    Ok(Some(root))
377}
378
379/// `parallel_replace_tree` parity: greedy longest-match scan.
380fn tree_replace(root: &TrieNode, s: &str) -> Result<String> {
381    let chars: Vec<char> = s.chars().collect();
382    let len = chars.len();
383    let mut out = String::with_capacity(s.len());
384    let mut i = 0usize;
385    while i < len {
386        let mut branch = &root.children;
387        let mut matched: Option<(usize, &String)> = None;
388        let mut j = 0usize;
389        while j < len - i {
390            let cc = chars[i + j];
391            match branch.get(&cc) {
392                Some(node) => {
393                    branch = &node.children;
394                    if let Some(repl) = &node.replacement {
395                        matched = Some((j + 1, repl));
396                    }
397                    j += 1;
398                }
399                None => break,
400            }
401        }
402        match matched {
403            Some((mlen, repl)) => {
404                out.push_str(repl);
405                i += mlen;
406            }
407            None => {
408                out.push(chars[i]);
409                i += 1;
410            }
411        }
412    }
413    Ok(out)
414}
415
416/// Sequential gsub pass for one sub rule.
417fn apply_sub(doc: &Document, rule: &Sub, s: &str) -> Result<String> {
418    let chars: Vec<char> = s.chars().collect();
419    let mut out = String::with_capacity(s.len());
420    let mut copied = 0usize; // chars[..copied] are already emitted
421    let mut pos = 0usize;
422    // A gsub pass attempts the pattern at every position INCLUDING the
423    // end (a zero-width pattern produces one final empty match).
424    while pos <= chars.len() {
425        let m = Matcher::new(&chars, doc);
426        match m.match_rule(rule, pos)? {
427            Some((end, replacement)) => {
428                out.extend(&chars[copied..pos]);
429                out.push_str(&replacement);
430                copied = end;
431                pos = std::cmp::max(end, pos + 1);
432            }
433            None => pos += 1,
434        }
435    }
436    out.extend(&chars[copied..]);
437    Ok(out)
438}
439
440// ----------------------------------------------------------------------
441// The matcher
442// ----------------------------------------------------------------------
443
444/// Per-rule match environment. Group slots are char-index ranges,
445/// 1-based (slot 0 unused), mirroring regex group numbering.
446struct Matcher<'a> {
447    chars: &'a [char],
448    doc: &'a Document,
449    groups: RefCell<Vec<Option<(usize, usize)>>>,
450    error: RefCell<Option<Error>>,
451}
452
453impl<'a> Matcher<'a> {
454    fn new(chars: &'a [char], doc: &'a Document) -> Matcher<'a> {
455        Matcher {
456            chars,
457            doc,
458            groups: RefCell::new(Vec::new()),
459            error: RefCell::new(None),
460        }
461    }
462
463    /// Match one sub rule at `pos`: constraints + from, then build the
464    /// replacement. Returns (end, replacement).
465    fn match_rule(&self, rule: &Sub, pos: usize) -> Result<Option<(usize, String)>> {
466        let slots = rule.group_count + 1;
467        self.groups.borrow_mut().resize_with(slots, || None);
468
469        // before -> (?<=…): some k <= pos where before ends at pos.
470        if let Some(before) = &rule.before {
471            if !self.lookbehind(before, pos)? {
472                return Ok(None);
473            }
474        }
475        // not_before -> (?<!…): the item must NOT match ending at pos.
476        // An empty item matches the empty string at every position, so
477        // `not_before ""` disables the rule — the Ruby (?<!) behavior.
478        if let Some(nb) = &rule.not_before {
479            if self.lookbehind(nb, pos)? {
480                return Ok(None);
481            }
482        }
483
484        let mut end_slot: Option<usize> = None;
485        let matched = {
486            let slot_ref = &mut end_slot;
487            self.match_item(&rule.from, pos, &mut |m, end| {
488                // not_after -> (?!…)
489                if let Some(na) = &rule.not_after {
490                    if m.lookahead(na, end)? {
491                        return Ok(false);
492                    }
493                }
494                // after -> (?=…)
495                if let Some(a) = &rule.after {
496                    if !m.lookahead(a, end)? {
497                        return Ok(false);
498                    }
499                }
500                *slot_ref = Some(end);
501                Ok(true)
502            })
503        };
504
505        if let Some(e) = self.error.borrow_mut().take() {
506            return Err(e);
507        }
508        if !matched {
509            return Ok(None);
510        }
511        let end = end_slot.unwrap_or(pos);
512
513        let replacement = match &rule.to {
514            SubTo::Upcase => self.chars[pos..end].iter().collect::<String>().to_uppercase(),
515            SubTo::Downcase => self.chars[pos..end].iter().collect::<String>().to_lowercase(),
516            SubTo::Item(to) => {
517                let collapsed = to.first_string();
518                let built = self.build_replacement(&collapsed)?;
519                self.decode_replacement(&built, pos, end)
520            }
521        };
522        Ok(Some((end, replacement)))
523    }
524
525    /// Positive lookbehind: does `item` match SOME span ending at pos?
526    fn lookbehind(&self, item: &Item, pos: usize) -> Result<bool> {
527        for start in (0..=pos).rev() {
528            let ok = self.match_item(item, start, &mut |_m, end| Ok(end == pos));
529            if let Some(e) = self.error.borrow_mut().take() {
530                return Err(e);
531            }
532            if ok {
533                return Ok(true);
534            }
535        }
536        Ok(false)
537    }
538
539    /// Lookahead: does `item` match starting at pos (any length)?
540    fn lookahead(&self, item: &Item, pos: usize) -> Result<bool> {
541        let ok = self.match_item(item, pos, &mut |_m, _end| Ok(true));
542        if let Some(e) = self.error.borrow_mut().take() {
543            return Err(e);
544        }
545        Ok(ok)
546    }
547
548    /// CPS backtracking matcher: match `item` at `pos`, calling `k`
549    /// with each candidate end; success when any continuation accepts.
550    fn match_item(
551        &self,
552        item: &Item,
553        pos: usize,
554        k: &mut dyn FnMut(&Matcher, usize) -> Result<bool>,
555    ) -> bool {
556        match item {
557            Item::None => self.call(k, pos),
558            Item::Str(s) => {
559                let pat: Vec<char> = s.chars().collect();
560                if pos + pat.len() <= self.chars.len()
561                    && self.chars[pos..pos + pat.len()] == pat[..]
562                {
563                    self.call(k, pos + pat.len())
564                } else {
565                    false
566                }
567            }
568            Item::Prim(prim) => match prim.matches_zero_width(self.chars, pos) {
569                Some(ok) => ok && self.call(k, pos),
570                None => {
571                    if pos < self.chars.len() && prim.matches_char(self.chars[pos]).unwrap_or(false)
572                    {
573                        self.call(k, pos + 1)
574                    } else {
575                        false
576                    }
577                }
578            },
579            Item::Concat(parts) => self.match_seq(parts, pos, k),
580            Item::Set(entries) => {
581                for entry in entries {
582                    if self.match_item(entry, pos, k) {
583                        return true;
584                    }
585                }
586                false
587            }
588            Item::Range(lo, hi) => {
589                if pos < self.chars.len() && *lo <= self.chars[pos] && self.chars[pos] <= *hi {
590                    self.call(k, pos + 1)
591                } else {
592                    false
593                }
594            }
595            Item::Maybe(inner) => {
596                let extended = self.match_item(inner, pos, k);
597                if extended {
598                    return true;
599                }
600                self.call(k, pos)
601            }
602            Item::Some(inner) => self.match_some(inner, pos, 1, k),
603            Item::CaptureGroup { id, inner } => {
604                let snapshot = self.groups.borrow().clone();
605                let id = *id;
606                let ok = self.match_item(inner, pos, &mut |m, end| {
607                    m.groups.borrow_mut()[id] = Some((pos, end));
608                    match k(m, end) {
609                        Ok(true) => Ok(true),
610                        failed => {
611                            *m.groups.borrow_mut() = snapshot.clone();
612                            failed
613                        }
614                    }
615                });
616                if !ok {
617                    *self.groups.borrow_mut() = snapshot;
618                }
619                ok
620            }
621            Item::CaptureRef(n) => {
622                let text = self
623                    .groups
624                    .borrow()
625                    .get(*n)
626                    .and_then(|g| g.as_ref())
627                    .map(|(s, e)| self.chars[*s..*e].iter().collect::<String>());
628                match text {
629                    Some(t) => {
630                        let pat: Vec<char> = t.chars().collect();
631                        if pos + pat.len() <= self.chars.len()
632                            && self.chars[pos..pos + pat.len()] == pat[..]
633                        {
634                            self.call(k, pos + pat.len())
635                        } else {
636                            false
637                        }
638                    }
639                    // Ruby \N with an unparticipated group fails to match.
640                    None => false,
641                }
642            }
643            Item::Alias { map, name } => match self.resolve_alias(map.as_deref(), name) {
644                Ok(Some(resolved)) => self.match_item(&resolved, pos, k),
645                Ok(None) => false,
646                Err(e) => {
647                    self.record_error(e);
648                    false
649                }
650            },
651            Item::Func(_) => {
652                self.record_error(Error::MapLogic("function item in match position".into()));
653                false
654            }
655        }
656    }
657
658    fn match_seq(
659        &self,
660        parts: &[Item],
661        pos: usize,
662        k: &mut dyn FnMut(&Matcher, usize) -> Result<bool>,
663    ) -> bool {
664        match parts.split_first() {
665            None => self.call(k, pos),
666            Some((first, rest)) => {
667                let rest: Vec<Item> = rest.to_vec();
668                self.match_item(first, pos, &mut |m, end| Ok(m.match_seq(&rest, end, k)))
669            }
670        }
671    }
672
673    fn match_some(
674        &self,
675        inner: &Item,
676        pos: usize,
677        count: usize,
678        k: &mut dyn FnMut(&Matcher, usize) -> Result<bool>,
679    ) -> bool {
680        // Greedy: extend first (one more inner match), then fall back
681        // to the continuation. Zero-width inner matches stop the loop
682        // (regex empty-loop guard).
683        let extended = self.match_item(inner, pos, &mut |m, end| {
684            if end > pos {
685                Ok(m.match_some(inner, end, count + 1, k))
686            } else {
687                Ok(false)
688            }
689        });
690        if extended {
691            return true;
692        }
693        if count >= 1 {
694            self.call(k, pos)
695        } else {
696            false
697        }
698    }
699
700    fn call(&self, k: &mut dyn FnMut(&Matcher, usize) -> Result<bool>, pos: usize) -> bool {
701        match k(self, pos) {
702            Ok(true) => true,
703            Ok(false) => false,
704            Err(e) => {
705                self.record_error(e);
706                false
707            }
708        }
709    }
710
711    fn record_error(&self, e: Error) {
712        let mut slot = self.error.borrow_mut();
713        if slot.is_none() {
714            *slot = Some(e);
715        }
716    }
717
718    fn resolve_alias(&self, map: Option<&str>, name: &str) -> Result<Option<Item>> {
719        // Stdlib primitives (checked after dep maps, like Ruby: the
720        // `i.map` branch comes first, stdlib second, local last).
721        if map.is_none() {
722            if let Some(prim) = Prim::from_name(name) {
723                return Ok(Some(Item::Prim(prim)));
724            }
725        }
726        match map {
727            Some(alias) => {
728                let doc = self
729                    .doc
730                    .dep_aliases
731                    .get(alias)
732                    .ok_or_else(|| Error::MapLogic(format!("dep alias {alias:?} not found")))?;
733                doc.lookup_alias(name).map(Some).ok_or_else(|| {
734                    Error::MapLogic(format!("alias {name:?} of dep {alias:?} not found"))
735                })
736            }
737            None => self.doc.lookup_alias(name).map(Some).ok_or_else(|| {
738                Error::MapLogic(format!("alias {name:?} not found in {}", self.doc.name))
739            }),
740        }
741    }
742
743    /// build_item(to, :str) semantics for the collapsed replacement.
744    fn build_replacement(&self, item: &Item) -> Result<String> {
745        match item {
746            Item::Str(s) => Ok(s.clone()),
747            Item::None => Ok(String::new()),
748            Item::Concat(parts) => {
749                let mut out = String::new();
750                for p in parts {
751                    out.push_str(&self.build_replacement(p)?);
752                }
753                Ok(out)
754            }
755            Item::CaptureRef(n) => Ok(format!("\\{n}")),
756            // Maybe/Some/CaptureGroup in :str build their data plus a
757            // literal suffix — the Ruby replacement-string quirk.
758            Item::Maybe(inner) => Ok(format!("{}?", self.build_replacement(inner)?)),
759            Item::Some(inner) => Ok(format!("{}+", self.build_replacement(inner)?)),
760            Item::CaptureGroup { inner, .. } => {
761                Ok(format!("({})", self.build_replacement(inner)?))
762            }
763            Item::Prim(prim) => match prim {
764                Prim::None_ => Ok(String::new()),
765                Prim::Space => Ok(" ".to_string()),
766                other => Err(Error::MapLogic(format!(
767                    "can't use {} in a string context",
768                    other.name()
769                ))),
770            },
771            Item::Alias { map, name } => {
772                if map.is_none() {
773                    if let Some(prim) = Prim::from_name(name) {
774                        return self.build_replacement(&Item::Prim(prim));
775                    }
776                }
777                match self.resolve_alias(map.as_deref(), name)? {
778                    Some(resolved) => self.build_replacement(&resolved),
779                    None => Err(Error::MapLogic(format!("alias {name:?} not found"))),
780                }
781            }
782            // nth_string collapse already handled Any; a residual
783            // Set/Range in replacement position is the Ruby
784            // SystemConversionError ("Can't use Any in a string context").
785            Item::Set(_) | Item::Range(_, _) => {
786                Err(Error::MapLogic("can't use Any in a string context".into()))
787            }
788            Item::Func(_) => Err(Error::MapLogic(
789                "function item in replacement position".into(),
790            )),
791        }
792    }
793
794    /// Ruby gsub replacement-string decoding over the built string:
795    /// \1..\9 group refs, \0 whole match, \\ literal backslash,
796    /// \& whole match; unknown escapes drop the backslash.
797    fn decode_replacement(&self, built: &str, start: usize, end: usize) -> String {        let chars: Vec<char> = built.chars().collect();
798        let mut out = String::with_capacity(built.len());
799        let mut i = 0usize;
800        while i < chars.len() {
801            if chars[i] == '\\' && i + 1 < chars.len() {
802                let next = chars[i + 1];
803                match next {
804                    '0' | '1'..='9' => {
805                        let n = next.to_digit(10).unwrap() as usize;
806                        if n == 0 {
807                            out.extend(&self.chars[start..end]);
808                        } else if let Some(Some((gs, ge))) = self.groups.borrow().get(n) {
809                            out.extend(&self.chars[*gs..*ge]);
810                        }
811                        i += 2;
812                        continue;
813                    }
814                    '\\' => {
815                        out.push('\\');
816                        i += 2;
817                        continue;
818                    }
819                    '&' => {
820                        out.extend(&self.chars[start..end]);
821                        i += 2;
822                        continue;
823                    }
824                    _ => {
825                        // Ruby drops the backslash on unknown escapes.
826                        out.push(next);
827                        i += 2;
828                        continue;
829                    }
830                }
831            }
832            out.push(chars[i]);
833            i += 1;
834        }
835        out
836    }
837}
838
839impl<'a> Matcher<'a> {
840    // ------------------------------------------------------------------
841    // build_item(:par) / build_item(:parstr) parity — the tree-path
842    // eligibility check for parallel groups. Errors mirror the Ruby
843    // SystemConversionError raises that force the megaregexp fallback.
844    // ------------------------------------------------------------------
845
846    /// build_item(item, :par): the key strings this `from` contributes
847    /// to the parallel tree. `Ok(vec![])` mirrors Ruby's silent nil
848    /// (function items); `Err` forces the megaregexp path.
849    fn build_par_from(&self, item: &Item) -> Result<Vec<String>> {
850        self.build_par_from_depth(item, 0)
851    }
852
853    fn build_par_from_depth(&self, item: &Item, depth: usize) -> Result<Vec<String>> {
854        if depth > 64 {
855            return Err(Error::MapLogic("alias expansion too deep".into()));
856        }
857        match item {
858            Item::Str(s) => Ok(vec![s.clone()]),
859            Item::None => Ok(vec![String::new()]),
860            Item::Prim(prim) => match prim {
861                Prim::None_ => Ok(vec![String::new()]),
862                Prim::Space => Ok(vec![" ".to_string()]),
863                other => Err(Error::MapLogic(format!(
864                    "can't use {} in a par context",
865                    other.name()
866                ))),
867            },
868            // Any(String/Range) -> one key per char; Any(Array) ->
869            // one key per entry. Nested Any entries produce nested
870            // arrays in Ruby (NoMethodError on `.chars`) -> fallback.
871            Item::Range(lo, hi) => {
872                let mut keys = Vec::new();
873                let mut c = *lo;
874                loop {
875                    keys.push(c.to_string());
876                    if c == *hi {
877                        break;
878                    }
879                    match char::from_u32(c as u32 + 1) {
880                        Some(n) => c = n,
881                        None => break,
882                    }
883                }
884                Ok(keys)
885            }
886            Item::Set(entries) => {
887                let mut keys = Vec::new();
888                for entry in entries {
889                    match entry {
890                        Item::Str(s) => keys.push(s.clone()),
891                        // Nested Any / Range / Alias entries become
892                        // arrays in the Ruby build -> raise -> fallback.
893                        _ => {
894                            return Err(Error::MapLogic(
895                                "can't parallelize nested any() entries".into(),
896                            ))
897                        }
898                    }
899                }
900                Ok(keys)
901            }
902            // Group in :par is the cartesian product of its children.
903            Item::Concat(parts) => {
904                let mut acc: Vec<String> = vec![String::new()];
905                for part in parts {
906                    let next = self.build_par_from_depth(part, depth)?;
907                    let mut joined = Vec::new();
908                    for a in &acc {
909                        for b in &next {
910                            joined.push(format!("{a}{b}"));
911                        }
912                    }
913                    acc = joined;
914                }
915                Ok(acc)
916            }
917            Item::Alias { map, name } => {
918                if map.is_none() {
919                    if let Some(prim) = Prim::from_name(name) {
920                        return self.build_par_from_depth(&Item::Prim(prim), depth);
921                    }
922                }
923                match self.resolve_alias(map.as_deref(), name)? {
924                    Some(resolved) => self.build_par_from_depth(&resolved, depth + 1),
925                    None => Ok(vec![]),
926                }
927            }
928            // Maybe/Some/CaptureGroup/CaptureRef raise in :par.
929            Item::Maybe(_) | Item::Some(_) => Err(Error::MapLogic(
930                "can't use a MaybeSome in a par context".into(),
931            )),
932            Item::CaptureGroup { .. } => Err(Error::MapLogic(
933                "can't use a CaptureGroup in a par context".into(),
934            )),
935            Item::CaptureRef(_) => Err(Error::MapLogic(
936                "Can't use CaptureRef in parallel mode".into(),
937            )),
938            // Function items have no build_item case -> nil -> no keys.
939            Item::Func(_) => Ok(vec![]),
940        }
941    }
942
943    /// build_item(item, :parstr): nth_string collapse, then the :par
944    /// string build. `to` in a parallel group must yield ONE string.
945    fn build_par_str(&self, item: &Item) -> Result<String> {
946        let collapsed = item.first_string();
947        self.build_par_str_inner(&collapsed, 0)
948    }
949
950    fn build_par_str_inner(&self, item: &Item, depth: usize) -> Result<String> {
951        if depth > 64 {
952            return Err(Error::MapLogic("alias expansion too deep".into()));
953        }
954        match item {
955            Item::Str(s) => Ok(s.clone()),
956            Item::None => Ok(String::new()),
957            Item::Prim(prim) => match prim {
958                Prim::None_ => Ok(String::new()),
959                Prim::Space => Ok(" ".to_string()),
960                other => Err(Error::MapLogic(format!(
961                    "can't use {} in a par context",
962                    other.name()
963                ))),
964            },
965            Item::Concat(parts) => {
966                let mut out = String::new();
967                for p in parts {
968                    out.push_str(&self.build_par_str_inner(p, depth)?);
969                }
970                Ok(out)
971            }
972            Item::Alias { map, name } => {
973                if map.is_none() {
974                    if let Some(prim) = Prim::from_name(name) {
975                        return self.build_par_str_inner(&Item::Prim(prim), depth);
976                    }
977                }
978                match self.resolve_alias(map.as_deref(), name)? {
979                    Some(resolved) => self.build_par_str_inner(&resolved, depth + 1),
980                    None => Err(Error::MapLogic(format!("alias {name:?} not found"))),
981                }
982            }
983            // A residual Any after the nth_string collapse yields an
984            // array in Ruby (not a replacement string) -> broken map.
985            Item::Set(_) | Item::Range(_, _) => Err(Error::MapLogic(
986                "parallel replacement is not a single string".into(),
987            )),
988            Item::Maybe(_) | Item::Some(_) => Err(Error::MapLogic(
989                "can't use a MaybeSome in a par context".into(),
990            )),
991            Item::CaptureGroup { .. } => Err(Error::MapLogic(
992                "can't use a CaptureGroup in a par context".into(),
993            )),
994            Item::CaptureRef(_) => Err(Error::MapLogic(
995                "Can't use CaptureRef in parallel mode".into(),
996            )),
997            Item::Func(_) => Err(Error::MapLogic(
998                "function item in a par context".into(),
999            )),
1000        }
1001    }
1002}