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    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    /// Yields every direct child expression of `self` in evaluation order.
245    ///
246    /// This is the single structural-traversal primitive: any pass that only
247    /// needs to recurse into the sub-expressions of a node (without caring
248    /// about the node's own kind) can fold over `children()` instead of
249    /// re-spelling the full `match`. Leaf nodes (`Null`, `Bool`, `Number`,
250    /// `String`, `Variable`, `Break`, `Continue`, `WaitSignal`,
251    /// `ResourceRef`, `ProcessRef`, `HostDescriptorConstructor` metadata, and
252    /// `TypeLiteral`) yield nothing.
253    ///
254    /// `Assign` includes any dynamic index expressions in its `target` path
255    /// (in path order) before the assigned value, matching the order in which
256    /// the compiler and linker visit them.
257    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
355/// Iterator over the direct child expressions yielded by [`Expr::children`].
356pub 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    /// The literal `null` type; usually only useful as part of a
589    /// `Union` (e.g. `str | null` for a nullable field).
590    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 of alternative type shapes, e.g. `str | int | null`.
602    /// Always has two or more variants; single-variant parses collapse
603    /// to the underlying `TypeExpr` in the parser.
604    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        // Field steps contribute no child expressions; the dynamic index is
862        // yielded before the assigned value.
863        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}