Skip to main content

assura_codegen/
expr.rs

1//! Expression-to-Rust code generation.
2//!
3//! Translates Assura AST expressions into Rust source code strings.
4
5use super::*;
6use assura_ast::{ExprFolder, fold_arg_list, fold_joined, literal_to_string};
7
8/// Hygienic variable name for the contract return value in generated Rust.
9///
10/// Uses a clearly compiler-generated prefix to avoid collision with
11/// user-defined variables.
12pub(crate) const RESULT_VAR: &str = "__assura_result";
13
14/// Prefix for `old(expr)` pre-state snapshot variables in generated Rust.
15pub(crate) const OLD_VAR_PREFIX: &str = "__assura_old_";
16
17/// Heuristic: returns true if the expression is likely a numeric value
18/// (variable, constant, literal, or arithmetic). Used to decide whether to
19/// emit `i128::from(...)` casts for cross-width comparisons.
20pub(crate) fn is_numeric_expr(expr: &SpExpr) -> bool {
21    match &expr.node {
22        Expr::Ident(_) | Expr::Literal(Literal::Int(_)) | Expr::Literal(Literal::Float(_)) => true,
23        Expr::Field(_, _) => true,
24        Expr::BinOp { op, .. } => op.is_arithmetic(),
25        Expr::UnaryOp {
26            op: UnaryOp::Neg, ..
27        } => true,
28        Expr::Old(e) | Expr::Cast { expr: e, .. } => is_numeric_expr(e),
29        Expr::Call { .. } | Expr::MethodCall { .. } | Expr::Index { .. } => true,
30        Expr::Let { body, .. } => is_numeric_expr(body),
31        Expr::If { then_branch, .. } => is_numeric_expr(then_branch),
32        Expr::Match { arms, .. } => arms.first().is_some_and(|a| is_numeric_expr(&a.body)),
33        // These are definitively not numeric expressions
34        Expr::Literal(Literal::Str(_) | Literal::Bool(_))
35        | Expr::UnaryOp {
36            op: UnaryOp::Not, ..
37        }
38        | Expr::Forall { .. }
39        | Expr::Exists { .. }
40        | Expr::List(_)
41        | Expr::Tuple(_)
42        | Expr::Ghost(_)
43        | Expr::Apply { .. }
44        | Expr::Block(_)
45        | Expr::Raw(_) => false,
46    }
47}
48
49/// Resolve an ordering clause body to a Rust `std::sync::atomic::Ordering` variant name.
50pub(crate) fn resolve_ordering_variant(body: &SpExpr) -> Option<&'static str> {
51    use assura_ast::MemoryOrdering;
52    let s = match &body.node {
53        Expr::Ident(s) => s.as_str(),
54        Expr::Raw(tokens) => {
55            return tokens
56                .iter()
57                .find_map(|t| MemoryOrdering::parse(t))
58                .map(|o| o.to_rust_ordering());
59        }
60        _ => return None,
61    };
62    MemoryOrdering::parse(s).map(|o| o.to_rust_ordering())
63}
64
65/// Convert an Assura `Expr` to a Rust expression string.
66pub(crate) fn expr_to_rust(expr: &SpExpr) -> String {
67    RustCodegenFolder {
68        static_context: false,
69    }
70    .fold_expr(expr)
71}
72
73/// Convert an Assura `Expr` to a Rust expression for use in const context.
74///
75/// Compared to [`expr_to_rust`], this variant:
76/// - Does not rename `result` to the compiler-generated result variable
77/// - Does not emit `i128::from()` casts for numeric comparisons
78/// - Does not translate `Implies`/`In`/`NotIn`/`Concat` to Rust idioms
79/// - Emits quantifiers as `/* forall ... */ true` comments
80/// - Passes `old(expr)` through as the inner expression
81/// - Uses simplified match patterns (`Ctor(..)` instead of field bindings)
82pub fn expr_to_rust_static(expr: &SpExpr) -> String {
83    RustCodegenFolder {
84        static_context: true,
85    }
86    .fold_expr(expr)
87}
88
89/// Unified Assura-to-Rust expression folder.
90///
91/// When `static_context` is false (runtime), produces full Rust code with
92/// `result` renaming, `i128` casts, quantifier-to-iterator translation, etc.
93/// When `static_context` is true, produces simplified Rust suitable for
94/// const/static contexts.
95struct RustCodegenFolder {
96    static_context: bool,
97}
98
99impl ExprFolder for RustCodegenFolder {
100    type Output = String;
101
102    fn fold_literal(&mut self, lit: &Literal) -> String {
103        literal_to_string(lit)
104    }
105
106    fn fold_ident(&mut self, name: &str) -> String {
107        if !self.static_context && name == "result" {
108            RESULT_VAR.to_string()
109        } else {
110            name.to_string()
111        }
112    }
113
114    fn fold_field(&mut self, base: &SpExpr, field: &str) -> String {
115        format!("{}.{field}", self.fold_expr(base))
116    }
117
118    fn fold_method_call(&mut self, receiver: &SpExpr, method: &str, args: &[SpExpr]) -> String {
119        format!(
120            "{}.{method}({})",
121            self.fold_expr(receiver),
122            fold_arg_list(self, args)
123        )
124    }
125
126    fn fold_call(&mut self, func: &SpExpr, args: &[SpExpr]) -> String {
127        format!("{}({})", self.fold_expr(func), fold_arg_list(self, args))
128    }
129
130    fn fold_index(&mut self, base: &SpExpr, index: &SpExpr) -> String {
131        format!("{}[{}]", self.fold_expr(base), self.fold_expr(index))
132    }
133
134    fn fold_binop(&mut self, lhs: &SpExpr, op: &BinOp, rhs: &SpExpr) -> String {
135        if !self.static_context {
136            // Runtime: handle special operators and i128 casts
137            match op {
138                BinOp::Implies => {
139                    return format!("(!{} || {})", self.fold_expr(lhs), self.fold_expr(rhs));
140                }
141                BinOp::In => {
142                    return format!("{}.contains(&{})", self.fold_expr(rhs), self.fold_expr(lhs));
143                }
144                BinOp::NotIn => {
145                    return format!(
146                        "!{}.contains(&{})",
147                        self.fold_expr(rhs),
148                        self.fold_expr(lhs)
149                    );
150                }
151                BinOp::Concat => {
152                    return format!(
153                        "[{}, {}].concat()",
154                        self.fold_expr(lhs),
155                        self.fold_expr(rhs)
156                    );
157                }
158                _ => {}
159            }
160            let op_s = op.as_rust_str();
161            if op.is_ordering_comparison() && is_numeric_expr(lhs) && is_numeric_expr(rhs) {
162                return format!(
163                    "(i128::from({}) {op_s} i128::from({}))",
164                    self.fold_expr(lhs),
165                    self.fold_expr(rhs)
166                );
167            }
168        }
169        format!(
170            "({} {} {})",
171            self.fold_expr(lhs),
172            op.as_rust_str(),
173            self.fold_expr(rhs)
174        )
175    }
176
177    fn fold_unary_op(&mut self, op: &UnaryOp, inner: &SpExpr) -> String {
178        let inner_s = self.fold_expr(inner);
179        if self.static_context {
180            format!("{}{inner_s}", op.as_rust_str())
181        } else {
182            format!("({}{})", op.as_rust_str(), inner_s)
183        }
184    }
185
186    fn fold_old(&mut self, inner: &SpExpr) -> String {
187        if self.static_context {
188            // old() is verification-only; emit inner for static
189            self.fold_expr(inner)
190        } else {
191            format!("{OLD_VAR_PREFIX}{}", old_var_name(inner))
192        }
193    }
194
195    fn fold_forall(&mut self, var: &str, domain: &SpExpr, body: &SpExpr) -> String {
196        if self.static_context {
197            let d = self.fold_expr(domain);
198            let b = self.fold_expr(body);
199            format!("/* forall {var} in {d}: {b} */ true")
200        } else {
201            format!(
202                "{}.iter().all(|{var}| {})",
203                self.fold_expr(domain),
204                self.fold_expr(body)
205            )
206        }
207    }
208
209    fn fold_exists(&mut self, var: &str, domain: &SpExpr, body: &SpExpr) -> String {
210        if self.static_context {
211            let d = self.fold_expr(domain);
212            let b = self.fold_expr(body);
213            format!("/* exists {var} in {d}: {b} */ true")
214        } else {
215            format!(
216                "{}.iter().any(|{var}| {})",
217                self.fold_expr(domain),
218                self.fold_expr(body)
219            )
220        }
221    }
222
223    fn fold_if(&mut self, cond: &SpExpr, then_br: &SpExpr, else_br: Option<&SpExpr>) -> String {
224        match else_br {
225            Some(eb) => format!(
226                "if {} {{ {} }} else {{ {} }}",
227                self.fold_expr(cond),
228                self.fold_expr(then_br),
229                self.fold_expr(eb)
230            ),
231            None => format!(
232                "if {} {{ {} }}",
233                self.fold_expr(cond),
234                self.fold_expr(then_br)
235            ),
236        }
237    }
238
239    fn fold_list(&mut self, items: &[SpExpr]) -> String {
240        format!("vec![{}]", fold_joined(self, items, ", "))
241    }
242
243    fn fold_cast(&mut self, inner: &SpExpr, ty: &str) -> String {
244        if self.static_context {
245            format!("({} as {ty})", self.fold_expr(inner))
246        } else {
247            format!("({} as {})", self.fold_expr(inner), map_type_token(ty))
248        }
249    }
250
251    fn fold_block(&mut self, exprs: &[SpExpr]) -> String {
252        fold_joined(self, exprs, " ")
253    }
254
255    fn fold_ghost(&mut self, inner: &SpExpr) -> String {
256        if self.static_context {
257            let s = self.fold_expr(inner);
258            format!("/* ghost: {s} */ ()")
259        } else {
260            "/* ghost erased */()".to_string()
261        }
262    }
263
264    fn fold_apply(&mut self, lemma_name: &str, args: &[SpExpr]) -> String {
265        if self.static_context {
266            format!("/* apply {lemma_name}({}) */ ()", fold_arg_list(self, args))
267        } else {
268            format!("/* lemma {lemma_name} applied */")
269        }
270    }
271
272    fn fold_let(&mut self, name: &str, value: &SpExpr, body: &SpExpr) -> String {
273        format!(
274            "{{ let {} = {}; {} }}",
275            name,
276            self.fold_expr(value),
277            self.fold_expr(body)
278        )
279    }
280
281    fn fold_match(&mut self, scrutinee: &SpExpr, arms: &[assura_ast::MatchArm]) -> String {
282        let scrut = self.fold_expr(scrutinee);
283        if self.static_context {
284            let arm_strs: Vec<String> = arms
285                .iter()
286                .map(|arm| {
287                    let pat = match &arm.pattern {
288                        assura_ast::Pattern::Ident(s) => s.clone(),
289                        assura_ast::Pattern::Wildcard => "_".to_string(),
290                        assura_ast::Pattern::Literal(lit) => match lit {
291                            Literal::Int(s) | Literal::Float(s) => s.clone(),
292                            Literal::Str(s) => format!("\"{s}\""),
293                            Literal::Bool(b) => b.to_string(),
294                        },
295                        assura_ast::Pattern::Constructor { name, fields } => {
296                            if fields.is_empty() {
297                                name.clone()
298                            } else {
299                                format!("{name}(..)")
300                            }
301                        }
302                        assura_ast::Pattern::Tuple(pats) => {
303                            let ps: Vec<&str> = pats.iter().map(|_| "_").collect();
304                            format!("({})", ps.join(", "))
305                        }
306                    };
307                    let body = self.fold_expr(&arm.body);
308                    format!("{pat} => {body}")
309                })
310                .collect();
311            format!("match {scrut} {{ {} }}", arm_strs.join(", "))
312        } else {
313            let arms_code: Vec<String> = arms
314                .iter()
315                .map(|arm| {
316                    let pat = match &arm.pattern {
317                        assura_ast::Pattern::Ident(name) => name.clone(),
318                        assura_ast::Pattern::Wildcard => "_".into(),
319                        assura_ast::Pattern::Literal(lit) => match lit {
320                            Literal::Int(s) | Literal::Float(s) => s.clone(),
321                            Literal::Str(s) => format!("\"{s}\""),
322                            Literal::Bool(b) => b.to_string(),
323                        },
324                        assura_ast::Pattern::Constructor { name, fields } => {
325                            if fields.is_empty() {
326                                name.clone()
327                            } else {
328                                let fs: Vec<String> = fields.iter().map(pattern_to_rust).collect();
329                                format!("{name}({})", fs.join(", "))
330                            }
331                        }
332                        assura_ast::Pattern::Tuple(pats) => {
333                            let ps: Vec<String> = pats.iter().map(pattern_to_rust).collect();
334                            format!("({})", ps.join(", "))
335                        }
336                    };
337                    let body = self.fold_expr(&arm.body);
338                    format!("    {pat} => {body},")
339                })
340                .collect();
341            let has_wildcard = arms.iter().any(|arm| {
342                matches!(
343                    &arm.pattern,
344                    assura_ast::Pattern::Wildcard | assura_ast::Pattern::Ident(_)
345                )
346            });
347            if !has_wildcard {
348                let mut all_arms = arms_code;
349                all_arms.push("    _ => unreachable!(\"non-exhaustive match\"),".to_string());
350                format!("match {} {{\n{}\n}}", scrut, all_arms.join("\n"))
351            } else {
352                format!("match {} {{\n{}\n}}", scrut, arms_code.join("\n"))
353            }
354        }
355    }
356
357    fn fold_tuple(&mut self, items: &[SpExpr]) -> String {
358        format!("({})", fold_joined(self, items, ", "))
359    }
360
361    fn fold_raw(&mut self, tokens: &[String]) -> String {
362        if self.static_context {
363            let clean: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
364            if clean.len() == 1 {
365                return clean[0].to_string();
366            }
367            if clean.len() >= 2 && clean[0] == "=" {
368                return clean[1..].join(" ");
369            }
370            clean.join(" ")
371        } else {
372            raw_tokens_to_rust(tokens)
373        }
374    }
375}
376
377/// Convert raw token sequences to Rust, handling quantifier patterns.
378///
379/// Detects `forall var in domain: body` and `exists var in domain: body`
380/// in raw tokens and translates them to `.iter().all(|var| body)` /
381/// `.iter().any(|var| body)` respectively. Falls back to joined tokens
382/// for non-quantifier sequences.
383pub(crate) fn raw_tokens_to_rust(tokens: &[String]) -> String {
384    if tokens.is_empty() {
385        return String::new();
386    }
387    // Detect: forall/exists VAR in DOMAIN : BODY
388    let first = tokens[0].as_str();
389    if matches!(first, "forall" | "exists")
390        && tokens.len() >= 5
391        && let Some(in_pos) = tokens[1..].iter().position(|t| t == "in")
392    {
393        let in_pos = in_pos + 1; // offset from tokens[0]
394        let var = &tokens[1..in_pos].join("_");
395        // Find the colon that separates domain from body
396        if let Some(colon_offset) = tokens[in_pos + 1..].iter().position(|t| t == ":") {
397            let colon_pos = in_pos + 1 + colon_offset;
398            let domain_tokens = &tokens[in_pos + 1..colon_pos];
399            let body_tokens = &tokens[colon_pos + 1..];
400
401            let domain = {
402                let mapped: Vec<&str> = domain_tokens.iter().map(|t| map_type_token(t)).collect();
403                smart_join_type_tokens(&mapped)
404            };
405            let body = raw_tokens_to_rust(body_tokens);
406
407            let method = if first == "forall" { "all" } else { "any" };
408            return format!("{domain}.iter().{method}(|{var}| {body})");
409        }
410    }
411
412    // Strip typestate annotations: `expr @ State` -> `true /* typestate: expr @ State */`
413    if let Some(at_pos) = tokens.iter().position(|t| t == "@") {
414        let before = &tokens[..at_pos];
415        let after = &tokens[at_pos + 1..];
416        let expr_s = raw_tokens_to_rust(before);
417        let state_s = after.join(" ");
418        return format!("true /* typestate: {expr_s} @ {state_s} */");
419    }
420
421    // Check for `result` keyword — replace with result var
422    let mapped: Vec<String> = tokens
423        .iter()
424        .map(|t| {
425            if t == "result" {
426                RESULT_VAR.to_string()
427            } else {
428                map_type_token(t).to_string()
429            }
430        })
431        .collect();
432    let refs: Vec<&str> = mapped.iter().map(|s| s.as_str()).collect();
433    smart_join_type_tokens(&refs)
434}
435
436// ---------------------------------------------------------------------------
437// old(expr) support
438// ---------------------------------------------------------------------------
439
440/// Check if an expression string contains patterns that would fail to compile
441/// against placeholder stub types:
442/// - Any field access (a.b) since stub types have no fields
443/// - Method calls on unknown objects
444/// - References to `{RESULT_VAR}.field`
445pub(crate) fn has_deep_field_access(expr: &str) -> bool {
446    // Detect struct field access like `state.head.extra` that would fail on stub types.
447    // Exclude method-call chains like `.iter().all()`, `.len()`, `.clone()` which are
448    // standard library methods and work fine.
449    let method_names = [
450        "iter",
451        "all",
452        "any",
453        "map",
454        "filter",
455        "len",
456        "is_empty",
457        "clone",
458        "count",
459        "sum",
460        "collect",
461        "flat_map",
462        "zip",
463        "enumerate",
464        "take",
465        "skip",
466        "find",
467        "fold",
468        "for_each",
469        "min",
470        "max",
471        "contains",
472        "position",
473        "into_iter",
474        "as_ref",
475        "as_mut",
476        "unwrap",
477        "unwrap_or",
478        "expect",
479        "ok",
480        "err",
481        "is_some",
482        "is_none",
483        "is_ok",
484        "is_err",
485    ];
486    for word in expr.split(|c: char| !c.is_alphanumeric() && c != '.' && c != '_') {
487        if word.contains('.') && !word.is_empty() {
488            let parts: Vec<&str> = word.split('.').collect();
489            if parts.len() >= 2
490                && parts[0]
491                    .chars()
492                    .next()
493                    .is_some_and(|c| c.is_alphabetic() || c == '_')
494            {
495                // Check if ALL dotted segments after the first are known method names
496                let all_methods = parts[1..].iter().all(|p| method_names.contains(p));
497                if !all_methods {
498                    return true;
499                }
500            }
501        }
502    }
503    // Result var field references (but not .iter(), etc.)
504    let result_dot = format!("{RESULT_VAR}.");
505    if expr.contains(&result_dot) {
506        // Check if all occurrences are followed by method calls
507        for chunk in expr.split(&result_dot) {
508            if chunk.is_empty() {
509                continue;
510            }
511            let after: String = chunk
512                .chars()
513                .take_while(|c| c.is_alphanumeric() || *c == '_')
514                .collect();
515            if !after.is_empty() && !method_names.contains(&after.as_str()) {
516                return true;
517            }
518        }
519    }
520    false
521}
522
523/// Convert a pattern to Rust pattern syntax.
524pub(crate) fn pattern_to_rust(pat: &assura_ast::Pattern) -> String {
525    match pat {
526        assura_ast::Pattern::Ident(name) => name.clone(),
527        assura_ast::Pattern::Wildcard => "_".into(),
528        assura_ast::Pattern::Literal(lit) => match lit {
529            Literal::Int(s) | Literal::Float(s) => s.clone(),
530            Literal::Str(s) => format!("\"{s}\""),
531            Literal::Bool(b) => b.to_string(),
532        },
533        assura_ast::Pattern::Constructor { name, fields } => {
534            if fields.is_empty() {
535                name.clone()
536            } else {
537                let fs: Vec<String> = fields.iter().map(pattern_to_rust).collect();
538                format!("{name}({})", fs.join(", "))
539            }
540        }
541        assura_ast::Pattern::Tuple(pats) => {
542            let ps: Vec<String> = pats.iter().map(pattern_to_rust).collect();
543            format!("({})", ps.join(", "))
544        }
545    }
546}
547
548pub(crate) fn old_var_name(expr: &SpExpr) -> String {
549    match &expr.node {
550        Expr::Ident(s) => s.clone(),
551        Expr::Field(recv, field) => format!("{}_{field}", old_var_name(recv)),
552        Expr::Call { func, .. } => old_var_name(func),
553        Expr::MethodCall {
554            receiver, method, ..
555        } => format!("{}_{method}", old_var_name(receiver)),
556        Expr::Index { expr: e, .. } => format!("{}_idx", old_var_name(e)),
557        Expr::Literal(lit) => match lit {
558            Literal::Int(s) | Literal::Float(s) => format!("lit_{s}"),
559            Literal::Str(s) => format!("lit_{}", s.trim_matches('"')),
560            Literal::Bool(b) => format!("lit_{b}"),
561        },
562        Expr::BinOp { lhs, op, rhs } => {
563            format!(
564                "{}_{}_{}",
565                old_var_name(lhs),
566                op.as_ident(),
567                old_var_name(rhs)
568            )
569        }
570        Expr::UnaryOp { op, expr: e } => {
571            let prefix = match op {
572                UnaryOp::Neg => "neg",
573                UnaryOp::Not => "not",
574            };
575            format!("{prefix}_{}", old_var_name(e))
576        }
577        Expr::Old(inner) => old_var_name(inner),
578        Expr::Cast { expr: e, .. } => old_var_name(e),
579        Expr::Ghost(inner) => format!("ghost_{}", old_var_name(inner)),
580        Expr::Forall { var, .. } => format!("forall_{var}"),
581        Expr::Exists { var, .. } => format!("exists_{var}"),
582        Expr::If { cond, .. } => format!("if_{}", old_var_name(cond)),
583        Expr::Let { name, .. } => format!("let_{name}"),
584        Expr::Match { scrutinee, .. } => format!("match_{}", old_var_name(scrutinee)),
585        Expr::Apply { lemma_name, .. } => format!("apply_{lemma_name}"),
586        Expr::List(_) => "list".to_string(),
587        Expr::Tuple(_) => "tuple".to_string(),
588        Expr::Block(exprs) => {
589            if let Some(first) = exprs.first() {
590                old_var_name(first)
591            } else {
592                "block".to_string()
593            }
594        }
595        Expr::Raw(tokens) => {
596            if let Some(first) = tokens.first() {
597                first.clone()
598            } else {
599                "raw".to_string()
600            }
601        }
602    }
603}
604
605/// Walk an expression tree and collect all `old(inner)` sub-expressions.
606/// Returns `(var_name, rust_expr)` pairs for generating pre-state snapshots.
607pub(crate) fn collect_old_exprs(expr: &SpExpr) -> Vec<(String, String)> {
608    let mut result = Vec::new();
609    collect_old_exprs_inner(expr, &mut result);
610    result
611}
612
613pub(crate) fn collect_old_exprs_inner(expr: &SpExpr, out: &mut Vec<(String, String)>) {
614    match &expr.node {
615        Expr::Old(inner) => {
616            let var = old_var_name(inner);
617            let rust = expr_to_rust(inner);
618            // Avoid duplicates
619            if !out.iter().any(|(v, _)| v == &var) {
620                out.push((var, rust));
621            }
622            // Also recurse into the inner expression (in case of nested old)
623            collect_old_exprs_inner(inner, out);
624        }
625        Expr::BinOp { lhs, rhs, .. } => {
626            collect_old_exprs_inner(lhs, out);
627            collect_old_exprs_inner(rhs, out);
628        }
629        Expr::UnaryOp { expr: e, .. } | Expr::Field(e, _) | Expr::Cast { expr: e, .. } => {
630            collect_old_exprs_inner(e, out);
631        }
632        Expr::Call { func, args } => {
633            collect_old_exprs_inner(func, out);
634            for a in args {
635                collect_old_exprs_inner(a, out);
636            }
637        }
638        Expr::MethodCall { receiver, args, .. } => {
639            collect_old_exprs_inner(receiver, out);
640            for a in args {
641                collect_old_exprs_inner(a, out);
642            }
643        }
644        Expr::Index { expr: e, index } => {
645            collect_old_exprs_inner(e, out);
646            collect_old_exprs_inner(index, out);
647        }
648        Expr::Forall { domain, body, .. } | Expr::Exists { domain, body, .. } => {
649            collect_old_exprs_inner(domain, out);
650            collect_old_exprs_inner(body, out);
651        }
652        Expr::If {
653            cond,
654            then_branch,
655            else_branch,
656        } => {
657            collect_old_exprs_inner(cond, out);
658            collect_old_exprs_inner(then_branch, out);
659            if let Some(eb) = else_branch {
660                collect_old_exprs_inner(eb, out);
661            }
662        }
663        Expr::List(items) | Expr::Block(items) => {
664            for item in items {
665                collect_old_exprs_inner(item, out);
666            }
667        }
668        Expr::Ghost(inner) => {
669            // Ghost blocks are erased but may reference old() in
670            // their verification expressions.
671            collect_old_exprs_inner(inner, out);
672        }
673        Expr::Apply { args, .. } => {
674            // Apply is erased but may reference old() in arguments.
675            for a in args {
676                collect_old_exprs_inner(a, out);
677            }
678        }
679        Expr::Match { scrutinee, arms } => {
680            collect_old_exprs_inner(scrutinee, out);
681            for arm in arms {
682                collect_old_exprs_inner(&arm.body, out);
683            }
684        }
685        Expr::Let { value, body, .. } => {
686            collect_old_exprs_inner(value, out);
687            collect_old_exprs_inner(body, out);
688        }
689        Expr::Tuple(elems) => {
690            for e in elems {
691                collect_old_exprs_inner(e, out);
692            }
693        }
694        // Leaf nodes: no old() inside
695        Expr::Literal(_) | Expr::Ident(_) | Expr::Raw(_) => {}
696    }
697}
698#[cfg(test)]
699#[path = "expr_tests.rs"]
700mod tests;