1use compact_str::CompactString;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5use crate::lexer::Span;
6
7pub type AstString = CompactString;
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct Program {
11 #[serde(default, skip_serializing_if = "Vec::is_empty")]
12 pub declarations: Vec<Declaration>,
13 pub main: Expr,
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
15 pub declaration_spans: Vec<Span>,
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
17 pub expression_spans: Vec<Span>,
18}
19
20impl Program {
21 pub fn block(expressions: Vec<Expr>) -> Self {
22 Self {
23 declarations: Vec::new(),
24 main: Expr::Block(expressions),
25 declaration_spans: Vec::new(),
26 expression_spans: Vec::new(),
27 }
28 }
29
30 pub(crate) fn module_with_spans(
31 declarations: Vec<Declaration>,
32 declaration_spans: Vec<Span>,
33 expressions: Vec<Expr>,
34 expression_spans: Vec<Span>,
35 ) -> Self {
36 Self {
37 declarations,
38 main: Expr::Block(expressions),
39 declaration_spans,
40 expression_spans,
41 }
42 }
43
44 pub fn process(&self, name: &str) -> Option<&ProcessDecl> {
45 self.declarations
46 .iter()
47 .find_map(|declaration| match declaration {
48 Declaration::Process(process) if process.name.as_str() == name => Some(process),
49 _ => None,
50 })
51 }
52}
53
54impl PartialEq for Program {
55 fn eq(&self, other: &Self) -> bool {
56 self.declarations == other.declarations && self.main == other.main
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub enum Declaration {
62 Type(TypeDecl),
63 Process(ProcessDecl),
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67pub struct TypeDecl {
68 pub name: AstString,
69 pub ty: TypeExpr,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct ProcessDecl {
74 pub name: AstString,
75 pub params: Vec<ProcessParam>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub return_ty: Option<TypeExpr>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub label: Option<LabelMetadata>,
80 pub body: Expr,
81}
82
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84pub struct ProcessParam {
85 pub name: AstString,
86 pub ty: TypeExpr,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct LabelMetadata {
91 pub title: AstString,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub description: Option<AstString>,
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
97pub struct AssignTarget {
98 pub root: AstString,
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub steps: Vec<AssignPathStep>,
101}
102
103impl AssignTarget {
104 pub fn variable(root: AstString) -> Self {
105 Self {
106 root,
107 steps: Vec::new(),
108 }
109 }
110
111 pub fn is_simple(&self) -> bool {
112 self.steps.is_empty()
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117pub enum AssignPathStep {
118 Field(AstString),
119 Index(Expr),
120}
121
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub enum Expr {
124 Block(Vec<Expr>),
125 LabelAnnotated {
126 label: LabelMetadata,
127 expr: Box<Expr>,
128 },
129 Null,
130 Bool(bool),
131 Number(f64),
132 String(AstString),
133 Variable(AstString),
134 List(Vec<Expr>),
135 Record(Vec<(AstString, Expr)>),
136 Assign {
137 target: AssignTarget,
138 expr: Box<Expr>,
139 },
140 If {
141 condition: Box<Expr>,
142 then_block: Box<Expr>,
143 else_block: Box<Expr>,
144 },
145 For {
146 binding: AstString,
147 iterable: Box<Expr>,
148 body: Box<Expr>,
149 },
150 While {
151 condition: Box<Expr>,
152 body: Box<Expr>,
153 },
154 Break,
155 Continue,
156 StartProcess(ProcessStartExpr),
157 ProcessRef {
158 process: AstString,
159 },
160 HostValueConstructor {
161 type_name: AstString,
162 input: Box<Expr>,
163 },
164 ResourceRef(ResourceRefExpr),
165 ReceiverCall {
166 receiver: Box<Expr>,
167 operation: AstString,
168 args: Vec<Expr>,
169 },
170 Await(Box<Expr>),
171 SleepFor(Box<Expr>),
172 SleepUntil(Box<Expr>),
173 WaitSignal,
174 SignalRun {
175 run: Box<Expr>,
176 payload: Box<Expr>,
177 },
178 ResultUnwrap(Box<Expr>),
179 Cancel(Box<Expr>),
180 Print(Box<Expr>),
181 Submit(Option<Box<Expr>>),
182 Yield(Box<Expr>),
183 Wake(Box<Expr>),
184 Finish(Option<Box<Expr>>),
185 Fail(Box<Expr>),
186 BuiltinCall {
187 name: AstString,
188 args: Vec<Expr>,
189 },
190 Field {
191 target: Box<Expr>,
192 field: AstString,
193 },
194 Index {
195 target: Box<Expr>,
196 index: Box<Expr>,
197 },
198 Unary {
199 op: UnaryOp,
200 expr: Box<Expr>,
201 },
202 Binary {
203 left: Box<Expr>,
204 op: BinaryOp,
205 right: Box<Expr>,
206 },
207 TypeLiteral(Box<TypeExpr>),
208}
209
210impl Expr {
211 pub fn children(&self) -> ExprChildren<'_> {
225 let mut buffer = SmallExprVec::new();
226 match self {
227 Expr::Null
228 | Expr::Bool(_)
229 | Expr::Number(_)
230 | Expr::String(_)
231 | Expr::Variable(_)
232 | Expr::Break
233 | Expr::Continue
234 | Expr::WaitSignal
235 | Expr::ProcessRef { .. }
236 | Expr::ResourceRef(_)
237 | Expr::TypeLiteral(_) => {}
238 Expr::Block(expressions) | Expr::List(expressions) => {
239 buffer.extend(expressions.iter());
240 }
241 Expr::LabelAnnotated { expr, .. } => buffer.push(expr),
242 Expr::Record(entries) => buffer.extend(entries.iter().map(|(_, value)| value)),
243 Expr::Assign { target, expr } => {
244 for step in &target.steps {
245 if let AssignPathStep::Index(index) = step {
246 buffer.push(index);
247 }
248 }
249 buffer.push(expr);
250 }
251 Expr::If {
252 condition,
253 then_block,
254 else_block,
255 } => {
256 buffer.push(condition);
257 buffer.push(then_block);
258 buffer.push(else_block);
259 }
260 Expr::For { iterable, body, .. } => {
261 buffer.push(iterable);
262 buffer.push(body);
263 }
264 Expr::While { condition, body } => {
265 buffer.push(condition);
266 buffer.push(body);
267 }
268 Expr::StartProcess(start) => buffer.extend(start.args.iter().map(|(_, value)| value)),
269 Expr::HostValueConstructor { input, .. } => buffer.push(input),
270 Expr::ReceiverCall { receiver, args, .. } => {
271 buffer.push(receiver);
272 buffer.extend(args.iter());
273 }
274 Expr::SignalRun { run, payload } => {
275 buffer.push(run);
276 buffer.push(payload);
277 }
278 Expr::Await(expr)
279 | Expr::SleepFor(expr)
280 | Expr::SleepUntil(expr)
281 | Expr::ResultUnwrap(expr)
282 | Expr::Cancel(expr)
283 | Expr::Print(expr)
284 | Expr::Yield(expr)
285 | Expr::Wake(expr)
286 | Expr::Fail(expr)
287 | Expr::Unary { expr, .. } => buffer.push(expr),
288 Expr::Submit(expr) | Expr::Finish(expr) => {
289 if let Some(expr) = expr {
290 buffer.push(expr);
291 }
292 }
293 Expr::BuiltinCall { args, .. } => buffer.extend(args.iter()),
294 Expr::Field { target, .. } => buffer.push(target),
295 Expr::Index { target, index } => {
296 buffer.push(target);
297 buffer.push(index);
298 }
299 Expr::Binary { left, right, .. } => {
300 buffer.push(left);
301 buffer.push(right);
302 }
303 }
304 ExprChildren {
305 buffer,
306 position: 0,
307 }
308 }
309}
310
311type SmallExprVec<'expr> = smallvec::SmallVec<[&'expr Expr; 3]>;
312
313pub struct ExprChildren<'expr> {
315 buffer: SmallExprVec<'expr>,
316 position: usize,
317}
318
319impl<'expr> Iterator for ExprChildren<'expr> {
320 type Item = &'expr Expr;
321
322 fn next(&mut self) -> Option<Self::Item> {
323 let item = self.buffer.get(self.position).copied();
324 if item.is_some() {
325 self.position += 1;
326 }
327 item
328 }
329
330 fn size_hint(&self) -> (usize, Option<usize>) {
331 let remaining = self.buffer.len() - self.position;
332 (remaining, Some(remaining))
333 }
334}
335
336impl ExactSizeIterator for ExprChildren<'_> {}
337
338pub trait ExprVisitor {
339 fn visit_expr(&mut self, expr: &Expr) {
340 walk_expr(self, expr);
341 }
342}
343
344pub fn walk_expr<V>(visitor: &mut V, expr: &Expr)
345where
346 V: ExprVisitor + ?Sized,
347{
348 for child in expr.children() {
349 visitor.visit_expr(child);
350 }
351}
352
353pub trait ExprFolder {
354 fn fold_expr(&mut self, expr: Expr) -> Expr {
355 fold_expr_children(self, expr)
356 }
357}
358
359pub fn fold_expr_children<F>(folder: &mut F, expr: Expr) -> Expr
360where
361 F: ExprFolder + ?Sized,
362{
363 match expr {
364 Expr::Block(expressions) => Expr::Block(
365 expressions
366 .into_iter()
367 .map(|expr| folder.fold_expr(expr))
368 .collect(),
369 ),
370 Expr::LabelAnnotated { label, expr } => Expr::LabelAnnotated {
371 label,
372 expr: Box::new(folder.fold_expr(*expr)),
373 },
374 Expr::List(items) => Expr::List(
375 items
376 .into_iter()
377 .map(|expr| folder.fold_expr(expr))
378 .collect(),
379 ),
380 Expr::Record(entries) => Expr::Record(
381 entries
382 .into_iter()
383 .map(|(name, value)| (name, folder.fold_expr(value)))
384 .collect(),
385 ),
386 Expr::Assign { target, expr } => Expr::Assign {
387 target: fold_assign_target(folder, target),
388 expr: Box::new(folder.fold_expr(*expr)),
389 },
390 Expr::If {
391 condition,
392 then_block,
393 else_block,
394 } => Expr::If {
395 condition: Box::new(folder.fold_expr(*condition)),
396 then_block: Box::new(folder.fold_expr(*then_block)),
397 else_block: Box::new(folder.fold_expr(*else_block)),
398 },
399 Expr::For {
400 binding,
401 iterable,
402 body,
403 } => Expr::For {
404 binding,
405 iterable: Box::new(folder.fold_expr(*iterable)),
406 body: Box::new(folder.fold_expr(*body)),
407 },
408 Expr::While { condition, body } => Expr::While {
409 condition: Box::new(folder.fold_expr(*condition)),
410 body: Box::new(folder.fold_expr(*body)),
411 },
412 Expr::StartProcess(mut start) => {
413 start.args = start
414 .args
415 .into_iter()
416 .map(|(name, value)| (name, folder.fold_expr(value)))
417 .collect();
418 Expr::StartProcess(start)
419 }
420 Expr::ProcessRef { process } => Expr::ProcessRef { process },
421 Expr::HostValueConstructor { type_name, input } => Expr::HostValueConstructor {
422 type_name,
423 input: Box::new(folder.fold_expr(*input)),
424 },
425 Expr::ReceiverCall {
426 receiver,
427 operation,
428 args,
429 } => Expr::ReceiverCall {
430 receiver: Box::new(folder.fold_expr(*receiver)),
431 operation,
432 args: args
433 .into_iter()
434 .map(|expr| folder.fold_expr(expr))
435 .collect(),
436 },
437 Expr::Await(expr) => Expr::Await(Box::new(folder.fold_expr(*expr))),
438 Expr::SleepFor(expr) => Expr::SleepFor(Box::new(folder.fold_expr(*expr))),
439 Expr::SleepUntil(expr) => Expr::SleepUntil(Box::new(folder.fold_expr(*expr))),
440 Expr::SignalRun { run, payload } => Expr::SignalRun {
441 run: Box::new(folder.fold_expr(*run)),
442 payload: Box::new(folder.fold_expr(*payload)),
443 },
444 Expr::ResultUnwrap(expr) => Expr::ResultUnwrap(Box::new(folder.fold_expr(*expr))),
445 Expr::Cancel(expr) => Expr::Cancel(Box::new(folder.fold_expr(*expr))),
446 Expr::Print(expr) => Expr::Print(Box::new(folder.fold_expr(*expr))),
447 Expr::Submit(expr) => Expr::Submit(expr.map(|expr| Box::new(folder.fold_expr(*expr)))),
448 Expr::Yield(expr) => Expr::Yield(Box::new(folder.fold_expr(*expr))),
449 Expr::Wake(expr) => Expr::Wake(Box::new(folder.fold_expr(*expr))),
450 Expr::Finish(expr) => Expr::Finish(expr.map(|expr| Box::new(folder.fold_expr(*expr)))),
451 Expr::Fail(expr) => Expr::Fail(Box::new(folder.fold_expr(*expr))),
452 Expr::BuiltinCall { name, args } => Expr::BuiltinCall {
453 name,
454 args: args
455 .into_iter()
456 .map(|expr| folder.fold_expr(expr))
457 .collect(),
458 },
459 Expr::Field { target, field } => Expr::Field {
460 target: Box::new(folder.fold_expr(*target)),
461 field,
462 },
463 Expr::Index { target, index } => Expr::Index {
464 target: Box::new(folder.fold_expr(*target)),
465 index: Box::new(folder.fold_expr(*index)),
466 },
467 Expr::Unary { op, expr } => Expr::Unary {
468 op,
469 expr: Box::new(folder.fold_expr(*expr)),
470 },
471 Expr::Binary { left, op, right } => Expr::Binary {
472 left: Box::new(folder.fold_expr(*left)),
473 op,
474 right: Box::new(folder.fold_expr(*right)),
475 },
476 leaf @ (Expr::Null
477 | Expr::Bool(_)
478 | Expr::Number(_)
479 | Expr::String(_)
480 | Expr::Variable(_)
481 | Expr::Break
482 | Expr::Continue
483 | Expr::ResourceRef(_)
484 | Expr::WaitSignal
485 | Expr::TypeLiteral(_)) => leaf,
486 }
487}
488
489fn fold_assign_target<F>(folder: &mut F, target: AssignTarget) -> AssignTarget
490where
491 F: ExprFolder + ?Sized,
492{
493 AssignTarget {
494 root: target.root,
495 steps: target
496 .steps
497 .into_iter()
498 .map(|step| match step {
499 AssignPathStep::Field(field) => AssignPathStep::Field(field),
500 AssignPathStep::Index(index) => AssignPathStep::Index(folder.fold_expr(index)),
501 })
502 .collect(),
503 }
504}
505
506#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
507pub enum TypeExpr {
508 Any,
509 Str,
510 Int,
511 Float,
512 Bool,
513 Dict,
514 Null,
517 Enum(Vec<AstString>),
518 List(Box<TypeExpr>),
519 Object(Vec<TypeField>),
520 Ref(AstString),
521 Process {
522 input: Box<TypeExpr>,
523 output: Box<TypeExpr>,
524 input_count: usize,
525 },
526 TriggerHandle(Box<TypeExpr>),
527 Union(Vec<TypeExpr>),
531}
532
533pub fn format_type_expr(ty: &TypeExpr) -> String {
534 match ty {
535 TypeExpr::Any => "any".to_string(),
536 TypeExpr::Str => "str".to_string(),
537 TypeExpr::Int => "int".to_string(),
538 TypeExpr::Float => "float".to_string(),
539 TypeExpr::Bool => "bool".to_string(),
540 TypeExpr::Dict => "dict".to_string(),
541 TypeExpr::Null => "null".to_string(),
542 TypeExpr::Enum(values) => format!(
543 "enum[{}]",
544 values
545 .iter()
546 .map(|value| format!("\"{value}\""))
547 .collect::<Vec<_>>()
548 .join(", ")
549 ),
550 TypeExpr::List(item) => format!("list[{}]", format_type_expr(item)),
551 TypeExpr::Object(fields) => {
552 let fields = fields
553 .iter()
554 .map(|field| {
555 let optional = if field.optional { "?" } else { "" };
556 format!(
557 "{}: {}{}",
558 field.name,
559 format_type_expr(&field.ty),
560 optional
561 )
562 })
563 .collect::<Vec<_>>()
564 .join(", ");
565 format!("{{ {fields} }}")
566 }
567 TypeExpr::Ref(name) => name.to_string(),
568 TypeExpr::Process { input, output, .. } => {
569 format!(
570 "Process<{}, {}>",
571 format_type_expr(input),
572 format_type_expr(output)
573 )
574 }
575 TypeExpr::TriggerHandle(event) => {
576 format!("TriggerHandle<{}>", format_type_expr(event))
577 }
578 TypeExpr::Union(items) => items
579 .iter()
580 .map(format_type_expr)
581 .collect::<Vec<_>>()
582 .join(" | "),
583 }
584}
585
586impl fmt::Display for TypeExpr {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 f.write_str(&format_type_expr(self))
589 }
590}
591
592#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
593pub struct TypeField {
594 pub name: AstString,
595 pub ty: TypeExpr,
596 pub optional: bool,
597}
598
599#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
600pub struct ProcessStartExpr {
601 pub process: AstString,
602 pub args: Vec<(AstString, Expr)>,
603}
604
605#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
606pub struct ResourceRefExpr {
607 #[serde(default, skip_serializing_if = "Vec::is_empty")]
608 pub path: Vec<AstString>,
609 pub resource_type: AstString,
610 pub alias: AstString,
611}
612
613impl ResourceRefExpr {
614 pub fn unresolved(path: Vec<AstString>) -> Self {
615 Self {
616 path,
617 resource_type: AstString::default(),
618 alias: AstString::default(),
619 }
620 }
621
622 pub fn resolved(
623 path: Vec<AstString>,
624 resource_type: impl Into<AstString>,
625 alias: impl Into<AstString>,
626 ) -> Self {
627 Self {
628 path,
629 resource_type: resource_type.into(),
630 alias: alias.into(),
631 }
632 }
633
634 pub fn path_string(&self) -> String {
635 if self.path.is_empty() {
636 format!("{}.{}", self.resource_type, self.alias)
637 } else {
638 self.path
639 .iter()
640 .map(AstString::as_str)
641 .collect::<Vec<_>>()
642 .join(".")
643 }
644 }
645}
646
647#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
648pub enum UnaryOp {
649 Negate,
650 Not,
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
654pub enum BinaryOp {
655 Add,
656 Subtract,
657 Multiply,
658 Divide,
659 Modulo,
660 Equal,
661 NotEqual,
662 Less,
663 LessEqual,
664 Greater,
665 GreaterEqual,
666 And,
667 Or,
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn type_expr_formatting_covers_nested_shapes() {
676 let ty = TypeExpr::Object(vec![
677 TypeField {
678 name: "status".into(),
679 ty: TypeExpr::Enum(vec!["ok".into(), "err".into()]),
680 optional: false,
681 },
682 TypeField {
683 name: "tags".into(),
684 ty: TypeExpr::List(Box::new(TypeExpr::Str)),
685 optional: true,
686 },
687 TypeField {
688 name: "owner".into(),
689 ty: TypeExpr::Ref("User".into()),
690 optional: false,
691 },
692 TypeField {
693 name: "value".into(),
694 ty: TypeExpr::Union(vec![TypeExpr::Int, TypeExpr::Null]),
695 optional: false,
696 },
697 ]);
698
699 assert_eq!(
700 format_type_expr(&ty),
701 r#"{ status: enum["ok", "err"], tags: list[str]?, owner: User, value: int | null }"#
702 );
703 assert_eq!(ty.to_string(), format_type_expr(&ty));
704 }
705
706 fn var(name: &str) -> Expr {
707 Expr::Variable(name.into())
708 }
709
710 fn child_vars(expr: &Expr) -> Vec<String> {
711 expr.children()
712 .map(|child| match child {
713 Expr::Variable(name) => name.to_string(),
714 other => format!("{other:?}"),
715 })
716 .collect()
717 }
718
719 #[test]
720 fn children_yields_leaves_as_empty() {
721 for leaf in [
722 Expr::Null,
723 Expr::Bool(true),
724 Expr::Number(1.0),
725 Expr::String("s".into()),
726 var("x"),
727 Expr::Break,
728 Expr::Continue,
729 Expr::WaitSignal,
730 Expr::TypeLiteral(Box::new(TypeExpr::Str)),
731 ] {
732 let children: Vec<_> = leaf.children().collect();
733 assert!(children.is_empty(), "{leaf:?} should have no children");
734 }
735 }
736
737 #[test]
738 fn children_yields_composite_subexpressions_in_order() {
739 let block = Expr::Block(vec![var("a"), var("b"), var("c")]);
740 assert_eq!(child_vars(&block), ["a", "b", "c"]);
741
742 let record = Expr::Record(vec![("k1".into(), var("v1")), ("k2".into(), var("v2"))]);
743 assert_eq!(child_vars(&record), ["v1", "v2"]);
744
745 let if_expr = Expr::If {
746 condition: Box::new(var("cond")),
747 then_block: Box::new(var("then")),
748 else_block: Box::new(var("else")),
749 };
750 assert_eq!(child_vars(&if_expr), ["cond", "then", "else"]);
751
752 let while_expr = Expr::While {
753 condition: Box::new(var("cond")),
754 body: Box::new(var("body")),
755 };
756 assert_eq!(child_vars(&while_expr), ["cond", "body"]);
757
758 let receiver = Expr::ReceiverCall {
759 receiver: Box::new(var("recv")),
760 operation: "op".into(),
761 args: vec![var("arg0"), var("arg1")],
762 };
763 assert_eq!(child_vars(&receiver), ["recv", "arg0", "arg1"]);
764
765 let binary = Expr::Binary {
766 left: Box::new(var("left")),
767 op: BinaryOp::Add,
768 right: Box::new(var("right")),
769 };
770 assert_eq!(child_vars(&binary), ["left", "right"]);
771 }
772
773 #[test]
774 fn children_yields_assign_index_steps_before_value() {
775 let assign = Expr::Assign {
776 target: AssignTarget {
777 root: "root".into(),
778 steps: vec![
779 AssignPathStep::Field("field".into()),
780 AssignPathStep::Index(var("idx")),
781 ],
782 },
783 expr: Box::new(var("value")),
784 };
785 assert_eq!(child_vars(&assign), ["idx", "value"]);
788 }
789
790 #[test]
791 fn children_handles_optional_finish_and_submit() {
792 assert!(Expr::Submit(None).children().next().is_none());
793 assert!(Expr::Finish(None).children().next().is_none());
794 assert_eq!(
795 child_vars(&Expr::Submit(Some(Box::new(var("done"))))),
796 ["done"]
797 );
798 }
799
800 #[test]
801 fn children_size_hint_is_exact() {
802 let block = Expr::Block(vec![var("a"), var("b"), var("c"), var("d")]);
803 let iter = block.children();
804 assert_eq!(iter.len(), 4);
805 assert_eq!(iter.size_hint(), (4, Some(4)));
806 }
807
808 #[test]
809 fn visitor_walks_descendants_through_single_child_boundary() {
810 struct VariableCollector(Vec<String>);
811
812 impl ExprVisitor for VariableCollector {
813 fn visit_expr(&mut self, expr: &Expr) {
814 if let Expr::Variable(name) = expr {
815 self.0.push(name.to_string());
816 }
817 walk_expr(self, expr);
818 }
819 }
820
821 let expr = Expr::While {
822 condition: Box::new(var("ready")),
823 body: Box::new(Expr::Block(vec![
824 Expr::Assign {
825 target: AssignTarget {
826 root: "items".into(),
827 steps: vec![AssignPathStep::Index(var("idx"))],
828 },
829 expr: Box::new(var("value")),
830 },
831 Expr::Submit(Some(Box::new(var("done")))),
832 ])),
833 };
834
835 let mut collector = VariableCollector(Vec::new());
836 collector.visit_expr(&expr);
837
838 assert_eq!(collector.0, ["ready", "idx", "value", "done"]);
839 }
840
841 #[test]
842 fn folder_reconstructs_owned_expr_trees() {
843 struct RenameVariables;
844
845 impl ExprFolder for RenameVariables {
846 fn fold_expr(&mut self, expr: Expr) -> Expr {
847 match expr {
848 Expr::Variable(name) => Expr::Variable(format!("renamed_{name}").into()),
849 other => fold_expr_children(self, other),
850 }
851 }
852 }
853
854 let expr = Expr::Assign {
855 target: AssignTarget {
856 root: "items".into(),
857 steps: vec![AssignPathStep::Index(var("idx"))],
858 },
859 expr: Box::new(Expr::List(vec![var("first"), var("second")])),
860 };
861
862 let mut folder = RenameVariables;
863 let folded = folder.fold_expr(expr);
864
865 let Expr::Assign { target, expr } = folded else {
866 panic!("expected assign");
867 };
868 assert!(matches!(
869 target.steps.as_slice(),
870 [AssignPathStep::Index(Expr::Variable(name))] if name.as_str() == "renamed_idx"
871 ));
872 let Expr::List(items) = *expr else {
873 panic!("expected list");
874 };
875 assert_eq!(items, vec![var("renamed_first"), var("renamed_second")]);
876 }
877}