Skip to main content

lashlang/
ast.rs

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    /// Yields every direct child expression of `self` in evaluation order.
212    ///
213    /// This is the single structural-traversal primitive: any pass that only
214    /// needs to recurse into the sub-expressions of a node (without caring
215    /// about the node's own kind) can fold over `children()` instead of
216    /// re-spelling the full `match`. Leaf nodes (`Null`, `Bool`, `Number`,
217    /// `String`, `Variable`, `Break`, `Continue`, `WaitSignal`,
218    /// `ResourceRef`, `ProcessRef`, `HostValueConstructor` metadata, and
219    /// `TypeLiteral`) yield nothing.
220    ///
221    /// `Assign` includes any dynamic index expressions in its `target` path
222    /// (in path order) before the assigned value, matching the order in which
223    /// the compiler and linker visit them.
224    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
313/// Iterator over the direct child expressions yielded by [`Expr::children`].
314pub 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    /// The literal `null` type; usually only useful as part of a
515    /// `Union` (e.g. `str | null` for a nullable field).
516    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 of alternative type shapes, e.g. `str | int | null`.
528    /// Always has two or more variants; single-variant parses collapse
529    /// to the underlying `TypeExpr` in the parser.
530    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        // Field steps contribute no child expressions; the dynamic index is
786        // yielded before the assigned value.
787        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}