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 let ctx = Ctx {
100 imported: {
101 let mut m = BTreeMap::new();
102 for doc in imports {
103 for (k, v) in &doc.aliases {
104 m.entry(k.clone()).or_insert_with(|| v.clone());
105 }
106 }
107 m
108 },
109 local_raw: alias_nodes.clone(),
110 dep_docs,
111 ruleset_raw: ruleset_nodes,
112 ruleset_memo: RefCell::new(BTreeMap::new()),
113 ruleset_stack: RefCell::new(Vec::new()),
114 };
115
116 for (name, _) in &ctx.ruleset_raw {
121 ctx.local_ruleset(name)?;
122 }
123 let rulesets: BTreeMap<String, Stage> = ctx.ruleset_memo.borrow().clone();
124
125 {
128 let mut seen = BTreeSet::new();
129 languages.retain(|l| seen.insert(l.clone()));
130 }
131
132 let mut aliases = BTreeMap::new();
134 for (name, value) in &alias_nodes {
135 aliases.insert(name.clone(), ctx.materialize(value)?);
136 }
137
138 let mut stages = BTreeMap::new();
139 for (name, node) in stage_nodes {
140 let stage = ctx.extract_stage(&name, node)?;
141 stages.insert(stage.name.clone(), stage);
142 }
143
144 Ok(Document {
145 name: filename_stem.to_string(),
146 system_code,
147 aliases,
148 stages,
149 rulesets,
150 dep_aliases: dep_docs
151 .iter()
152 .filter_map(|(k, v)| {
153 dependencies
154 .iter()
155 .find(|d| d.alias.as_deref() == Some(k.as_str()))
156 .map(|_| (k.clone(), v.clone()))
157 })
158 .collect(),
159 imports: imports.to_vec(),
160 tests,
161 metadata,
162 languages,
163 })
164}
165
166struct Ctx<'a> {
167 imported: BTreeMap<String, Item>,
169 local_raw: Vec<(String, TNode)>,
171 dep_docs: &'a BTreeMap<String, Rc<Document>>,
173 ruleset_raw: Vec<(String, &'a TNode)>,
175 ruleset_memo: RefCell<BTreeMap<String, Stage>>,
178 ruleset_stack: RefCell<Vec<String>>,
181}
182
183impl<'a> Ctx<'a> {
184 fn extract_stage(&self, name: &str, node: &TNode) -> Result<Stage> {
189 let items = self.extract_stage_items(node)?;
190 Ok(Stage { name: name.to_string(), items, reverse_run: None })
191 }
192
193 fn extract_stage_items(&self, node: &TNode) -> Result<Vec<StageItem>> {
197 let mut items: Vec<StageItem> = Vec::new();
198 for entry in node.ruby_array() {
199 let Some(hash) = entry.ruby_array_hash() else { continue };
200 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
201 if keys.contains(&"use_map") {
202 items.extend(self.resolve_use_items(hash)?);
204 } else if keys.contains(&"sequence") {
205 let node = pget(hash, "sequence").unwrap();
206 items.push(StageItem::Sequential(self.extract_rule_list(node)?));
207 } else if keys.contains(&"parallel") {
208 let node = pget(hash, "parallel").unwrap();
209 items.push(StageItem::Parallel(self.extract_rule_list(node)?));
210 } else if keys.contains(&"separate") {
211 let separator = pget(hash, "separator")
212 .map(|v| self.materialize(v))
213 .transpose()?
214 .map(|item| render_str(&item.first_string()));
215 items.push(StageItem::Funcall(Funcall {
216 name: FuncallName::Separate,
217 kwargs: separator
218 .map(|s| vec![("separator".to_string(), s)])
219 .unwrap_or_default(),
220 reverse_run: None,
221 }));
222 } else if keys.contains(&"compose") {
223 let op = leaf_text(pget(hash, "compose").unwrap());
224 let name = if op == "decompose" {
225 FuncallName::Decompose
226 } else {
227 FuncallName::Compose
228 };
229 items.push(StageItem::Funcall(Funcall { name, kwargs: vec![], reverse_run: None }));
230 } else if keys.contains(&"case") {
231 let op = leaf_text(pget(hash, "case").unwrap());
232 let name = match op.as_str() {
233 "upcase" => FuncallName::Upcase,
234 "downcase" => FuncallName::Downcase,
235 "title_case" => FuncallName::TitleCase,
236 other => return Err(Error::MapLogic(format!("unknown case op {other:?}"))),
237 };
238 let kwargs = pget(hash, "case_kwargs").map(extract_kwargs).unwrap_or_default();
239 items.push(StageItem::Funcall(Funcall { name, kwargs, reverse_run: None }));
240 } else if keys.contains(&"funcall_name") {
241 let fname = leaf_text(pget(hash, "funcall_name").unwrap());
242 let kwargs = pget(hash, "funcall_kwargs").map(extract_kwargs).unwrap_or_default();
243 let fname_enum = FuncallName::from_name(&fname)
244 .ok_or_else(|| crate::stdlib::unsupported_funcall(&fname))?;
245 items.push(StageItem::Funcall(Funcall { name: fname_enum, kwargs, reverse_run: None }));
246 } else if keys.contains(&"dep") {
247 let dep = ident(pget(hash, "dep").unwrap());
248 let stage = ident(pget(hash, "stage").unwrap());
249 items.push(StageItem::Run {
250 dep: Some(dep),
251 stage,
252 reverse_run: None,
253 });
254 } else if keys.contains(&"run_stage_only") {
255 let stage = pget(hash, "run_stage_only")
256 .and_then(|v| v.get("stage"))
257 .map(ident)
258 .unwrap_or_default();
259 items.push(StageItem::Run { dep: None, stage, reverse_run: None });
260 } else if keys.contains(&"bare_rule") {
261 let rule = pget(hash, "bare_rule").unwrap();
262 items.push(StageItem::Sequential(vec![self.extract_rule(rule)?]));
263 }
264 }
266 Ok(items)
267 }
268
269 fn resolve_use_stage(&self, hash: &[(String, TNode)]) -> Result<Stage> {
276 let use_map = pget(hash, "use_map")
277 .map(ident)
278 .ok_or_else(|| Error::MapLogic("use directive without a target".into()))?;
279 match pget(hash, "use_ruleset") {
280 Some(rs_node) => {
281 let rs = ident(rs_node);
282 let doc = self.dep_docs.get(&use_map).ok_or_else(|| {
283 Error::MapLogic(format!("dep alias {use_map:?} not found"))
284 })?;
285 doc.rulesets.get(&rs).cloned().ok_or_else(|| {
286 Error::MapLogic(format!("ruleset {rs:?} of dep {use_map:?} not found"))
287 })
288 }
289 None => self.local_ruleset(&use_map),
290 }
291 }
292
293 fn local_ruleset(&self, name: &str) -> Result<Stage> {
296 if let Some(built) = self.ruleset_memo.borrow().get(name) {
297 return Ok(built.clone());
298 }
299 {
300 let stack = self.ruleset_stack.borrow();
301 if stack.iter().any(|n| n == name) {
302 let mut chain = stack.clone();
303 chain.push(name.to_string());
304 return Err(Error::MapLogic(format!(
305 "use cycle: {}",
306 chain.join(" -> ")
307 )));
308 }
309 }
310 let node = self
311 .ruleset_raw
312 .iter()
313 .find(|(n, _)| n.as_str() == name)
314 .map(|(_, node)| *node)
315 .ok_or_else(|| Error::MapLogic(format!("ruleset {name:?} not found")))?;
316 self.ruleset_stack.borrow_mut().push(name.to_string());
317 let expanded = self.extract_stage_items(node);
318 self.ruleset_stack.borrow_mut().pop();
319 let stage = Stage {
320 name: name.to_string(),
321 items: expanded?,
322 reverse_run: None,
323 };
324 self.ruleset_memo
325 .borrow_mut()
326 .insert(name.to_string(), stage.clone());
327 Ok(stage)
328 }
329
330 fn resolve_use_items(&self, hash: &[(String, TNode)]) -> Result<Vec<StageItem>> {
334 let mut stage = self.resolve_use_stage(hash)?;
335 if let Some(orules) = pget(hash, "override_rules") {
336 let overrides = self.extract_rule_list(orules)?;
337 stage.items = merge_override_items(stage.items, overrides)?;
338 }
339 Ok(stage.items)
340 }
341
342 fn resolve_use_rules(&self, hash: &[(String, TNode)]) -> Result<Vec<Sub>> {
347 let stage = self.resolve_use_stage(hash)?;
348 let mut rules = Vec::new();
349 flatten_rules(&stage.items, &mut rules)?;
350 if let Some(orules) = pget(hash, "override_rules") {
351 let overrides = self.extract_rule_list(orules)?;
352 rules = merge_override_rules(rules, overrides);
353 }
354 Ok(rules)
355 }
356
357 fn extract_rule_list(&self, node: &TNode) -> Result<Vec<Sub>> {
358 let mut rules = Vec::new();
359 for r in node.ruby_array() {
360 if r.has_key("use_map") {
365 let Some(hash) = r.ruby_array_hash() else { continue };
366 rules.extend(self.resolve_use_rules(hash)?);
367 continue;
368 }
369 if is_none_capture(r.get("from")) && is_none_capture(r.get("to")) {
371 continue;
372 }
373 rules.push(self.extract_rule(r)?);
374 }
375 Ok(rules)
376 }
377
378 fn extract_rule(&self, r: &TNode) -> Result<Sub> {
379 let from = match r.get("from") {
380 Some(n) => self.materialize(n)?,
381 None => Item::None,
382 };
383 let to = match r.get("to") {
384 Some(n) => self.materialize(n)?,
385 None => Item::None,
386 };
387
388 let mut before = None;
389 let mut not_before = None;
390 let mut after = None;
391 let mut not_after = None;
392
393 if let Some(constraints) = r.get("constraints") {
394 for c in constraints.ruby_array() {
395 let Some(hash) = c.ruby_array_hash() else { continue };
396 for (kind, item_node) in hash.iter() {
397 let target = match kind.as_str() {
398 "before" => &mut before,
399 "not_before" => &mut not_before,
400 "after" => &mut after,
401 "not_after" => &mut not_after,
402 _ => continue,
403 };
404 *target = Some(self.materialize_class(item_node)?);
407 }
408 }
409 }
410
411 let to_target = match &to {
414 Item::Func(FuncName::Upcase) => SubTo::Upcase,
415 Item::Func(FuncName::Downcase) => SubTo::Downcase,
416 item => SubTo::Item(item.clone()),
417 };
418
419 let mut when = Vec::new();
422 if let Some(wc) = r.get("when_clause") {
423 if let Some(langs) = wc.get("when_language") {
424 for l in langs.ruby_array() {
425 when.push(ident(l));
426 }
427 }
428 }
429
430 let mut sub = Sub {
431 from,
432 to: to_target,
433 before,
434 not_before,
435 after,
436 not_after,
437 group_count: 0,
438 reverse_run: None,
439 when,
440 };
441 sub.from = self.expand_aliases(&sub.from, 0)?;
446 for c in [&mut sub.before, &mut sub.not_before] {
447 if let Some(i) = c {
448 *i = self.expand_aliases(i, 0)?;
449 }
450 }
451 for c in [&mut sub.not_after, &mut sub.after] {
453 if let Some(i) = c {
454 *i = self.expand_aliases(i, 0)?;
455 }
456 }
457 sub.group_count = assign_group_ids(&mut sub);
458 Ok(sub)
459 }
460
461 fn expand_aliases(&self, item: &Item, depth: usize) -> Result<Item> {
467 if depth > 32 {
468 return Err(Error::MapLogic(
469 "alias expansion too deep (alias cycle?)".into(),
470 ));
471 }
472 Ok(match item {
473 Item::Alias { map, name } => {
474 if map.is_some() {
475 let alias = map.clone().unwrap();
477 let doc = self
478 .dep_docs
479 .get(&alias)
480 .ok_or_else(|| Error::MapLogic(format!("dep alias {alias:?} not found")))?;
481 match doc.lookup_alias(name) {
482 Some(resolved) => self.expand_aliases(&resolved, depth + 1)?,
483 None => {
484 return Err(Error::MapLogic(format!(
485 "alias {name:?} of dep {alias:?} not found"
486 )))
487 }
488 }
489 } else if let Some(prim) = Prim::from_name(name) {
490 Item::Prim(prim)
491 } else if let Some(data) = self.imported.get(name) {
492 self.expand_aliases(data, depth + 1)?
493 } else if let Some((_, raw)) = self
494 .local_raw
495 .iter()
496 .find(|(n, _)| n.as_str() == name)
497 {
498 let resolved = self.materialize(raw)?;
499 self.expand_aliases(&resolved, depth + 1)?
500 } else {
501 return Err(Error::MapLogic(format!(
502 "alias {name:?} not found during expansion"
503 )));
504 }
505 }
506 Item::CaptureGroup { id, inner } => Item::CaptureGroup {
507 id: *id,
508 inner: Box::new(self.expand_aliases(inner, depth)?),
509 },
510 Item::Maybe(inner) => Item::Maybe(Box::new(self.expand_aliases(inner, depth)?)),
511 Item::Some(inner) => Item::Some(Box::new(self.expand_aliases(inner, depth)?)),
512 Item::Concat(parts) => Item::Concat(
513 parts
514 .iter()
515 .map(|p| self.expand_aliases(p, depth))
516 .collect::<Result<Vec<_>>>()?,
517 ),
518 Item::Set(entries) => Item::Set(
519 entries
520 .iter()
521 .map(|p| self.expand_set_entry(p, depth))
522 .collect::<Result<Vec<_>>>()?,
523 ),
524 other => other.clone(),
525 })
526 }
527
528 fn expand_set_entry(&self, entry: &Item, depth: usize) -> Result<Item> {
531 match entry {
532 Item::Alias { .. } => self.expand_aliases(entry, depth),
533 Item::Concat(parts) => Ok(Item::Concat(
534 parts
535 .iter()
536 .map(|p| self.expand_set_entry(p, depth))
537 .collect::<Result<Vec<_>>>()?,
538 )),
539 Item::Set(entries) => Ok(Item::Set(
540 entries
541 .iter()
542 .map(|p| self.expand_set_entry(p, depth))
543 .collect::<Result<Vec<_>>>()?,
544 )),
545 Item::Maybe(inner) => Ok(Item::Maybe(Box::new(self.expand_set_entry(inner, depth)?))),
546 Item::Some(inner) => Ok(Item::Some(Box::new(self.expand_set_entry(inner, depth)?))),
547 other => Ok(other.clone()),
548 }
549 }
550
551 fn materialize(&self, fragment: &TNode) -> Result<Item> {
556 match fragment {
557 TNode::Hash(_) => {
558 if fragment.has_key("concatenation") {
559 let parts = fragment.get("concatenation").unwrap();
560 let items = parts
561 .ruby_array()
562 .iter()
563 .map(|p| self.convert_fragment(p))
564 .collect::<Result<Vec<_>>>()?;
565 Ok(Item::concat_from_parts(items))
566 } else {
567 let item = self.convert_fragment(fragment)?;
568 Ok(Item::concat_from_parts(vec![item]))
569 }
570 }
571 TNode::Array(parts) => {
572 let items = parts
573 .iter()
574 .map(|p| self.convert_fragment(p))
575 .collect::<Result<Vec<_>>>()?;
576 Ok(Item::concat_from_parts(items))
577 }
578 TNode::Nil => Ok(Item::None),
579 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
580 }
581 }
582
583 fn convert_fragment(&self, node: &TNode) -> Result<Item> {
585 if let TNode::Hash(pairs) = node {
586 let (key, value) = (&pairs[0].0, &pairs[0].1);
587 match key.as_str() {
588 "string" => return Ok(Item::Str(decode_string_value(value))),
589 "none" => return Ok(Item::None),
590 "any_char" => {
591 return Ok(Item::Alias {
592 map: None,
593 name: "any_character".to_string(),
594 })
595 }
596 "primitive" => {
597 let name = leaf_text(value);
598 return Prim::from_name(&name)
599 .map(Item::Prim)
600 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
601 }
602 "function" => {
603 let name = leaf_text(value);
604 return match name.as_str() {
605 "upcase" => Ok(Item::Func(FuncName::Upcase)),
606 "downcase" => Ok(Item::Func(FuncName::Downcase)),
607 other => Err(Error::MapLogic(format!(
612 "function item {other:?} is not supported in this position"
613 ))),
614 };
615 }
616 "alias" => {
617 return Ok(match value.get("qualified_name") {
622 Some(q) => Item::Alias {
623 map: value.get("identifier").map(ident),
624 name: ident(q),
625 },
626 None => Item::Alias {
627 map: None,
628 name: match value.get("identifier") {
629 Some(id) => ident(id),
630 None => leaf_text(value),
631 },
632 },
633 });
634 }
635 "alias_ref" => {
636 return Ok(Item::Alias {
637 map: None,
638 name: match value.get("identifier") {
639 Some(id) => ident(id),
640 None => leaf_text(value),
641 },
642 });
643 }
644 "ref" => {
645 let digit = leaf_text(value);
646 return Ok(Item::CaptureRef(digit.parse().map_err(|_| {
647 Error::MapLogic(format!("bad ref({digit:?})"))
648 })?));
649 }
650 "capture_inner" => {
651 return Ok(Item::CaptureGroup {
652 id: 0,
653 inner: Box::new(self.materialize(value)?),
654 })
655 }
656 "maybe_inner" => return Ok(Item::Maybe(Box::new(self.materialize(value)?))),
657 "some_inner" => return Ok(Item::Some(Box::new(self.materialize(value)?))),
658 "any" => return self.convert_any_arg(value),
659 "concatenation" => return self.materialize(node),
660 _ => {}
661 }
662 }
663 self.convert_other(node)
664 }
665
666 fn convert_any_arg(&self, value: &TNode) -> Result<Item> {
669 if let Some(hash) = value.ruby_array_hash() {
670 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
671 let get = |name: &str| pget(hash, name);
672 if keys.contains(&"lo") && keys.contains(&"hi") {
673 let lo = unquote(get("lo").unwrap());
674 let hi = unquote(get("hi").unwrap());
675 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
676 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
677 _ => Err(Error::MapLogic("empty any() range bounds".into())),
678 };
679 }
680 if keys.contains(&"single") {
681 let s = unquote(get("single").unwrap());
682 return Ok(Item::Set(
683 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
684 ));
685 }
686 if keys.contains(&"list") {
687 let items = get("list")
688 .unwrap()
689 .ruby_array()
690 .iter()
691 .map(|e| self.convert_set_entry(e))
692 .collect::<Result<Vec<_>>>()?;
693 return Ok(Item::Set(items));
694 }
695 if keys.contains(&"alias_ref") || keys.contains(&"alias") {
696 let alias = self.convert_set_entry(value)?;
699 return Ok(Item::Set(vec![alias]));
700 }
701 if keys.contains(&"any_item") {
702 return self.convert_any_arg(get("any_item").unwrap());
703 }
704 if keys.contains(&"any_char") {
705 return Ok(Item::Set(vec![Item::Alias {
706 map: None,
707 name: "any_character".to_string(),
708 }]));
709 }
710 if keys.contains(&"primitive") {
711 return Ok(Item::Set(vec![self.convert_set_entry(value)?]));
712 }
713 if keys.contains(&"string") {
714 let s = decode_string_value(get("string").unwrap());
715 return Ok(Item::Set(
716 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
717 ));
718 }
719 if keys.contains(&"concatenation") {
720 let item = self.materialize(value)?;
721 return Ok(Item::Set(vec![item]));
722 }
723 }
724 self.convert_set_entry(value)
725 }
726
727 fn convert_set_entry(&self, entry: &TNode) -> Result<Item> {
729 if let TNode::Hash(pairs) = entry {
730 let (key, value) = (&pairs[0].0, &pairs[0].1);
731 match key.as_str() {
732 "string" => return Ok(Item::Str(decode_string_value(value))),
733 "primitive" => {
734 let name = leaf_text(value);
735 return Prim::from_name(&name)
736 .map(Item::Prim)
737 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
738 }
739 "alias_ref" | "alias" => return self.convert_class_alias(value),
740 "concatenation" => return self.materialize(entry),
741 "any" => return self.convert_any_arg(value),
742 _ => {}
743 }
744 }
745 self.materialize(entry)
746 }
747
748 fn convert_class_alias(&self, value: &TNode) -> Result<Item> {
752 let (map, name) = match value.get("qualified_name") {
753 Some(q) => (value.get("identifier").map(ident), ident(q)),
754 None => (None, match value.get("identifier") {
755 Some(id) => ident(id),
756 None => leaf_text(value),
757 }),
758 };
759 if map.is_some() {
763 return Ok(Item::Alias { map, name });
764 }
765 if let Some(prim) = Prim::from_name(&name) {
766 return Ok(Item::Prim(prim));
767 }
768 if let Some(data) = self.imported.get(&name) {
770 return match data {
771 Item::Set(entries) => Ok(Item::Set(entries.clone())),
772 Item::Range(lo, hi) => Ok(Item::Range(*lo, *hi)),
773 Item::Str(s) => Ok(Item::Set(
774 chars_of(s).map(|c| Item::Str(c.to_string())).collect(),
775 )),
776 _ => Ok(vacuous()),
777 };
778 }
779 if let Some((_, raw)) = self.local_raw.iter().find(|(n, _)| *n == name) {
784 let converted = self.materialize(raw)?;
785 return Ok(match converted {
786 Item::Set(_) | Item::Range(_, _) | Item::Str(_) => converted,
787 _ => vacuous(),
788 });
789 }
790 Ok(vacuous())
791 }
792
793 fn materialize_class(&self, fragment: &TNode) -> Result<Item> {
795 match fragment {
796 TNode::Hash(_) if fragment.has_key("concatenation") => {
797 let parts = fragment.get("concatenation").unwrap();
798 let items = parts
799 .ruby_array()
800 .iter()
801 .map(|p| self.convert_class_fragment(p))
802 .collect::<Result<Vec<_>>>()?;
803 Ok(Item::concat_from_parts(items))
804 }
805 TNode::Hash(_) => {
806 let item = self.convert_class_fragment(fragment)?;
807 Ok(Item::concat_from_parts(vec![item]))
808 }
809 TNode::Array(parts) => {
810 let items = parts
811 .iter()
812 .map(|p| self.convert_class_fragment(p))
813 .collect::<Result<Vec<_>>>()?;
814 Ok(Item::concat_from_parts(items))
815 }
816 TNode::Nil => Ok(Item::None),
817 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
818 }
819 }
820
821 fn convert_class_fragment(&self, node: &TNode) -> Result<Item> {
822 if let TNode::Hash(pairs) = node {
825 if let Some((key, value)) = pairs.first() {
826 if key == "alias_ref" || key == "alias" {
827 return self.convert_class_alias(value);
828 }
829 }
830 }
831 self.convert_fragment(node)
832 }
833
834 fn convert_other(&self, node: &TNode) -> Result<Item> {
835 let _ = self;
836 if let TNode::Hash(pairs) = node {
837 let keys: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
838 let get = |name: &str| pget(pairs, name);
839 if keys.contains(&"lo") && keys.contains(&"hi") {
840 let lo = unquote(get("lo").unwrap());
841 let hi = unquote(get("hi").unwrap());
842 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
843 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
844 _ => Err(Error::MapLogic("empty range bounds".into())),
845 };
846 }
847 }
848 Ok(Item::Str(leaf_text(node)))
849 }
850}
851
852fn merge_override_rules(rules: Vec<Sub>, overrides: Vec<Sub>) -> Vec<Sub> {
864 let replaced: BTreeSet<String> = overrides.iter().map(|o| o.from_key()).collect();
865 let mut kept: Vec<Sub> = rules
866 .into_iter()
867 .filter(|r| !replaced.contains(&r.from_key()))
868 .collect();
869 kept.extend(overrides);
870 kept
871}
872
873fn merge_override_items(
882 mut items: Vec<StageItem>,
883 overrides: Vec<Sub>,
884) -> Result<Vec<StageItem>> {
885 let replaced: BTreeSet<String> = overrides.iter().map(|o| o.from_key()).collect();
886 for item in items.iter_mut() {
887 if let StageItem::Parallel(list) | StageItem::Sequential(list) = item {
888 list.retain(|r| !replaced.contains(&r.from_key()));
889 }
890 }
891 match items.last_mut() {
892 Some(StageItem::Parallel(list)) => list.extend(overrides),
893 _ => {
894 if !overrides.is_empty() {
895 items.push(StageItem::Sequential(overrides));
896 }
897 }
898 }
899 Ok(items)
900}
901
902fn flatten_rules(items: &[StageItem], out: &mut Vec<Sub>) -> Result<()> {
905 for item in items {
906 match item {
907 StageItem::Parallel(list) | StageItem::Sequential(list) => {
908 out.extend(list.iter().cloned());
909 }
910 _ => {
911 return Err(Error::MapLogic(
912 "use inside a parallel/sequence block: the referenced \
913 ruleset carries non-rule items (funcalls/stage runs) \
914 that cannot join the pass"
915 .into(),
916 ))
917 }
918 }
919 }
920 Ok(())
921}
922
923fn vacuous() -> Item {
924 Item::Str(String::new())
925}
926
927fn is_none_capture(node: Option<&TNode>) -> bool {
932 match node {
933 None => true,
934 Some(TNode::Nil) => true,
935 Some(n) => n.has_key("none"),
936 }
937}
938
939pub fn decode_string_value(value: &TNode) -> String {
941 match value {
942 TNode::Hash(_) => decode_part(value),
943 TNode::Array(parts) => parts.iter().map(decode_part).collect(),
944 TNode::Str(s) => s.clone(),
945 TNode::Nil => String::new(),
946 }
947}
948
949fn decode_part(part: &TNode) -> String {
950 if let Some(pairs) = part.ruby_array_hash() {
951 if let Some((key, value)) = pairs.first() {
952 return match key.as_str() {
953 "run" => leaf_text(value),
954 "newline" => "\n".to_string(),
955 "carriage_return" => "\r".to_string(),
956 "tab" => "\t".to_string(),
957 "dquote" => "\"".to_string(),
958 "backslash" => "\\".to_string(),
959 "u_lone" => "U".to_string(),
960 "unicode" => {
961 let hex = leaf_text(value);
962 match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
963 Some(c) => c.to_string(),
964 None => hex,
966 }
967 }
968 _ => leaf_text(part),
969 };
970 }
971 return String::new();
972 }
973 match part {
974 TNode::Str(s) => s.clone(),
975 _ => leaf_text(part),
976 }
977}
978
979pub fn unquote(fragment: &TNode) -> String {
981 match fragment {
982 TNode::Nil => String::new(),
983 TNode::Str(s) => s.clone(),
984 TNode::Hash(pairs) => {
985 if let Some((_, v)) = pairs.iter().find(|(k2, _)| k2 == "string") {
986 decode_string_value(v)
987 } else {
988 leaf_text(fragment)
989 }
990 }
991 TNode::Array(_) => leaf_text(fragment),
992 }
993}
994
995pub fn ident(fragment: &TNode) -> String {
997 match fragment {
998 TNode::Nil => String::new(),
999 TNode::Str(s) => s.clone(),
1000 _ => match fragment.get("identifier") {
1001 Some(id) => leaf_text(id),
1002 None => leaf_text(fragment),
1003 },
1004 }
1005}
1006
1007fn chars_of(s: &str) -> impl Iterator<Item = char> + '_ {
1008 s.chars()
1009}
1010
1011fn render_str(item: &Item) -> String {
1014 match item {
1015 Item::Str(s) => s.clone(),
1016 Item::None => String::new(),
1017 Item::Concat(parts) => parts.iter().map(render_str).collect(),
1018 _ => String::new(),
1019 }
1020}
1021
1022pub fn extract_kwargs(node: &TNode) -> Vec<(String, String)> {
1024 let mut out = Vec::new();
1025 let entries: Vec<&TNode> = match node.get("kwarg") {
1026 Some(kw) => kw.ruby_array().iter().collect(),
1027 None => vec![node],
1028 };
1029 for entry in entries {
1030 if let (Some(name), Some(value)) = (entry.get("kwarg_name"), entry.get("kwarg_value")) {
1031 out.push((ident(name), unquote(value)));
1032 }
1033 }
1034 out
1035}
1036
1037pub fn unescape_braces(text: &str) -> String {
1039 let mut out = String::with_capacity(text.len());
1040 let mut chars = text.chars();
1041 while let Some(c) = chars.next() {
1042 if c == '\\' {
1043 match chars.next() {
1044 Some(n @ ('{' | '}' | '\\')) => out.push(n),
1045 Some(other) => {
1046 out.push('\\');
1047 out.push(other);
1048 }
1049 None => out.push('\\'),
1050 }
1051 } else {
1052 out.push(c);
1053 }
1054 }
1055 out
1056}
1057
1058pub fn normalize_heredoc(text: &str) -> String {
1061 let lines: Vec<&str> = text.lines().collect();
1062 let content: Vec<&&str> = lines
1063 .iter()
1064 .filter(|l| !l.trim().is_empty())
1065 .collect();
1066 if content.is_empty() {
1067 return String::new();
1068 }
1069 if content.len() == 1 {
1070 return content[0].trim().to_string();
1071 }
1072 let min_indent = content
1073 .iter()
1074 .skip(1)
1075 .map(|l| l.len() - l.trim_start_matches([' ', '\t']).len())
1076 .min()
1077 .unwrap_or(0);
1078 let mut result: Vec<String> = Vec::new();
1079 for line in &lines {
1080 if line.trim().is_empty() {
1081 result.push(String::new());
1082 } else {
1083 let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
1084 if indent >= min_indent {
1085 result.push(line[min_indent..].to_string());
1086 } else {
1087 result.push(line.trim().to_string());
1088 }
1089 }
1090 }
1091 let joined = result.join("\n");
1092 let trimmed = joined.trim_end().to_string();
1093 if text.ends_with('\n') && !text.ends_with("\n\n") {
1094 trimmed + "\n"
1095 } else {
1096 trimmed
1097 }
1098}
1099
1100fn extract_metadata(arr: &TNode, out: &mut Vec<(String, String)>) {
1102 for field in arr.ruby_array() {
1103 let Some(hash) = field.ruby_array_hash() else { continue };
1104 if let Some(v) = pget(hash, "field_name") {
1105 let name = ident(v);
1106 let val = if let Some(b) = pget(hash, "field_block") {
1107 normalize_heredoc(&unescape_braces(&leaf_text(b)))
1108 } else if let Some(fv) = pget(hash, "field_value") {
1109 match fv {
1110 TNode::Hash(_) if fv.has_key("string") => unquote(fv),
1111 _ => fv.as_str().unwrap_or_default().trim().to_string(),
1112 }
1113 } else {
1114 String::new()
1115 };
1116 out.push((name, val));
1117 } else {
1118 for (key, val) in hash.iter() {
1119 if matches!(val, TNode::Nil) {
1120 continue;
1121 }
1122 let text = match val {
1123 TNode::Hash(_) if val.has_key("string") => unquote(val),
1124 _ => leaf_text(val),
1125 };
1126 out.push((key.clone(), text));
1127 }
1128 }
1129 }
1130}
1131
1132pub fn assign_group_ids(sub: &mut Sub) -> usize {
1136 let mut counter = 0usize;
1137 for item in [&mut sub.before, &mut sub.not_before] {
1138 if let Some(i) = item {
1139 assign_in(i, &mut counter);
1140 }
1141 }
1142 assign_in(&mut sub.from, &mut counter);
1143 for item in [&mut sub.not_after, &mut sub.after] {
1144 if let Some(i) = item {
1145 assign_in(i, &mut counter);
1146 }
1147 }
1148 counter
1149}
1150
1151fn assign_in(item: &mut Item, counter: &mut usize) {
1152 match item {
1153 Item::CaptureGroup { id, inner } => {
1154 *counter += 1;
1155 *id = *counter;
1156 assign_in(inner, counter);
1157 }
1158 Item::Maybe(inner) | Item::Some(inner) => assign_in(inner, counter),
1159 Item::Concat(parts) => parts.iter_mut().for_each(|p| assign_in(p, counter)),
1160 Item::Set(entries) => entries.iter_mut().for_each(|e| assign_in(e, counter)),
1161 _ => {}
1162 }
1163}