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