1use crate::document::{Dependency, Document, Stage, StageItem, Sub, SubTo, Tests};
16use crate::error::{Error, Result};
17use crate::item::{FuncName, Item};
18use crate::stdlib::{Funcall, FuncallName, Prim};
19use crate::tree::{leaf_text, pget, TNode};
20use std::cell::RefCell;
21use std::collections::{BTreeMap, BTreeSet};
22use std::rc::Rc;
23
24pub fn dependency_targets(tree: &TNode) -> Vec<(String, Option<String>)> {
27 let system = tree.get("system").unwrap_or(tree);
28 let mut out = Vec::new();
29 let Some(body) = system.get("body") else { return out };
30 for block in body.ruby_array() {
31 if let Some(pair) = block.get("target") {
32 let target = unquote(pair);
33 let alias = block.get("alias").map(ident);
34 out.push((target, alias));
35 }
36 }
37 out
38}
39
40pub fn build(
41 tree: &TNode,
42 filename_stem: &str,
43 imports: &[Rc<Document>],
44 dep_docs: &BTreeMap<String, Rc<Document>>,
45) -> Result<Document> {
46 let system = tree.get("system").unwrap_or(tree);
47 let system_code = system.get("system_code").map(unquote).unwrap_or_default();
48
49 let body: Vec<&TNode> = match system.get("body") {
50 Some(b) => b.ruby_array().iter().collect(),
51 None => Vec::new(),
52 };
53
54 let mut metadata = Vec::new();
56 let mut alias_nodes: Vec<(String, TNode)> = Vec::new();
57 let mut tests = Tests::default();
58 let mut stage_nodes: Vec<(String, &TNode)> = Vec::new();
59 let mut dependencies: Vec<Dependency> = Vec::new();
60 let mut ruleset_nodes: Vec<(String, &TNode)> = Vec::new();
62 let mut languages: Vec<String> = Vec::new();
63
64 for block in &body {
65 let Some(hash) = block.ruby_array_hash() else { continue };
66 if let Some(fields) = pget(hash, "metadata") {
67 extract_metadata(fields, &mut metadata);
68 } else if let Some(arr) = pget(hash, "aliases") {
69 for a in arr.ruby_array() {
70 if let Some(name_node) = a.get("name") {
71 let name = ident(name_node);
72 let value = a.get("value").cloned().unwrap_or(TNode::Nil);
73 alias_nodes.push((name, value));
74 }
75 }
76 } else if let Some(arr) = pget(hash, "tests") {
77 for t in arr.ruby_array() {
78 let input = t.get("input").map(unquote).unwrap_or_default();
79 let expected = t.get("expected").map(unquote).unwrap_or_default();
80 tests.data.push((input, expected));
81 }
82 } else if let (Some(name_node), Some(stage_node)) = (pget(hash, "stage_name"), pget(hash, "stage")) {
83 stage_nodes.push((ident(name_node), stage_node));
84 } else if let (Some(name_node), Some(rs_node)) = (pget(hash, "ruleset_name"), pget(hash, "ruleset")) {
85 ruleset_nodes.push((ident(name_node), rs_node));
86 } else if let Some(lang_list) = pget(hash, "language") {
87 for l in lang_list.ruby_array() {
88 languages.push(ident(l));
89 }
90 } else if let Some(t) = pget(hash, "target") {
91 let target = unquote(t);
92 let alias = pget(hash, "alias").map(ident);
93 dependencies.push(Dependency { target, alias });
94 }
95 }
96
97 {
100 let mut seen = BTreeSet::new();
101 for (name, _) in &ruleset_nodes {
102 if !seen.insert(name.clone()) {
103 return Err(Error::MapLogic(format!(
104 "ruleset {name:?} declared more than once in {filename_stem}"
105 )));
106 }
107 }
108 }
109
110 let ctx = Ctx {
113 imported: {
114 let mut m = BTreeMap::new();
115 for doc in imports {
116 for (k, v) in &doc.aliases {
117 m.entry(k.clone()).or_insert_with(|| v.clone());
118 }
119 }
120 m
121 },
122 local_raw: alias_nodes.clone(),
123 dep_docs,
124 ruleset_raw: ruleset_nodes,
125 ruleset_memo: RefCell::new(BTreeMap::new()),
126 ruleset_stack: RefCell::new(Vec::new()),
127 };
128
129 for (name, _) in &ctx.ruleset_raw {
134 ctx.local_ruleset(name)?;
135 }
136 let rulesets: BTreeMap<String, Stage> = ctx.ruleset_memo.borrow().clone();
137
138 {
141 let mut seen = BTreeSet::new();
142 languages.retain(|l| seen.insert(l.clone()));
143 }
144
145 let mut aliases = BTreeMap::new();
147 for (name, value) in &alias_nodes {
148 aliases.insert(name.clone(), ctx.materialize(value)?);
149 }
150
151 let mut stages = BTreeMap::new();
152 for (name, node) in stage_nodes {
153 let stage = ctx.extract_stage(&name, node)?;
154 stages.insert(stage.name.clone(), stage);
155 }
156
157 Ok(Document {
158 name: filename_stem.to_string(),
159 system_code,
160 aliases,
161 stages,
162 rulesets,
163 dep_aliases: dep_docs
164 .iter()
165 .filter_map(|(k, v)| {
166 dependencies
167 .iter()
168 .find(|d| d.alias.as_deref() == Some(k.as_str()))
169 .map(|_| (k.clone(), v.clone()))
170 })
171 .collect(),
172 imports: imports.to_vec(),
173 tests,
174 metadata,
175 languages,
176 })
177}
178
179struct Ctx<'a> {
180 imported: BTreeMap<String, Item>,
182 local_raw: Vec<(String, TNode)>,
184 dep_docs: &'a BTreeMap<String, Rc<Document>>,
186 ruleset_raw: Vec<(String, &'a TNode)>,
188 ruleset_memo: RefCell<BTreeMap<String, Stage>>,
191 ruleset_stack: RefCell<Vec<String>>,
194}
195
196impl<'a> Ctx<'a> {
197 fn extract_stage(&self, name: &str, node: &TNode) -> Result<Stage> {
202 let items = self.extract_stage_items(node)?;
203 Ok(Stage { name: name.to_string(), items, reverse_run: None })
204 }
205
206 fn extract_stage_items(&self, node: &TNode) -> Result<Vec<StageItem>> {
210 let mut items: Vec<StageItem> = Vec::new();
211 for entry in node.ruby_array() {
212 let Some(hash) = entry.ruby_array_hash() else { continue };
213 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
214 if keys.contains(&"use_map") {
215 items.extend(self.resolve_use_items(hash)?);
217 } else if keys.contains(&"sequence") {
218 let node = pget(hash, "sequence").unwrap();
219 items.push(StageItem::Sequential(self.extract_rule_list(node)?));
220 } else if keys.contains(&"parallel") {
221 let node = pget(hash, "parallel").unwrap();
222 items.push(StageItem::Parallel(self.extract_rule_list(node)?));
223 } else if keys.contains(&"separate") {
224 let separator = pget(hash, "separator")
225 .map(|v| self.materialize(v))
226 .transpose()?
227 .map(|item| render_str(&item.first_string()));
228 items.push(StageItem::Funcall(Funcall {
229 name: FuncallName::Separate,
230 kwargs: separator
231 .map(|s| vec![("separator".to_string(), s)])
232 .unwrap_or_default(),
233 reverse_run: None,
234 }));
235 } else if keys.contains(&"compose") {
236 let op = leaf_text(pget(hash, "compose").unwrap());
237 let name = if op == "decompose" {
238 FuncallName::Decompose
239 } else {
240 FuncallName::Compose
241 };
242 items.push(StageItem::Funcall(Funcall { name, kwargs: vec![], reverse_run: None }));
243 } else if keys.contains(&"case") {
244 let op = leaf_text(pget(hash, "case").unwrap());
245 let name = match op.as_str() {
246 "upcase" => FuncallName::Upcase,
247 "downcase" => FuncallName::Downcase,
248 "title_case" => FuncallName::TitleCase,
249 other => return Err(Error::MapLogic(format!("unknown case op {other:?}"))),
250 };
251 let kwargs = pget(hash, "case_kwargs").map(extract_kwargs).unwrap_or_default();
252 items.push(StageItem::Funcall(Funcall { name, kwargs, reverse_run: None }));
253 } else if keys.contains(&"funcall_name") {
254 let fname = leaf_text(pget(hash, "funcall_name").unwrap());
255 let kwargs = pget(hash, "funcall_kwargs").map(extract_kwargs).unwrap_or_default();
256 let fname_enum = FuncallName::from_name(&fname)
257 .ok_or_else(|| crate::stdlib::unsupported_funcall(&fname))?;
258 items.push(StageItem::Funcall(Funcall { name: fname_enum, kwargs, reverse_run: None }));
259 } else if keys.contains(&"dep") {
260 let dep = ident(pget(hash, "dep").unwrap());
261 let stage = ident(pget(hash, "stage").unwrap());
262 items.push(StageItem::Run {
263 dep: Some(dep),
264 stage,
265 reverse_run: None,
266 });
267 } else if keys.contains(&"run_stage_only") {
268 let stage = pget(hash, "run_stage_only")
269 .and_then(|v| v.get("stage"))
270 .map(ident)
271 .unwrap_or_default();
272 items.push(StageItem::Run { dep: None, stage, reverse_run: None });
273 } else if keys.contains(&"bare_rule") {
274 let rule = pget(hash, "bare_rule").unwrap();
275 items.push(StageItem::Sequential(vec![self.extract_rule(rule)?]));
276 }
277 }
279 Ok(items)
280 }
281
282 fn resolve_use_stage(&self, hash: &[(String, TNode)]) -> Result<Stage> {
289 let use_map = pget(hash, "use_map")
290 .map(ident)
291 .ok_or_else(|| Error::MapLogic("use directive without a target".into()))?;
292 match pget(hash, "use_ruleset") {
293 Some(rs_node) => {
294 let rs = ident(rs_node);
295 let doc = self.dep_docs.get(&use_map).ok_or_else(|| {
296 Error::MapLogic(format!("dep alias {use_map:?} not found"))
297 })?;
298 doc.rulesets.get(&rs).cloned().ok_or_else(|| {
299 Error::MapLogic(format!("ruleset {rs:?} of dep {use_map:?} not found"))
300 })
301 }
302 None => self.local_ruleset(&use_map),
303 }
304 }
305
306 fn local_ruleset(&self, name: &str) -> Result<Stage> {
309 if let Some(built) = self.ruleset_memo.borrow().get(name) {
310 return Ok(built.clone());
311 }
312 {
313 let stack = self.ruleset_stack.borrow();
314 if stack.iter().any(|n| n == name) {
315 let mut chain = stack.clone();
316 chain.push(name.to_string());
317 return Err(Error::MapLogic(format!(
318 "use cycle: {}",
319 chain.join(" -> ")
320 )));
321 }
322 }
323 let node = self
324 .ruleset_raw
325 .iter()
326 .find(|(n, _)| n.as_str() == name)
327 .map(|(_, node)| *node)
328 .ok_or_else(|| Error::MapLogic(format!("ruleset {name:?} not found")))?;
329 self.ruleset_stack.borrow_mut().push(name.to_string());
330 let expanded = self.extract_stage_items(node);
331 self.ruleset_stack.borrow_mut().pop();
332 let stage = Stage {
333 name: name.to_string(),
334 items: expanded?,
335 reverse_run: None,
336 };
337 self.ruleset_memo
338 .borrow_mut()
339 .insert(name.to_string(), stage.clone());
340 Ok(stage)
341 }
342
343 fn resolve_use_items(&self, hash: &[(String, TNode)]) -> Result<Vec<StageItem>> {
347 let mut stage = self.resolve_use_stage(hash)?;
348 if let Some(orules) = pget(hash, "override_rules") {
349 let overrides = self.extract_rule_list(orules)?;
350 stage.items = merge_override_items(stage.items, overrides)?;
351 }
352 Ok(stage.items)
353 }
354
355 fn resolve_use_rules(&self, hash: &[(String, TNode)]) -> Result<Vec<Sub>> {
360 let stage = self.resolve_use_stage(hash)?;
361 let mut rules = Vec::new();
362 flatten_rules(&stage.items, &mut rules)?;
363 if let Some(orules) = pget(hash, "override_rules") {
364 let overrides = self.extract_rule_list(orules)?;
365 rules = merge_override_rules(rules, overrides);
366 }
367 Ok(rules)
368 }
369
370 fn extract_rule_list(&self, node: &TNode) -> Result<Vec<Sub>> {
371 let mut rules = Vec::new();
372 for r in node.ruby_array() {
373 if r.has_key("use_map") {
378 let Some(hash) = r.ruby_array_hash() else { continue };
379 rules.extend(self.resolve_use_rules(hash)?);
380 continue;
381 }
382 if is_none_capture(r.get("from")) && is_none_capture(r.get("to")) {
384 continue;
385 }
386 rules.push(self.extract_rule(r)?);
387 }
388 Ok(rules)
389 }
390
391 fn extract_rule(&self, r: &TNode) -> Result<Sub> {
392 let from = match r.get("from") {
393 Some(n) => self.materialize(n)?,
394 None => Item::None,
395 };
396 let to = match r.get("to") {
397 Some(n) => self.materialize(n)?,
398 None => Item::None,
399 };
400
401 let mut before = None;
402 let mut not_before = None;
403 let mut after = None;
404 let mut not_after = None;
405
406 if let Some(constraints) = r.get("constraints") {
407 for c in constraints.ruby_array() {
408 let Some(hash) = c.ruby_array_hash() else { continue };
409 for (kind, item_node) in hash.iter() {
410 let target = match kind.as_str() {
411 "before" => &mut before,
412 "not_before" => &mut not_before,
413 "after" => &mut after,
414 "not_after" => &mut not_after,
415 _ => continue,
416 };
417 *target = Some(self.materialize_class(item_node)?);
420 }
421 }
422 }
423
424 let to_target = match &to {
427 Item::Func(FuncName::Upcase) => SubTo::Upcase,
428 Item::Func(FuncName::Downcase) => SubTo::Downcase,
429 item => SubTo::Item(item.clone()),
430 };
431
432 let mut when = Vec::new();
435 if let Some(wc) = r.get("when_clause") {
436 if let Some(langs) = wc.get("when_language") {
437 for l in langs.ruby_array() {
438 when.push(ident(l));
439 }
440 }
441 }
442
443 let mut sub = Sub {
444 from,
445 to: to_target,
446 before,
447 not_before,
448 after,
449 not_after,
450 group_count: 0,
451 reverse_run: None,
452 when,
453 };
454 sub.from = self.expand_aliases(&sub.from, 0)?;
459 for c in [&mut sub.before, &mut sub.not_before] {
460 if let Some(i) = c {
461 *i = self.expand_aliases(i, 0)?;
462 }
463 }
464 for c in [&mut sub.not_after, &mut sub.after] {
466 if let Some(i) = c {
467 *i = self.expand_aliases(i, 0)?;
468 }
469 }
470 sub.group_count = assign_group_ids(&mut sub);
471 Ok(sub)
472 }
473
474 fn expand_aliases(&self, item: &Item, depth: usize) -> Result<Item> {
480 if depth > 32 {
481 return Err(Error::MapLogic(
482 "alias expansion too deep (alias cycle?)".into(),
483 ));
484 }
485 Ok(match item {
486 Item::Alias { map, name } => {
487 if map.is_some() {
488 let alias = map.clone().unwrap();
490 let doc = self
491 .dep_docs
492 .get(&alias)
493 .ok_or_else(|| Error::MapLogic(format!("dep alias {alias:?} not found")))?;
494 match doc.lookup_alias(name) {
495 Some(resolved) => self.expand_aliases(&resolved, depth + 1)?,
496 None => {
497 return Err(Error::MapLogic(format!(
498 "alias {name:?} of dep {alias:?} not found"
499 )))
500 }
501 }
502 } else if let Some(prim) = Prim::from_name(name) {
503 Item::Prim(prim)
504 } else if let Some(data) = self.imported.get(name) {
505 self.expand_aliases(data, depth + 1)?
506 } else if let Some((_, raw)) = self
507 .local_raw
508 .iter()
509 .find(|(n, _)| n.as_str() == name)
510 {
511 let resolved = self.materialize(raw)?;
512 self.expand_aliases(&resolved, depth + 1)?
513 } else {
514 return Err(Error::MapLogic(format!(
515 "alias {name:?} not found during expansion"
516 )));
517 }
518 }
519 Item::CaptureGroup { id, inner } => Item::CaptureGroup {
520 id: *id,
521 inner: Box::new(self.expand_aliases(inner, depth)?),
522 },
523 Item::Maybe(inner) => Item::Maybe(Box::new(self.expand_aliases(inner, depth)?)),
524 Item::Some(inner) => Item::Some(Box::new(self.expand_aliases(inner, depth)?)),
525 Item::Concat(parts) => Item::Concat(
526 parts
527 .iter()
528 .map(|p| self.expand_aliases(p, depth))
529 .collect::<Result<Vec<_>>>()?,
530 ),
531 Item::Set(entries) => Item::Set(
532 entries
533 .iter()
534 .map(|p| self.expand_set_entry(p, depth))
535 .collect::<Result<Vec<_>>>()?,
536 ),
537 other => other.clone(),
538 })
539 }
540
541 fn expand_set_entry(&self, entry: &Item, depth: usize) -> Result<Item> {
544 match entry {
545 Item::Alias { .. } => self.expand_aliases(entry, depth),
546 Item::Concat(parts) => Ok(Item::Concat(
547 parts
548 .iter()
549 .map(|p| self.expand_set_entry(p, depth))
550 .collect::<Result<Vec<_>>>()?,
551 )),
552 Item::Set(entries) => Ok(Item::Set(
553 entries
554 .iter()
555 .map(|p| self.expand_set_entry(p, depth))
556 .collect::<Result<Vec<_>>>()?,
557 )),
558 Item::Maybe(inner) => Ok(Item::Maybe(Box::new(self.expand_set_entry(inner, depth)?))),
559 Item::Some(inner) => Ok(Item::Some(Box::new(self.expand_set_entry(inner, depth)?))),
560 other => Ok(other.clone()),
561 }
562 }
563
564 fn materialize(&self, fragment: &TNode) -> Result<Item> {
569 match fragment {
570 TNode::Hash(_) => {
571 if fragment.has_key("concatenation") {
572 let parts = fragment.get("concatenation").unwrap();
573 let items = parts
574 .ruby_array()
575 .iter()
576 .map(|p| self.convert_fragment(p))
577 .collect::<Result<Vec<_>>>()?;
578 Ok(Item::concat_from_parts(items))
579 } else {
580 let item = self.convert_fragment(fragment)?;
581 Ok(Item::concat_from_parts(vec![item]))
582 }
583 }
584 TNode::Array(parts) => {
585 let items = parts
586 .iter()
587 .map(|p| self.convert_fragment(p))
588 .collect::<Result<Vec<_>>>()?;
589 Ok(Item::concat_from_parts(items))
590 }
591 TNode::Nil => Ok(Item::None),
592 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
593 }
594 }
595
596 fn convert_fragment(&self, node: &TNode) -> Result<Item> {
598 if let TNode::Hash(pairs) = node {
599 let (key, value) = (&pairs[0].0, &pairs[0].1);
600 match key.as_str() {
601 "string" => return Ok(Item::Str(decode_string_value(value))),
602 "none" => return Ok(Item::None),
603 "any_char" => {
604 return Ok(Item::Alias {
605 map: None,
606 name: "any_character".to_string(),
607 })
608 }
609 "primitive" => {
610 let name = leaf_text(value);
611 return Prim::from_name(&name)
612 .map(Item::Prim)
613 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
614 }
615 "function" => {
616 let name = leaf_text(value);
617 return match name.as_str() {
618 "upcase" => Ok(Item::Func(FuncName::Upcase)),
619 "downcase" => Ok(Item::Func(FuncName::Downcase)),
620 other => Err(Error::MapLogic(format!(
625 "function item {other:?} is not supported in this position"
626 ))),
627 };
628 }
629 "alias" => {
630 return Ok(match value.get("qualified_name") {
635 Some(q) => Item::Alias {
636 map: value.get("identifier").map(ident),
637 name: ident(q),
638 },
639 None => Item::Alias {
640 map: None,
641 name: match value.get("identifier") {
642 Some(id) => ident(id),
643 None => leaf_text(value),
644 },
645 },
646 });
647 }
648 "alias_ref" => {
649 return Ok(Item::Alias {
650 map: None,
651 name: match value.get("identifier") {
652 Some(id) => ident(id),
653 None => leaf_text(value),
654 },
655 });
656 }
657 "ref" => {
658 let digit = leaf_text(value);
659 return Ok(Item::CaptureRef(digit.parse().map_err(|_| {
660 Error::MapLogic(format!("bad ref({digit:?})"))
661 })?));
662 }
663 "capture_inner" => {
664 return Ok(Item::CaptureGroup {
665 id: 0,
666 inner: Box::new(self.materialize(value)?),
667 })
668 }
669 "maybe_inner" => return Ok(Item::Maybe(Box::new(self.materialize(value)?))),
670 "some_inner" => return Ok(Item::Some(Box::new(self.materialize(value)?))),
671 "any" => return self.convert_any_arg(value),
672 "concatenation" => return self.materialize(node),
673 _ => {}
674 }
675 }
676 self.convert_other(node)
677 }
678
679 fn convert_any_arg(&self, value: &TNode) -> Result<Item> {
682 if let Some(hash) = value.ruby_array_hash() {
683 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
684 let get = |name: &str| pget(hash, name);
685 if keys.contains(&"lo") && keys.contains(&"hi") {
686 let lo = unquote(get("lo").unwrap());
687 let hi = unquote(get("hi").unwrap());
688 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
689 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
690 _ => Err(Error::MapLogic("empty any() range bounds".into())),
691 };
692 }
693 if keys.contains(&"single") {
694 let s = unquote(get("single").unwrap());
695 return Ok(Item::Set(
696 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
697 ));
698 }
699 if keys.contains(&"list") {
700 let items = get("list")
701 .unwrap()
702 .ruby_array()
703 .iter()
704 .map(|e| self.convert_set_entry(e))
705 .collect::<Result<Vec<_>>>()?;
706 return Ok(Item::Set(items));
707 }
708 if keys.contains(&"alias_ref") || keys.contains(&"alias") {
709 let alias = self.convert_set_entry(value)?;
712 return Ok(Item::Set(vec![alias]));
713 }
714 if keys.contains(&"any_item") {
715 return self.convert_any_arg(get("any_item").unwrap());
716 }
717 if keys.contains(&"any_char") {
718 return Ok(Item::Set(vec![Item::Alias {
719 map: None,
720 name: "any_character".to_string(),
721 }]));
722 }
723 if keys.contains(&"primitive") {
724 return Ok(Item::Set(vec![self.convert_set_entry(value)?]));
725 }
726 if keys.contains(&"string") {
727 let s = decode_string_value(get("string").unwrap());
728 return Ok(Item::Set(
729 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
730 ));
731 }
732 if keys.contains(&"concatenation") {
733 let item = self.materialize(value)?;
734 return Ok(Item::Set(vec![item]));
735 }
736 }
737 self.convert_set_entry(value)
738 }
739
740 fn convert_set_entry(&self, entry: &TNode) -> Result<Item> {
742 if let TNode::Hash(pairs) = entry {
743 let (key, value) = (&pairs[0].0, &pairs[0].1);
744 match key.as_str() {
745 "string" => return Ok(Item::Str(decode_string_value(value))),
746 "primitive" => {
747 let name = leaf_text(value);
748 return Prim::from_name(&name)
749 .map(Item::Prim)
750 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
751 }
752 "alias_ref" | "alias" => return self.convert_class_alias(value),
753 "concatenation" => return self.materialize(entry),
754 "any" => return self.convert_any_arg(value),
755 _ => {}
756 }
757 }
758 self.materialize(entry)
759 }
760
761 fn convert_class_alias(&self, value: &TNode) -> Result<Item> {
765 let (map, name) = match value.get("qualified_name") {
766 Some(q) => (value.get("identifier").map(ident), ident(q)),
767 None => (None, match value.get("identifier") {
768 Some(id) => ident(id),
769 None => leaf_text(value),
770 }),
771 };
772 if map.is_some() {
776 return Ok(Item::Alias { map, name });
777 }
778 if let Some(prim) = Prim::from_name(&name) {
779 return Ok(Item::Prim(prim));
780 }
781 if let Some(data) = self.imported.get(&name) {
783 return match data {
784 Item::Set(entries) => Ok(Item::Set(entries.clone())),
785 Item::Range(lo, hi) => Ok(Item::Range(*lo, *hi)),
786 Item::Str(s) => Ok(Item::Set(
787 chars_of(s).map(|c| Item::Str(c.to_string())).collect(),
788 )),
789 _ => Ok(vacuous()),
790 };
791 }
792 if let Some((_, raw)) = self.local_raw.iter().find(|(n, _)| *n == name) {
797 let converted = self.materialize(raw)?;
798 return Ok(match converted {
799 Item::Set(_) | Item::Range(_, _) | Item::Str(_) => converted,
800 _ => vacuous(),
801 });
802 }
803 Ok(vacuous())
804 }
805
806 fn materialize_class(&self, fragment: &TNode) -> Result<Item> {
808 match fragment {
809 TNode::Hash(_) if fragment.has_key("concatenation") => {
810 let parts = fragment.get("concatenation").unwrap();
811 let items = parts
812 .ruby_array()
813 .iter()
814 .map(|p| self.convert_class_fragment(p))
815 .collect::<Result<Vec<_>>>()?;
816 Ok(Item::concat_from_parts(items))
817 }
818 TNode::Hash(_) => {
819 let item = self.convert_class_fragment(fragment)?;
820 Ok(Item::concat_from_parts(vec![item]))
821 }
822 TNode::Array(parts) => {
823 let items = parts
824 .iter()
825 .map(|p| self.convert_class_fragment(p))
826 .collect::<Result<Vec<_>>>()?;
827 Ok(Item::concat_from_parts(items))
828 }
829 TNode::Nil => Ok(Item::None),
830 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
831 }
832 }
833
834 fn convert_class_fragment(&self, node: &TNode) -> Result<Item> {
835 if let TNode::Hash(pairs) = node {
838 if let Some((key, value)) = pairs.first() {
839 if key == "alias_ref" || key == "alias" {
840 return self.convert_class_alias(value);
841 }
842 }
843 }
844 self.convert_fragment(node)
845 }
846
847 fn convert_other(&self, node: &TNode) -> Result<Item> {
848 let _ = self;
849 if let TNode::Hash(pairs) = node {
850 let keys: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
851 let get = |name: &str| pget(pairs, name);
852 if keys.contains(&"lo") && keys.contains(&"hi") {
853 let lo = unquote(get("lo").unwrap());
854 let hi = unquote(get("hi").unwrap());
855 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
856 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
857 _ => Err(Error::MapLogic("empty range bounds".into())),
858 };
859 }
860 }
861 Ok(Item::Str(leaf_text(node)))
862 }
863}
864
865fn merge_override_rules(rules: Vec<Sub>, overrides: Vec<Sub>) -> Vec<Sub> {
877 let replaced: BTreeSet<String> = overrides.iter().map(|o| o.from_key()).collect();
878 let mut kept: Vec<Sub> = rules
879 .into_iter()
880 .filter(|r| !replaced.contains(&r.from_key()))
881 .collect();
882 kept.extend(overrides);
883 kept
884}
885
886fn merge_override_items(
895 mut items: Vec<StageItem>,
896 overrides: Vec<Sub>,
897) -> Result<Vec<StageItem>> {
898 let replaced: BTreeSet<String> = overrides.iter().map(|o| o.from_key()).collect();
899 for item in items.iter_mut() {
900 if let StageItem::Parallel(list) | StageItem::Sequential(list) = item {
901 list.retain(|r| !replaced.contains(&r.from_key()));
902 }
903 }
904 match items.last_mut() {
905 Some(StageItem::Parallel(list)) => list.extend(overrides),
906 _ => {
907 if !overrides.is_empty() {
908 items.push(StageItem::Sequential(overrides));
909 }
910 }
911 }
912 Ok(items)
913}
914
915fn flatten_rules(items: &[StageItem], out: &mut Vec<Sub>) -> Result<()> {
918 for item in items {
919 match item {
920 StageItem::Parallel(list) | StageItem::Sequential(list) => {
921 out.extend(list.iter().cloned());
922 }
923 _ => {
924 return Err(Error::MapLogic(
925 "use inside a parallel/sequence block: the referenced \
926 ruleset carries non-rule items (funcalls/stage runs) \
927 that cannot join the pass"
928 .into(),
929 ))
930 }
931 }
932 }
933 Ok(())
934}
935
936fn vacuous() -> Item {
937 Item::Str(String::new())
938}
939
940fn is_none_capture(node: Option<&TNode>) -> bool {
945 match node {
946 None => true,
947 Some(TNode::Nil) => true,
948 Some(n) => n.has_key("none"),
949 }
950}
951
952pub fn decode_string_value(value: &TNode) -> String {
954 match value {
955 TNode::Hash(_) => decode_part(value),
956 TNode::Array(parts) => parts.iter().map(decode_part).collect(),
957 TNode::Str(s) => s.clone(),
958 TNode::Nil => String::new(),
959 }
960}
961
962fn decode_part(part: &TNode) -> String {
963 if let Some(pairs) = part.ruby_array_hash() {
964 if let Some((key, value)) = pairs.first() {
965 return match key.as_str() {
966 "run" => leaf_text(value),
967 "newline" => "\n".to_string(),
968 "carriage_return" => "\r".to_string(),
969 "tab" => "\t".to_string(),
970 "dquote" => "\"".to_string(),
971 "backslash" => "\\".to_string(),
972 "u_lone" => "U".to_string(),
973 "unicode" => {
974 let hex = leaf_text(value);
975 match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
976 Some(c) => c.to_string(),
977 None => hex,
979 }
980 }
981 _ => leaf_text(part),
982 };
983 }
984 return String::new();
985 }
986 match part {
987 TNode::Str(s) => s.clone(),
988 _ => leaf_text(part),
989 }
990}
991
992pub fn unquote(fragment: &TNode) -> String {
994 match fragment {
995 TNode::Nil => String::new(),
996 TNode::Str(s) => s.clone(),
997 TNode::Hash(pairs) => {
998 if let Some((_, v)) = pairs.iter().find(|(k2, _)| k2 == "string") {
999 decode_string_value(v)
1000 } else {
1001 leaf_text(fragment)
1002 }
1003 }
1004 TNode::Array(_) => leaf_text(fragment),
1005 }
1006}
1007
1008pub fn ident(fragment: &TNode) -> String {
1010 match fragment {
1011 TNode::Nil => String::new(),
1012 TNode::Str(s) => s.clone(),
1013 _ => match fragment.get("identifier") {
1014 Some(id) => leaf_text(id),
1015 None => leaf_text(fragment),
1016 },
1017 }
1018}
1019
1020fn chars_of(s: &str) -> impl Iterator<Item = char> + '_ {
1021 s.chars()
1022}
1023
1024fn render_str(item: &Item) -> String {
1027 match item {
1028 Item::Str(s) => s.clone(),
1029 Item::None => String::new(),
1030 Item::Concat(parts) => parts.iter().map(render_str).collect(),
1031 _ => String::new(),
1032 }
1033}
1034
1035pub fn extract_kwargs(node: &TNode) -> Vec<(String, String)> {
1037 let mut out = Vec::new();
1038 let entries: Vec<&TNode> = match node.get("kwarg") {
1039 Some(kw) => kw.ruby_array().iter().collect(),
1040 None => vec![node],
1041 };
1042 for entry in entries {
1043 if let (Some(name), Some(value)) = (entry.get("kwarg_name"), entry.get("kwarg_value")) {
1044 out.push((ident(name), unquote(value)));
1045 }
1046 }
1047 out
1048}
1049
1050pub fn unescape_braces(text: &str) -> String {
1052 let mut out = String::with_capacity(text.len());
1053 let mut chars = text.chars();
1054 while let Some(c) = chars.next() {
1055 if c == '\\' {
1056 match chars.next() {
1057 Some(n @ ('{' | '}' | '\\')) => out.push(n),
1058 Some(other) => {
1059 out.push('\\');
1060 out.push(other);
1061 }
1062 None => out.push('\\'),
1063 }
1064 } else {
1065 out.push(c);
1066 }
1067 }
1068 out
1069}
1070
1071pub fn normalize_heredoc(text: &str) -> String {
1074 let lines: Vec<&str> = text.lines().collect();
1075 let content: Vec<&&str> = lines
1076 .iter()
1077 .filter(|l| !l.trim().is_empty())
1078 .collect();
1079 if content.is_empty() {
1080 return String::new();
1081 }
1082 if content.len() == 1 {
1083 return content[0].trim().to_string();
1084 }
1085 let min_indent = content
1086 .iter()
1087 .skip(1)
1088 .map(|l| l.len() - l.trim_start_matches([' ', '\t']).len())
1089 .min()
1090 .unwrap_or(0);
1091 let mut result: Vec<String> = Vec::new();
1092 for line in &lines {
1093 if line.trim().is_empty() {
1094 result.push(String::new());
1095 } else {
1096 let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
1097 if indent >= min_indent {
1098 result.push(line[min_indent..].to_string());
1099 } else {
1100 result.push(line.trim().to_string());
1101 }
1102 }
1103 }
1104 let joined = result.join("\n");
1105 let trimmed = joined.trim_end().to_string();
1106 if text.ends_with('\n') && !text.ends_with("\n\n") {
1107 trimmed + "\n"
1108 } else {
1109 trimmed
1110 }
1111}
1112
1113fn extract_metadata(arr: &TNode, out: &mut Vec<(String, String)>) {
1115 for field in arr.ruby_array() {
1116 let Some(hash) = field.ruby_array_hash() else { continue };
1117 if let Some(v) = pget(hash, "field_name") {
1118 let name = ident(v);
1119 let val = if let Some(b) = pget(hash, "field_block") {
1120 normalize_heredoc(&unescape_braces(&leaf_text(b)))
1121 } else if let Some(fv) = pget(hash, "field_value") {
1122 match fv {
1123 TNode::Hash(_) if fv.has_key("string") => unquote(fv),
1124 _ => fv.as_str().unwrap_or_default().trim().to_string(),
1125 }
1126 } else {
1127 String::new()
1128 };
1129 out.push((name, val));
1130 } else {
1131 for (key, val) in hash.iter() {
1132 if matches!(val, TNode::Nil) {
1133 continue;
1134 }
1135 let text = match val {
1136 TNode::Hash(_) if val.has_key("string") => unquote(val),
1137 _ => leaf_text(val),
1138 };
1139 out.push((key.clone(), text));
1140 }
1141 }
1142 }
1143}
1144
1145pub fn assign_group_ids(sub: &mut Sub) -> usize {
1149 let mut counter = 0usize;
1150 for item in [&mut sub.before, &mut sub.not_before] {
1151 if let Some(i) = item {
1152 assign_in(i, &mut counter);
1153 }
1154 }
1155 assign_in(&mut sub.from, &mut counter);
1156 for item in [&mut sub.not_after, &mut sub.after] {
1157 if let Some(i) = item {
1158 assign_in(i, &mut counter);
1159 }
1160 }
1161 counter
1162}
1163
1164fn assign_in(item: &mut Item, counter: &mut usize) {
1165 match item {
1166 Item::CaptureGroup { id, inner } => {
1167 *counter += 1;
1168 *id = *counter;
1169 assign_in(inner, counter);
1170 }
1171 Item::Maybe(inner) | Item::Some(inner) => assign_in(inner, counter),
1172 Item::Concat(parts) => parts.iter_mut().for_each(|p| assign_in(p, counter)),
1173 Item::Set(entries) => entries.iter_mut().for_each(|e| assign_in(e, counter)),
1174 _ => {}
1175 }
1176}