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    #[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    Yield(Box<Expr>),
209    Wake(Box<Expr>),
210    Finish(Box<Expr>),
211    Fail(Box<Expr>),
212    BuiltinCall {
213        name: AstString,
214        args: Vec<Expr>,
215    },
216    Field {
217        target: Box<Expr>,
218        field: AstString,
219    },
220    Index {
221        target: Box<Expr>,
222        index: Box<Expr>,
223    },
224    Unary {
225        op: UnaryOp,
226        expr: Box<Expr>,
227    },
228    Binary {
229        left: Box<Expr>,
230        op: BinaryOp,
231        right: Box<Expr>,
232    },
233    TypeLiteral(Box<TypeExpr>),
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
237pub enum ListComprehensionClause {
238    For { binding: AstString, iterable: Expr },
239    If { condition: Expr },
240}
241
242impl Expr {
243    /// Yields every direct child expression of `self` in evaluation order.
244    ///
245    /// This is the single structural-traversal primitive: any pass that only
246    /// needs to recurse into the sub-expressions of a node (without caring
247    /// about the node's own kind) can fold over `children()` instead of
248    /// re-spelling the full `match`. Leaf nodes (`Null`, `Bool`, `Number`,
249    /// `String`, `Variable`, `Break`, `Continue`, `WaitSignal`,
250    /// `ResourceRef`, `ProcessRef`, `HostDescriptorConstructor` metadata, and
251    /// `TypeLiteral`) yield nothing.
252    ///
253    /// `Assign` includes any dynamic index expressions in its `target` path
254    /// (in path order) before the assigned value, matching the order in which
255    /// the compiler and linker visit them.
256    pub fn children(&self) -> ExprChildren<'_> {
257        let mut buffer = SmallExprVec::new();
258        match self {
259            Expr::Null
260            | Expr::Bool(_)
261            | Expr::Number(_)
262            | Expr::String(_)
263            | Expr::Variable(_)
264            | Expr::Break
265            | Expr::Continue
266            | Expr::WaitSignal { .. }
267            | Expr::ProcessRef { .. }
268            | Expr::ResourceRef(_)
269            | Expr::TypeLiteral(_) => {}
270            Expr::Block(expressions) | Expr::Tuple(expressions) | Expr::List(expressions) => {
271                buffer.extend(expressions.iter());
272            }
273            Expr::ListComprehension { element, clauses } => {
274                for clause in clauses {
275                    match clause {
276                        ListComprehensionClause::For { iterable, .. } => buffer.push(iterable),
277                        ListComprehensionClause::If { condition } => buffer.push(condition),
278                    }
279                }
280                buffer.push(element);
281            }
282            Expr::LabelAnnotated { expr, .. } => buffer.push(expr),
283            Expr::Record(entries) => buffer.extend(entries.iter().map(|(_, value)| value)),
284            Expr::Assign { target, expr } => {
285                for step in &target.steps {
286                    if let AssignPathStep::Index(index) = step {
287                        buffer.push(index);
288                    }
289                }
290                buffer.push(expr);
291            }
292            Expr::If {
293                condition,
294                then_block,
295                else_block,
296            } => {
297                buffer.push(condition);
298                buffer.push(then_block);
299                buffer.push(else_block);
300            }
301            Expr::For { iterable, body, .. } => {
302                buffer.push(iterable);
303                buffer.push(body);
304            }
305            Expr::While { condition, body } => {
306                buffer.push(condition);
307                buffer.push(body);
308            }
309            Expr::StartProcess(start) => buffer.extend(start.args.iter().map(|(_, value)| value)),
310            Expr::HostDescriptorConstructor { input, .. } => buffer.push(input),
311            Expr::ReceiverCall { receiver, args, .. } => {
312                buffer.push(receiver);
313                buffer.extend(args.iter());
314            }
315            Expr::SignalRun { run, payload, .. } => {
316                buffer.push(run);
317                buffer.push(payload);
318            }
319            Expr::Await(expr)
320            | Expr::SleepFor(expr)
321            | Expr::SleepUntil(expr)
322            | Expr::ResultUnwrap(expr)
323            | Expr::Cancel(expr)
324            | Expr::Print(expr)
325            | Expr::Yield(expr)
326            | Expr::Wake(expr)
327            | Expr::Fail(expr)
328            | Expr::Unary { expr, .. } => buffer.push(expr),
329            Expr::Finish(expr) => buffer.push(expr),
330            Expr::BuiltinCall { args, .. } => buffer.extend(args.iter()),
331            Expr::Field { target, .. } => buffer.push(target),
332            Expr::Index { target, index } => {
333                buffer.push(target);
334                buffer.push(index);
335            }
336            Expr::Binary { left, right, .. } => {
337                buffer.push(left);
338                buffer.push(right);
339            }
340        }
341        ExprChildren {
342            buffer,
343            position: 0,
344        }
345    }
346}
347
348type SmallExprVec<'expr> = smallvec::SmallVec<[&'expr Expr; 3]>;
349
350/// Iterator over the direct child expressions yielded by [`Expr::children`].
351pub struct ExprChildren<'expr> {
352    buffer: SmallExprVec<'expr>,
353    position: usize,
354}
355
356impl<'expr> Iterator for ExprChildren<'expr> {
357    type Item = &'expr Expr;
358
359    fn next(&mut self) -> Option<Self::Item> {
360        let item = self.buffer.get(self.position).copied();
361        if item.is_some() {
362            self.position += 1;
363        }
364        item
365    }
366
367    fn size_hint(&self) -> (usize, Option<usize>) {
368        let remaining = self.buffer.len() - self.position;
369        (remaining, Some(remaining))
370    }
371}
372
373impl ExactSizeIterator for ExprChildren<'_> {}
374
375pub trait ExprVisitor {
376    fn visit_expr(&mut self, expr: &Expr) {
377        walk_expr(self, expr);
378    }
379}
380
381pub fn walk_expr<V>(visitor: &mut V, expr: &Expr)
382where
383    V: ExprVisitor + ?Sized,
384{
385    for child in expr.children() {
386        visitor.visit_expr(child);
387    }
388}
389
390pub trait ExprFolder {
391    fn fold_expr(&mut self, expr: Expr) -> Expr {
392        fold_expr_children(self, expr)
393    }
394}
395
396pub fn fold_expr_children<F>(folder: &mut F, expr: Expr) -> Expr
397where
398    F: ExprFolder + ?Sized,
399{
400    match expr {
401        Expr::Block(expressions) => Expr::Block(
402            expressions
403                .into_iter()
404                .map(|expr| folder.fold_expr(expr))
405                .collect(),
406        ),
407        Expr::LabelAnnotated { label, expr } => Expr::LabelAnnotated {
408            label,
409            expr: Box::new(folder.fold_expr(*expr)),
410        },
411        Expr::Tuple(items) => Expr::Tuple(
412            items
413                .into_iter()
414                .map(|expr| folder.fold_expr(expr))
415                .collect(),
416        ),
417        Expr::List(items) => Expr::List(
418            items
419                .into_iter()
420                .map(|expr| folder.fold_expr(expr))
421                .collect(),
422        ),
423        Expr::ListComprehension { element, clauses } => Expr::ListComprehension {
424            element: Box::new(folder.fold_expr(*element)),
425            clauses: clauses
426                .into_iter()
427                .map(|clause| fold_list_comprehension_clause(folder, clause))
428                .collect(),
429        },
430        Expr::Record(entries) => Expr::Record(
431            entries
432                .into_iter()
433                .map(|(name, value)| (name, folder.fold_expr(value)))
434                .collect(),
435        ),
436        Expr::Assign { target, expr } => Expr::Assign {
437            target: fold_assign_target(folder, target),
438            expr: Box::new(folder.fold_expr(*expr)),
439        },
440        Expr::If {
441            condition,
442            then_block,
443            else_block,
444        } => Expr::If {
445            condition: Box::new(folder.fold_expr(*condition)),
446            then_block: Box::new(folder.fold_expr(*then_block)),
447            else_block: Box::new(folder.fold_expr(*else_block)),
448        },
449        Expr::For {
450            binding,
451            iterable,
452            body,
453        } => Expr::For {
454            binding,
455            iterable: Box::new(folder.fold_expr(*iterable)),
456            body: Box::new(folder.fold_expr(*body)),
457        },
458        Expr::While { condition, body } => Expr::While {
459            condition: Box::new(folder.fold_expr(*condition)),
460            body: Box::new(folder.fold_expr(*body)),
461        },
462        Expr::StartProcess(mut start) => {
463            start.args = start
464                .args
465                .into_iter()
466                .map(|(name, value)| (name, folder.fold_expr(value)))
467                .collect();
468            Expr::StartProcess(start)
469        }
470        Expr::ProcessRef { process } => Expr::ProcessRef { process },
471        Expr::HostDescriptorConstructor { type_name, input } => Expr::HostDescriptorConstructor {
472            type_name,
473            input: Box::new(folder.fold_expr(*input)),
474        },
475        Expr::ReceiverCall {
476            receiver,
477            operation,
478            args,
479        } => Expr::ReceiverCall {
480            receiver: Box::new(folder.fold_expr(*receiver)),
481            operation,
482            args: args
483                .into_iter()
484                .map(|expr| folder.fold_expr(expr))
485                .collect(),
486        },
487        Expr::Await(expr) => Expr::Await(Box::new(folder.fold_expr(*expr))),
488        Expr::SleepFor(expr) => Expr::SleepFor(Box::new(folder.fold_expr(*expr))),
489        Expr::SleepUntil(expr) => Expr::SleepUntil(Box::new(folder.fold_expr(*expr))),
490        Expr::SignalRun { run, name, payload } => Expr::SignalRun {
491            run: Box::new(folder.fold_expr(*run)),
492            name,
493            payload: Box::new(folder.fold_expr(*payload)),
494        },
495        Expr::ResultUnwrap(expr) => Expr::ResultUnwrap(Box::new(folder.fold_expr(*expr))),
496        Expr::Cancel(expr) => Expr::Cancel(Box::new(folder.fold_expr(*expr))),
497        Expr::Print(expr) => Expr::Print(Box::new(folder.fold_expr(*expr))),
498        Expr::Yield(expr) => Expr::Yield(Box::new(folder.fold_expr(*expr))),
499        Expr::Wake(expr) => Expr::Wake(Box::new(folder.fold_expr(*expr))),
500        Expr::Finish(expr) => Expr::Finish(Box::new(folder.fold_expr(*expr))),
501        Expr::Fail(expr) => Expr::Fail(Box::new(folder.fold_expr(*expr))),
502        Expr::BuiltinCall { name, args } => Expr::BuiltinCall {
503            name,
504            args: args
505                .into_iter()
506                .map(|expr| folder.fold_expr(expr))
507                .collect(),
508        },
509        Expr::Field { target, field } => Expr::Field {
510            target: Box::new(folder.fold_expr(*target)),
511            field,
512        },
513        Expr::Index { target, index } => Expr::Index {
514            target: Box::new(folder.fold_expr(*target)),
515            index: Box::new(folder.fold_expr(*index)),
516        },
517        Expr::Unary { op, expr } => Expr::Unary {
518            op,
519            expr: Box::new(folder.fold_expr(*expr)),
520        },
521        Expr::Binary { left, op, right } => Expr::Binary {
522            left: Box::new(folder.fold_expr(*left)),
523            op,
524            right: Box::new(folder.fold_expr(*right)),
525        },
526        leaf @ (Expr::Null
527        | Expr::Bool(_)
528        | Expr::Number(_)
529        | Expr::String(_)
530        | Expr::Variable(_)
531        | Expr::Break
532        | Expr::Continue
533        | Expr::ResourceRef(_)
534        | Expr::WaitSignal { .. }
535        | Expr::TypeLiteral(_)) => leaf,
536    }
537}
538
539fn fold_list_comprehension_clause<F>(
540    folder: &mut F,
541    clause: ListComprehensionClause,
542) -> ListComprehensionClause
543where
544    F: ExprFolder + ?Sized,
545{
546    match clause {
547        ListComprehensionClause::For { binding, iterable } => ListComprehensionClause::For {
548            binding,
549            iterable: folder.fold_expr(iterable),
550        },
551        ListComprehensionClause::If { condition } => ListComprehensionClause::If {
552            condition: folder.fold_expr(condition),
553        },
554    }
555}
556
557fn fold_assign_target<F>(folder: &mut F, target: AssignTarget) -> AssignTarget
558where
559    F: ExprFolder + ?Sized,
560{
561    AssignTarget {
562        root: target.root,
563        steps: target
564            .steps
565            .into_iter()
566            .map(|step| match step {
567                AssignPathStep::Field(field) => AssignPathStep::Field(field),
568                AssignPathStep::Index(index) => AssignPathStep::Index(folder.fold_expr(index)),
569            })
570            .collect(),
571    }
572}
573
574#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575pub enum TypeExpr {
576    Any,
577    Str,
578    Int,
579    Float,
580    Bool,
581    Dict,
582    /// The literal `null` type; usually only useful as part of a
583    /// `Union` (e.g. `str | null` for a nullable field).
584    Null,
585    Enum(Vec<AstString>),
586    List(Box<TypeExpr>),
587    Object(Vec<TypeField>),
588    Ref(AstString),
589    Process {
590        input: Box<TypeExpr>,
591        output: Box<TypeExpr>,
592        input_count: usize,
593    },
594    TriggerHandle(Box<TypeExpr>),
595    /// Union of alternative type shapes, e.g. `str | int | null`.
596    /// Always has two or more variants; single-variant parses collapse
597    /// to the underlying `TypeExpr` in the parser.
598    Union(Vec<TypeExpr>),
599}
600
601pub fn format_type_expr(ty: &TypeExpr) -> String {
602    match ty {
603        TypeExpr::Any => "any".to_string(),
604        TypeExpr::Str => "str".to_string(),
605        TypeExpr::Int => "int".to_string(),
606        TypeExpr::Float => "float".to_string(),
607        TypeExpr::Bool => "bool".to_string(),
608        TypeExpr::Dict => "dict".to_string(),
609        TypeExpr::Null => "null".to_string(),
610        TypeExpr::Enum(values) => format!(
611            "enum[{}]",
612            values
613                .iter()
614                .map(|value| format!("\"{value}\""))
615                .collect::<Vec<_>>()
616                .join(", ")
617        ),
618        TypeExpr::List(item) => format!("list[{}]", format_type_expr(item)),
619        TypeExpr::Object(fields) => {
620            let fields = fields
621                .iter()
622                .map(|field| {
623                    let optional = if field.optional { "?" } else { "" };
624                    format!(
625                        "{}: {}{}",
626                        field.name,
627                        format_type_expr(&field.ty),
628                        optional
629                    )
630                })
631                .collect::<Vec<_>>()
632                .join(", ");
633            format!("{{ {fields} }}")
634        }
635        TypeExpr::Ref(name) => name.to_string(),
636        TypeExpr::Process { input, output, .. } => {
637            format!(
638                "Process<{}, {}>",
639                format_type_expr(input),
640                format_type_expr(output)
641            )
642        }
643        TypeExpr::TriggerHandle(event) => {
644            format!("TriggerHandle<{}>", format_type_expr(event))
645        }
646        TypeExpr::Union(items) => items
647            .iter()
648            .map(format_type_expr)
649            .collect::<Vec<_>>()
650            .join(" | "),
651    }
652}
653
654impl fmt::Display for TypeExpr {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        f.write_str(&format_type_expr(self))
657    }
658}
659
660#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
661pub struct TypeField {
662    pub name: AstString,
663    pub ty: TypeExpr,
664    pub optional: bool,
665}
666
667#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
668pub struct ProcessStartExpr {
669    pub process: AstString,
670    pub args: Vec<(AstString, Expr)>,
671}
672
673#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
674pub struct ResourceRefExpr {
675    #[serde(default, skip_serializing_if = "Vec::is_empty")]
676    pub path: Vec<AstString>,
677    pub resource_type: AstString,
678    pub alias: AstString,
679}
680
681impl ResourceRefExpr {
682    pub fn unresolved(path: Vec<AstString>) -> Self {
683        Self {
684            path,
685            resource_type: AstString::default(),
686            alias: AstString::default(),
687        }
688    }
689
690    pub fn resolved(
691        path: Vec<AstString>,
692        resource_type: impl Into<AstString>,
693        alias: impl Into<AstString>,
694    ) -> Self {
695        Self {
696            path,
697            resource_type: resource_type.into(),
698            alias: alias.into(),
699        }
700    }
701
702    pub fn path_string(&self) -> String {
703        if self.path.is_empty() {
704            format!("{}.{}", self.resource_type, self.alias)
705        } else {
706            self.path
707                .iter()
708                .map(AstString::as_str)
709                .collect::<Vec<_>>()
710                .join(".")
711        }
712    }
713}
714
715#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
716pub enum UnaryOp {
717    Negate,
718    Not,
719}
720
721#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
722pub enum BinaryOp {
723    Add,
724    Subtract,
725    Multiply,
726    Divide,
727    Modulo,
728    Equal,
729    NotEqual,
730    Less,
731    LessEqual,
732    Greater,
733    GreaterEqual,
734    And,
735    Or,
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    #[test]
743    fn type_expr_formatting_covers_nested_shapes() {
744        let ty = TypeExpr::Object(vec![
745            TypeField {
746                name: "status".into(),
747                ty: TypeExpr::Enum(vec!["ok".into(), "err".into()]),
748                optional: false,
749            },
750            TypeField {
751                name: "tags".into(),
752                ty: TypeExpr::List(Box::new(TypeExpr::Str)),
753                optional: true,
754            },
755            TypeField {
756                name: "owner".into(),
757                ty: TypeExpr::Ref("User".into()),
758                optional: false,
759            },
760            TypeField {
761                name: "value".into(),
762                ty: TypeExpr::Union(vec![TypeExpr::Int, TypeExpr::Null]),
763                optional: false,
764            },
765        ]);
766
767        assert_eq!(
768            format_type_expr(&ty),
769            r#"{ status: enum["ok", "err"], tags: list[str]?, owner: User, value: int | null }"#
770        );
771        assert_eq!(ty.to_string(), format_type_expr(&ty));
772    }
773
774    fn var(name: &str) -> Expr {
775        Expr::Variable(name.into())
776    }
777
778    fn child_vars(expr: &Expr) -> Vec<String> {
779        expr.children()
780            .map(|child| match child {
781                Expr::Variable(name) => name.to_string(),
782                other => format!("{other:?}"),
783            })
784            .collect()
785    }
786
787    #[test]
788    fn children_yields_leaves_as_empty() {
789        for leaf in [
790            Expr::Null,
791            Expr::Bool(true),
792            Expr::Number(1.0),
793            Expr::String("s".into()),
794            var("x"),
795            Expr::Break,
796            Expr::Continue,
797            Expr::WaitSignal {
798                name: "ready".into(),
799            },
800            Expr::TypeLiteral(Box::new(TypeExpr::Str)),
801        ] {
802            let children: Vec<_> = leaf.children().collect();
803            assert!(children.is_empty(), "{leaf:?} should have no children");
804        }
805    }
806
807    #[test]
808    fn children_yields_composite_subexpressions_in_order() {
809        let block = Expr::Block(vec![var("a"), var("b"), var("c")]);
810        assert_eq!(child_vars(&block), ["a", "b", "c"]);
811
812        let record = Expr::Record(vec![("k1".into(), var("v1")), ("k2".into(), var("v2"))]);
813        assert_eq!(child_vars(&record), ["v1", "v2"]);
814
815        let if_expr = Expr::If {
816            condition: Box::new(var("cond")),
817            then_block: Box::new(var("then")),
818            else_block: Box::new(var("else")),
819        };
820        assert_eq!(child_vars(&if_expr), ["cond", "then", "else"]);
821
822        let while_expr = Expr::While {
823            condition: Box::new(var("cond")),
824            body: Box::new(var("body")),
825        };
826        assert_eq!(child_vars(&while_expr), ["cond", "body"]);
827
828        let receiver = Expr::ReceiverCall {
829            receiver: Box::new(var("recv")),
830            operation: "op".into(),
831            args: vec![var("arg0"), var("arg1")],
832        };
833        assert_eq!(child_vars(&receiver), ["recv", "arg0", "arg1"]);
834
835        let binary = Expr::Binary {
836            left: Box::new(var("left")),
837            op: BinaryOp::Add,
838            right: Box::new(var("right")),
839        };
840        assert_eq!(child_vars(&binary), ["left", "right"]);
841    }
842
843    #[test]
844    fn children_yields_assign_index_steps_before_value() {
845        let assign = Expr::Assign {
846            target: AssignTarget {
847                root: "root".into(),
848                steps: vec![
849                    AssignPathStep::Field("field".into()),
850                    AssignPathStep::Index(var("idx")),
851                ],
852            },
853            expr: Box::new(var("value")),
854        };
855        // Field steps contribute no child expressions; the dynamic index is
856        // yielded before the assigned value.
857        assert_eq!(child_vars(&assign), ["idx", "value"]);
858    }
859
860    #[test]
861    fn children_handles_finish() {
862        assert_eq!(child_vars(&Expr::Finish(Box::new(var("done")))), ["done"]);
863    }
864
865    #[test]
866    fn children_size_hint_is_exact() {
867        let block = Expr::Block(vec![var("a"), var("b"), var("c"), var("d")]);
868        let iter = block.children();
869        assert_eq!(iter.len(), 4);
870        assert_eq!(iter.size_hint(), (4, Some(4)));
871    }
872
873    #[test]
874    fn visitor_walks_descendants_through_single_child_boundary() {
875        struct VariableCollector(Vec<String>);
876
877        impl ExprVisitor for VariableCollector {
878            fn visit_expr(&mut self, expr: &Expr) {
879                if let Expr::Variable(name) = expr {
880                    self.0.push(name.to_string());
881                }
882                walk_expr(self, expr);
883            }
884        }
885
886        let expr = Expr::While {
887            condition: Box::new(var("ready")),
888            body: Box::new(Expr::Block(vec![
889                Expr::Assign {
890                    target: AssignTarget {
891                        root: "items".into(),
892                        steps: vec![AssignPathStep::Index(var("idx"))],
893                    },
894                    expr: Box::new(var("value")),
895                },
896                Expr::Finish(Box::new(var("done"))),
897            ])),
898        };
899
900        let mut collector = VariableCollector(Vec::new());
901        collector.visit_expr(&expr);
902
903        assert_eq!(collector.0, ["ready", "idx", "value", "done"]);
904    }
905
906    #[test]
907    fn folder_reconstructs_owned_expr_trees() {
908        struct RenameVariables;
909
910        impl ExprFolder for RenameVariables {
911            fn fold_expr(&mut self, expr: Expr) -> Expr {
912                match expr {
913                    Expr::Variable(name) => Expr::Variable(format!("renamed_{name}").into()),
914                    other => fold_expr_children(self, other),
915                }
916            }
917        }
918
919        let expr = Expr::Assign {
920            target: AssignTarget {
921                root: "items".into(),
922                steps: vec![AssignPathStep::Index(var("idx"))],
923            },
924            expr: Box::new(Expr::List(vec![var("first"), var("second")])),
925        };
926
927        let mut folder = RenameVariables;
928        let folded = folder.fold_expr(expr);
929
930        let Expr::Assign { target, expr } = folded else {
931            panic!("expected assign");
932        };
933        assert!(matches!(
934            target.steps.as_slice(),
935            [AssignPathStep::Index(Expr::Variable(name))] if name.as_str() == "renamed_idx"
936        ));
937        let Expr::List(items) = *expr else {
938            panic!("expected list");
939        };
940        assert_eq!(items, vec![var("renamed_first"), var("renamed_second")]);
941    }
942}