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 std::collections::HashSet;
6
7use super::*;
8use assura_ast::{ExprFolder, fold_arg_list, fold_joined, literal_to_string};
9
10/// Hygienic variable name for the contract return value in generated Rust.
11///
12/// Uses a clearly compiler-generated prefix to avoid collision with
13/// user-defined variables.
14pub(crate) const RESULT_VAR: &str = "__assura_result";
15
16/// Prefix for `old(expr)` pre-state snapshot variables in generated Rust.
17pub(crate) const OLD_VAR_PREFIX: &str = "__assura_old_";
18
19/// Returns true if the expression contains a literal that exceeds i128 range
20/// (e.g. u128::MAX). Such literals cannot be wrapped in `i128::from(...)`.
21fn has_u128_literal(expr: &SpExpr) -> bool {
22    match &expr.node {
23        Expr::Literal(Literal::Int(s)) => s.parse::<i128>().is_err() && s.parse::<u128>().is_ok(),
24        Expr::BinOp { lhs, rhs, .. } => has_u128_literal(lhs) || has_u128_literal(rhs),
25        Expr::UnaryOp { expr: e, .. }
26        | Expr::Old(e)
27        | Expr::Cast { expr: e, .. }
28        | Expr::Field(e, _) => has_u128_literal(e),
29        _ => false,
30    }
31}
32
33/// Returns true if the expression tree contains a Float literal or references
34/// a variable known to be float-typed. Used to skip `i128::from()` wrapping
35/// since `f64` does not implement `Into<i128>`.
36fn has_float_expr(expr: &SpExpr, float_vars: &HashSet<String>) -> bool {
37    match &expr.node {
38        Expr::Literal(Literal::Float(_)) => true,
39        Expr::Ident(name) => float_vars.contains(name.as_str()),
40        Expr::BinOp { lhs, rhs, .. } => {
41            has_float_expr(lhs, float_vars) || has_float_expr(rhs, float_vars)
42        }
43        Expr::UnaryOp { expr: e, .. }
44        | Expr::Old(e)
45        | Expr::Cast { expr: e, .. }
46        | Expr::Field(e, _) => has_float_expr(e, float_vars),
47        Expr::MethodCall { receiver, args, .. } => {
48            has_float_expr(receiver, float_vars)
49                || args.iter().any(|a| has_float_expr(a, float_vars))
50        }
51        Expr::Call { args, .. } => args.iter().any(|a| has_float_expr(a, float_vars)),
52        Expr::Let { body, .. } => has_float_expr(body, float_vars),
53        Expr::If {
54            then_branch,
55            else_branch,
56            ..
57        } => {
58            has_float_expr(then_branch, float_vars)
59                || else_branch
60                    .as_ref()
61                    .is_some_and(|e| has_float_expr(e, float_vars))
62        }
63        _ => false,
64    }
65}
66
67/// Returns true if the folded Rust string already contains i128 widening.
68/// Used to detect branch type mismatches in if/match expressions where
69/// one branch gets i128::from() from arithmetic widening but the other
70/// stays as a plain variable (i64).
71fn has_inner_i128(folded: &str) -> bool {
72    folded.contains("i128::from(") || folded.contains("_i128")
73}
74
75/// Heuristic: returns true if the expression is likely a numeric value
76/// (variable, constant, literal, or arithmetic). Used to decide whether to
77/// emit `i128::from(...)` casts for cross-width comparisons.
78pub(crate) fn is_numeric_expr(expr: &SpExpr) -> bool {
79    match &expr.node {
80        Expr::Ident(_) | Expr::Literal(Literal::Int(_)) | Expr::Literal(Literal::Float(_)) => true,
81        Expr::Field(_, _) => true,
82        Expr::BinOp { op, .. } => op.is_arithmetic(),
83        Expr::UnaryOp {
84            op: UnaryOp::Neg, ..
85        } => true,
86        Expr::Old(e) | Expr::Cast { expr: e, .. } => is_numeric_expr(e),
87        Expr::Call { .. } | Expr::MethodCall { .. } | Expr::Index { .. } => true,
88        Expr::Let { body, .. } => is_numeric_expr(body),
89        Expr::If { then_branch, .. } => is_numeric_expr(then_branch),
90        Expr::Match { arms, .. } => arms.first().is_some_and(|a| is_numeric_expr(&a.body)),
91        // These are definitively not numeric expressions
92        Expr::Literal(Literal::Str(_) | Literal::Bool(_))
93        | Expr::UnaryOp {
94            op: UnaryOp::Not, ..
95        }
96        | Expr::Forall { .. }
97        | Expr::Exists { .. }
98        | Expr::List(_)
99        | Expr::Tuple(_)
100        | Expr::Ghost(_)
101        | Expr::Apply { .. }
102        | Expr::Block(_)
103        | Expr::Raw(_) => false,
104    }
105}
106
107/// Returns true if the domain expression is a range (`a..b` via `BinOp::Range`).
108///
109/// Ranges in Rust implement `IntoIterator` but not `iter()` directly, so
110/// quantifiers over ranges must use `.into_iter()` instead of `.iter().copied()`.
111fn is_range_domain(expr: &SpExpr) -> bool {
112    matches!(
113        &expr.node,
114        Expr::BinOp {
115            op: BinOp::Range,
116            ..
117        }
118    )
119}
120
121/// Returns true if the domain expression is an abstract mathematical type
122/// identifier (Int, Nat, Float, Bool, String) that cannot be iterated at runtime.
123///
124/// Quantifiers like `forall x in Int: ...` are verification-only; at runtime
125/// they are emitted as `true` with a comment.
126fn is_abstract_type_domain(expr: &SpExpr) -> bool {
127    match &expr.node {
128        Expr::Ident(name) => matches!(
129            name.as_str(),
130            "Int" | "Nat" | "Float" | "Bool" | "String" | "Bytes" | "Unit"
131        ),
132        Expr::Raw(tokens) if tokens.len() == 1 => matches!(
133            tokens[0].as_str(),
134            "Int" | "Nat" | "Float" | "Bool" | "String" | "Bytes" | "Unit"
135        ),
136        _ => false,
137    }
138}
139
140/// Resolve an ordering clause body to a Rust `std::sync::atomic::Ordering` variant name.
141pub(crate) fn resolve_ordering_variant(body: &SpExpr) -> Option<&'static str> {
142    use assura_ast::MemoryOrdering;
143    let s = match &body.node {
144        Expr::Ident(s) => s.as_str(),
145        Expr::Raw(tokens) => {
146            return tokens
147                .iter()
148                .find_map(|t| MemoryOrdering::parse(t))
149                .map(|o| o.to_rust_ordering());
150        }
151        _ => return None,
152    };
153    MemoryOrdering::parse(s).map(|o| o.to_rust_ordering())
154}
155
156/// Convert an Assura `Expr` to a Rust expression string.
157pub(crate) fn expr_to_rust(expr: &SpExpr) -> String {
158    RustCodegenFolder {
159        static_context: false,
160        float_vars: HashSet::new(),
161    }
162    .fold_expr(expr)
163}
164
165/// Like [`expr_to_rust`] but with knowledge of which variables are float-typed.
166/// Comparisons and arithmetic involving these variables use direct `f64`
167/// operations instead of `i128::from()` widening.
168pub(crate) fn expr_to_rust_with_floats(expr: &SpExpr, float_vars: HashSet<String>) -> String {
169    RustCodegenFolder {
170        static_context: false,
171        float_vars,
172    }
173    .fold_expr(expr)
174}
175
176/// Convert an Assura `Expr` to a Rust expression for use in const context.
177///
178/// Compared to [`expr_to_rust`], this variant:
179/// - Does not rename `result` to the compiler-generated result variable
180/// - Does not emit `i128::from()` casts for numeric comparisons
181/// - Does not translate `Implies`/`In`/`NotIn`/`Concat` to Rust idioms
182/// - Emits quantifiers as `/* forall ... */ true` comments
183/// - Passes `old(expr)` through as the inner expression
184/// - Uses simplified match patterns (`Ctor(..)` instead of field bindings)
185pub fn expr_to_rust_static(expr: &SpExpr) -> String {
186    RustCodegenFolder {
187        static_context: true,
188        float_vars: HashSet::new(),
189    }
190    .fold_expr(expr)
191}
192
193/// Unified Assura-to-Rust expression folder.
194///
195/// When `static_context` is false (runtime), produces full Rust code with
196/// `result` renaming, `i128` casts, quantifier-to-iterator translation, etc.
197/// When `static_context` is true, produces simplified Rust suitable for
198/// const/static contexts.
199struct RustCodegenFolder {
200    static_context: bool,
201    /// Names of variables known to be `Float` (`f64` in Rust). When a
202    /// comparison or arithmetic involves a float variable or literal,
203    /// `i128::from()` wrapping is skipped (f64 does not implement `Into<i128>`).
204    float_vars: HashSet<String>,
205}
206
207impl ExprFolder for RustCodegenFolder {
208    type Output = String;
209
210    fn fold_literal(&mut self, lit: &Literal) -> String {
211        match lit {
212            // Suffix large integer literals so they are not inferred as i32
213            // inside `i128::from(...)` wrappings. Values within i32 range
214            // are emitted without a suffix. Values in i64 range get `_i64`.
215            // Values exceeding i64 range get `_i128`.
216            Literal::Int(s) => {
217                if let Ok(v) = s.parse::<i128>() {
218                    if v > i128::from(i64::MAX) || v < i128::from(i64::MIN) {
219                        return format!("{s}_i128");
220                    }
221                    if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) {
222                        return format!("{s}_i64");
223                    }
224                } else if s.parse::<u128>().is_ok() {
225                    // Value exceeds i128 range (e.g. u128::MAX); emit as u128.
226                    return format!("{s}_u128");
227                }
228                s.clone()
229            }
230            _ => literal_to_string(lit),
231        }
232    }
233
234    fn fold_ident(&mut self, name: &str) -> String {
235        if !self.static_context && name == "result" {
236            RESULT_VAR.to_string()
237        } else {
238            name.to_string()
239        }
240    }
241
242    fn fold_field(&mut self, base: &SpExpr, field: &str) -> String {
243        format!("{}.{field}", self.fold_expr(base))
244    }
245
246    fn fold_method_call(&mut self, receiver: &SpExpr, method: &str, args: &[SpExpr]) -> String {
247        // Assura `length`/`len`/`size` map to Rust `.len()`; Nat is u64.
248        if args.is_empty() && matches!(method, "length" | "len" | "size") {
249            return format!("{}.len() as u64", self.fold_expr(receiver));
250        }
251        format!(
252            "{}.{method}({})",
253            self.fold_expr(receiver),
254            fold_arg_list(self, args)
255        )
256    }
257
258    fn fold_call(&mut self, func: &SpExpr, args: &[SpExpr]) -> String {
259        // Map common pure numeric builtins to Rust methods / associated functions.
260        if let Expr::Ident(name) = &func.node {
261            match (name.as_str(), args.len()) {
262                ("abs", 1) => return format!("{}.abs()", self.fold_expr(&args[0])),
263                ("min", 2) => {
264                    return format!(
265                        "{}.min({})",
266                        self.fold_expr(&args[0]),
267                        self.fold_expr(&args[1])
268                    );
269                }
270                ("max", 2) => {
271                    return format!(
272                        "{}.max({})",
273                        self.fold_expr(&args[0]),
274                        self.fold_expr(&args[1])
275                    );
276                }
277                _ => {}
278            }
279        }
280        format!("{}({})", self.fold_expr(func), fold_arg_list(self, args))
281    }
282
283    fn fold_index(&mut self, base: &SpExpr, index: &SpExpr) -> String {
284        let idx = self.fold_expr(index);
285        if !self.static_context && is_numeric_expr(index) {
286            // Array/slice indexing requires usize. Assura Int maps to i64/i128
287            // in codegen, and numeric expressions may be widened. Cast the index
288            // to usize to satisfy Rust's indexing requirements.
289            format!("{}[({idx}) as usize]", self.fold_expr(base))
290        } else {
291            format!("{}[{idx}]", self.fold_expr(base))
292        }
293    }
294
295    fn fold_binop(&mut self, lhs: &SpExpr, op: &BinOp, rhs: &SpExpr) -> String {
296        if !self.static_context {
297            // Runtime: handle special operators and i128 casts
298            match op {
299                BinOp::Implies => {
300                    return format!("(!{} || {})", self.fold_expr(lhs), self.fold_expr(rhs));
301                }
302                BinOp::In => {
303                    return format!("{}.contains(&{})", self.fold_expr(rhs), self.fold_expr(lhs));
304                }
305                BinOp::NotIn => {
306                    return format!(
307                        "!{}.contains(&{})",
308                        self.fold_expr(rhs),
309                        self.fold_expr(lhs)
310                    );
311                }
312                BinOp::Concat => {
313                    return format!(
314                        "[{}, {}].concat()",
315                        self.fold_expr(lhs),
316                        self.fold_expr(rhs)
317                    );
318                }
319                _ => {}
320            }
321            let op_s = op.as_rust_str();
322            // Widen all numeric comparisons and arithmetic to i128 to prevent
323            // mixed-type errors (e.g. i64 + u64, u64 == i128) in generated
324            // code when contracts mix Int and Nat typed inputs.
325            // Skip i128 wrapping when either side has a u128-scale literal
326            // (e.g. u128::MAX) since i128::from(u128) does not exist.
327            // Also skip when either side involves Float (f64 does not
328            // implement Into<i128>).
329            if (op.is_comparison() || op.is_arithmetic())
330                && is_numeric_expr(lhs)
331                && is_numeric_expr(rhs)
332                && !has_float_expr(lhs, &self.float_vars)
333                && !has_float_expr(rhs, &self.float_vars)
334            {
335                if has_u128_literal(lhs) || has_u128_literal(rhs) {
336                    return format!(
337                        "(({} as u128) {op_s} ({} as u128))",
338                        self.fold_expr(lhs),
339                        self.fold_expr(rhs)
340                    );
341                }
342                return format!(
343                    "(i128::from({}) {op_s} i128::from({}))",
344                    self.fold_expr(lhs),
345                    self.fold_expr(rhs)
346                );
347            }
348        }
349        format!(
350            "({} {} {})",
351            self.fold_expr(lhs),
352            op.as_rust_str(),
353            self.fold_expr(rhs)
354        )
355    }
356
357    fn fold_unary_op(&mut self, op: &UnaryOp, inner: &SpExpr) -> String {
358        let inner_s = self.fold_expr(inner);
359        if self.static_context {
360            format!("{}{inner_s}", op.as_rust_str())
361        } else {
362            format!("({}{})", op.as_rust_str(), inner_s)
363        }
364    }
365
366    fn fold_old(&mut self, inner: &SpExpr) -> String {
367        if self.static_context {
368            // old() is verification-only; emit inner for static
369            self.fold_expr(inner)
370        } else {
371            format!("{OLD_VAR_PREFIX}{}", old_var_name(inner))
372        }
373    }
374
375    fn fold_forall(&mut self, var: &str, domain: &SpExpr, body: &SpExpr) -> String {
376        if self.static_context || is_abstract_type_domain(domain) {
377            let d = self.fold_expr(domain);
378            let b = self.fold_expr(body);
379            format!("/* forall {var} in {d}: {b} */ true")
380        } else if is_range_domain(domain) {
381            format!(
382                "({}).into_iter().all(|{var}| {})",
383                self.fold_expr(domain),
384                self.fold_expr(body)
385            )
386        } else {
387            format!(
388                "{}.iter().copied().all(|{var}| {})",
389                self.fold_expr(domain),
390                self.fold_expr(body)
391            )
392        }
393    }
394
395    fn fold_exists(&mut self, var: &str, domain: &SpExpr, body: &SpExpr) -> String {
396        if self.static_context || is_abstract_type_domain(domain) {
397            let d = self.fold_expr(domain);
398            let b = self.fold_expr(body);
399            format!("/* exists {var} in {d}: {b} */ true")
400        } else if is_range_domain(domain) {
401            format!(
402                "({}).into_iter().any(|{var}| {})",
403                self.fold_expr(domain),
404                self.fold_expr(body)
405            )
406        } else {
407            format!(
408                "{}.iter().copied().any(|{var}| {})",
409                self.fold_expr(domain),
410                self.fold_expr(body)
411            )
412        }
413    }
414
415    fn fold_if(&mut self, cond: &SpExpr, then_br: &SpExpr, else_br: Option<&SpExpr>) -> String {
416        match else_br {
417            Some(eb) => {
418                let then_s = self.fold_expr(then_br);
419                let else_s = self.fold_expr(eb);
420                // In runtime context, if either branch contains i128 arithmetic
421                // but the other is a plain ident/literal, the branches produce
422                // incompatible types. Normalize both to i128::from() when both
423                // are numeric to ensure type consistency.
424                if !self.static_context
425                    && is_numeric_expr(then_br)
426                    && is_numeric_expr(eb)
427                    && (has_inner_i128(&then_s) || has_inner_i128(&else_s))
428                    && !has_float_expr(then_br, &self.float_vars)
429                    && !has_float_expr(eb, &self.float_vars)
430                {
431                    let t = if has_inner_i128(&then_s) {
432                        then_s
433                    } else {
434                        format!("i128::from({then_s})")
435                    };
436                    let e = if has_inner_i128(&else_s) {
437                        else_s
438                    } else {
439                        format!("i128::from({else_s})")
440                    };
441                    return format!("if {} {{ {} }} else {{ {} }}", self.fold_expr(cond), t, e);
442                }
443                format!(
444                    "if {} {{ {} }} else {{ {} }}",
445                    self.fold_expr(cond),
446                    then_s,
447                    else_s
448                )
449            }
450            None => format!(
451                "if {} {{ {} }}",
452                self.fold_expr(cond),
453                self.fold_expr(then_br)
454            ),
455        }
456    }
457
458    fn fold_list(&mut self, items: &[SpExpr]) -> String {
459        format!("vec![{}]", fold_joined(self, items, ", "))
460    }
461
462    fn fold_cast(&mut self, inner: &SpExpr, ty: &str) -> String {
463        if self.static_context {
464            format!("({} as {ty})", self.fold_expr(inner))
465        } else {
466            format!("({} as {})", self.fold_expr(inner), map_type_token(ty))
467        }
468    }
469
470    fn fold_block(&mut self, exprs: &[SpExpr]) -> String {
471        fold_joined(self, exprs, " ")
472    }
473
474    fn fold_ghost(&mut self, inner: &SpExpr) -> String {
475        if self.static_context {
476            let s = self.fold_expr(inner);
477            format!("/* ghost: {s} */ ()")
478        } else {
479            "/* ghost erased */()".to_string()
480        }
481    }
482
483    fn fold_apply(&mut self, lemma_name: &str, args: &[SpExpr]) -> String {
484        if self.static_context {
485            format!("/* apply {lemma_name}({}) */ ()", fold_arg_list(self, args))
486        } else {
487            format!("/* lemma {lemma_name} applied */")
488        }
489    }
490
491    fn fold_let(&mut self, name: &str, value: &SpExpr, body: &SpExpr) -> String {
492        format!(
493            "{{ let {} = {}; {} }}",
494            name,
495            self.fold_expr(value),
496            self.fold_expr(body)
497        )
498    }
499
500    fn fold_match(&mut self, scrutinee: &SpExpr, arms: &[assura_ast::MatchArm]) -> String {
501        let scrut = self.fold_expr(scrutinee);
502        if self.static_context {
503            let arm_strs: Vec<String> = arms
504                .iter()
505                .map(|arm| {
506                    let pat = match &arm.pattern {
507                        assura_ast::Pattern::Ident(s) => s.clone(),
508                        assura_ast::Pattern::Wildcard => "_".to_string(),
509                        assura_ast::Pattern::Literal(lit) => match lit {
510                            Literal::Int(s) | Literal::Float(s) => s.clone(),
511                            Literal::Str(s) => format!("\"{s}\""),
512                            Literal::Bool(b) => b.to_string(),
513                        },
514                        assura_ast::Pattern::Constructor { name, fields } => {
515                            if fields.is_empty() {
516                                name.clone()
517                            } else {
518                                format!("{name}(..)")
519                            }
520                        }
521                        assura_ast::Pattern::Tuple(pats) => {
522                            let ps: Vec<&str> = pats.iter().map(|_| "_").collect();
523                            format!("({})", ps.join(", "))
524                        }
525                    };
526                    let body = self.fold_expr(&arm.body);
527                    format!("{pat} => {body}")
528                })
529                .collect();
530            format!("match {scrut} {{ {} }}", arm_strs.join(", "))
531        } else {
532            let arms_code: Vec<String> = arms
533                .iter()
534                .map(|arm| {
535                    let pat = match &arm.pattern {
536                        assura_ast::Pattern::Ident(name) => name.clone(),
537                        assura_ast::Pattern::Wildcard => "_".into(),
538                        assura_ast::Pattern::Literal(lit) => match lit {
539                            Literal::Int(s) | Literal::Float(s) => s.clone(),
540                            Literal::Str(s) => format!("\"{s}\""),
541                            Literal::Bool(b) => b.to_string(),
542                        },
543                        assura_ast::Pattern::Constructor { name, fields } => {
544                            if fields.is_empty() {
545                                name.clone()
546                            } else {
547                                let fs: Vec<String> = fields.iter().map(pattern_to_rust).collect();
548                                format!("{name}({})", fs.join(", "))
549                            }
550                        }
551                        assura_ast::Pattern::Tuple(pats) => {
552                            let ps: Vec<String> = pats.iter().map(pattern_to_rust).collect();
553                            format!("({})", ps.join(", "))
554                        }
555                    };
556                    let body = self.fold_expr(&arm.body);
557                    format!("    {pat} => {body},")
558                })
559                .collect();
560            let has_wildcard = arms.iter().any(|arm| {
561                matches!(
562                    &arm.pattern,
563                    assura_ast::Pattern::Wildcard | assura_ast::Pattern::Ident(_)
564                )
565            });
566            if !has_wildcard {
567                let mut all_arms = arms_code;
568                all_arms.push("    _ => unreachable!(\"non-exhaustive match\"),".to_string());
569                format!("match {} {{\n{}\n}}", scrut, all_arms.join("\n"))
570            } else {
571                format!("match {} {{\n{}\n}}", scrut, arms_code.join("\n"))
572            }
573        }
574    }
575
576    fn fold_tuple(&mut self, items: &[SpExpr]) -> String {
577        format!("({})", fold_joined(self, items, ", "))
578    }
579
580    fn fold_raw(&mut self, tokens: &[String]) -> String {
581        if self.static_context {
582            let clean: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
583            if clean.len() == 1 {
584                return clean[0].to_string();
585            }
586            if clean.len() >= 2 && clean[0] == "=" {
587                return clean[1..].join(" ");
588            }
589            clean.join(" ")
590        } else {
591            raw_tokens_to_rust(tokens)
592        }
593    }
594}
595
596/// Convert raw token sequences to Rust, handling quantifier patterns.
597///
598/// Detects `forall var in domain: body` and `exists var in domain: body`
599/// in raw tokens and translates them to `.iter().all(|var| body)` /
600/// `.iter().any(|var| body)` respectively. Falls back to joined tokens
601/// for non-quantifier sequences.
602pub(crate) fn raw_tokens_to_rust(tokens: &[String]) -> String {
603    if tokens.is_empty() {
604        return String::new();
605    }
606    // Detect: forall/exists VAR in DOMAIN : BODY
607    let first = tokens[0].as_str();
608    if matches!(first, "forall" | "exists")
609        && tokens.len() >= 5
610        && let Some(in_pos) = tokens[1..].iter().position(|t| t == "in")
611    {
612        let in_pos = in_pos + 1; // offset from tokens[0]
613        let var = &tokens[1..in_pos].join("_");
614        // Find the colon that separates domain from body
615        if let Some(colon_offset) = tokens[in_pos + 1..].iter().position(|t| t == ":") {
616            let colon_pos = in_pos + 1 + colon_offset;
617            let domain_tokens = &tokens[in_pos + 1..colon_pos];
618            let body_tokens = &tokens[colon_pos + 1..];
619
620            let domain = {
621                let mapped: Vec<&str> = domain_tokens.iter().map(|t| map_type_token(t)).collect();
622                smart_join_type_tokens(&mapped)
623            };
624            let body = raw_tokens_to_rust(body_tokens);
625
626            let method = if first == "forall" { "all" } else { "any" };
627            return format!("{domain}.iter().copied().{method}(|{var}| {body})");
628        }
629    }
630
631    // Strip typestate annotations: `expr @ State` -> `true /* typestate: expr @ State */`
632    if let Some(at_pos) = tokens.iter().position(|t| t == "@") {
633        let before = &tokens[..at_pos];
634        let after = &tokens[at_pos + 1..];
635        let expr_s = raw_tokens_to_rust(before);
636        let state_s = after.join(" ");
637        return format!("true /* typestate: {expr_s} @ {state_s} */");
638    }
639
640    // Check for `result` keyword — replace with result var
641    let mapped: Vec<String> = tokens
642        .iter()
643        .map(|t| {
644            if t == "result" {
645                RESULT_VAR.to_string()
646            } else {
647                map_type_token(t).to_string()
648            }
649        })
650        .collect();
651    let refs: Vec<&str> = mapped.iter().map(|s| s.as_str()).collect();
652    smart_join_type_tokens(&refs)
653}
654
655// ---------------------------------------------------------------------------
656// old(expr) support
657// ---------------------------------------------------------------------------
658
659/// Check if an expression string contains patterns that would fail to compile
660/// against placeholder stub types:
661/// - Any field access (a.b) since stub types have no fields
662/// - Method calls on unknown objects
663/// - References to `{RESULT_VAR}.field`
664pub(crate) fn has_deep_field_access(expr: &str) -> bool {
665    // Detect struct field access like `state.head.extra` that would fail on stub types.
666    // Exclude method-call chains like `.iter().all()`, `.len()`, `.clone()` which are
667    // standard library methods and work fine.
668    let method_names = [
669        "iter",
670        "all",
671        "any",
672        "map",
673        "filter",
674        "len",
675        "is_empty",
676        "clone",
677        "count",
678        "sum",
679        "collect",
680        "flat_map",
681        "zip",
682        "enumerate",
683        "take",
684        "skip",
685        "find",
686        "fold",
687        "for_each",
688        "min",
689        "max",
690        "contains",
691        "position",
692        "into_iter",
693        "as_ref",
694        "as_mut",
695        "unwrap",
696        "unwrap_or",
697        "expect",
698        "ok",
699        "err",
700        "is_some",
701        "is_none",
702        "is_ok",
703        "is_err",
704    ];
705    for word in expr.split(|c: char| !c.is_alphanumeric() && c != '.' && c != '_') {
706        if word.contains('.') && !word.is_empty() {
707            let parts: Vec<&str> = word.split('.').collect();
708            if parts.len() >= 2
709                && parts[0]
710                    .chars()
711                    .next()
712                    .is_some_and(|c| c.is_alphabetic() || c == '_')
713            {
714                // Check if ALL dotted segments after the first are known method names
715                let all_methods = parts[1..].iter().all(|p| method_names.contains(p));
716                if !all_methods {
717                    return true;
718                }
719            }
720        }
721    }
722    // Result var field references (but not .iter(), etc.)
723    let result_dot = format!("{RESULT_VAR}.");
724    if expr.contains(&result_dot) {
725        // Check if all occurrences are followed by method calls
726        for chunk in expr.split(&result_dot) {
727            if chunk.is_empty() {
728                continue;
729            }
730            let after: String = chunk
731                .chars()
732                .take_while(|c| c.is_alphanumeric() || *c == '_')
733                .collect();
734            if !after.is_empty() && !method_names.contains(&after.as_str()) {
735                return true;
736            }
737        }
738    }
739    false
740}
741
742/// Convert a pattern to Rust pattern syntax.
743pub(crate) fn pattern_to_rust(pat: &assura_ast::Pattern) -> String {
744    match pat {
745        assura_ast::Pattern::Ident(name) => name.clone(),
746        assura_ast::Pattern::Wildcard => "_".into(),
747        assura_ast::Pattern::Literal(lit) => match lit {
748            Literal::Int(s) | Literal::Float(s) => s.clone(),
749            Literal::Str(s) => format!("\"{s}\""),
750            Literal::Bool(b) => b.to_string(),
751        },
752        assura_ast::Pattern::Constructor { name, fields } => {
753            if fields.is_empty() {
754                name.clone()
755            } else {
756                let fs: Vec<String> = fields.iter().map(pattern_to_rust).collect();
757                format!("{name}({})", fs.join(", "))
758            }
759        }
760        assura_ast::Pattern::Tuple(pats) => {
761            let ps: Vec<String> = pats.iter().map(pattern_to_rust).collect();
762            format!("({})", ps.join(", "))
763        }
764    }
765}
766
767pub(crate) fn old_var_name(expr: &SpExpr) -> String {
768    match &expr.node {
769        Expr::Ident(s) => s.clone(),
770        Expr::Field(recv, field) => format!("{}_{field}", old_var_name(recv)),
771        Expr::Call { func, .. } => old_var_name(func),
772        Expr::MethodCall {
773            receiver, method, ..
774        } => format!("{}_{method}", old_var_name(receiver)),
775        Expr::Index { expr: e, .. } => format!("{}_idx", old_var_name(e)),
776        Expr::Literal(lit) => match lit {
777            Literal::Int(s) | Literal::Float(s) => format!("lit_{s}"),
778            Literal::Str(s) => format!("lit_{}", s.trim_matches('"')),
779            Literal::Bool(b) => format!("lit_{b}"),
780        },
781        Expr::BinOp { lhs, op, rhs } => {
782            format!(
783                "{}_{}_{}",
784                old_var_name(lhs),
785                op.as_ident(),
786                old_var_name(rhs)
787            )
788        }
789        Expr::UnaryOp { op, expr: e } => {
790            let prefix = match op {
791                UnaryOp::Neg => "neg",
792                UnaryOp::Not => "not",
793            };
794            format!("{prefix}_{}", old_var_name(e))
795        }
796        Expr::Old(inner) => old_var_name(inner),
797        Expr::Cast { expr: e, .. } => old_var_name(e),
798        Expr::Ghost(inner) => format!("ghost_{}", old_var_name(inner)),
799        Expr::Forall { var, .. } => format!("forall_{var}"),
800        Expr::Exists { var, .. } => format!("exists_{var}"),
801        Expr::If { cond, .. } => format!("if_{}", old_var_name(cond)),
802        Expr::Let { name, .. } => format!("let_{name}"),
803        Expr::Match { scrutinee, .. } => format!("match_{}", old_var_name(scrutinee)),
804        Expr::Apply { lemma_name, .. } => format!("apply_{lemma_name}"),
805        Expr::List(_) => "list".to_string(),
806        Expr::Tuple(_) => "tuple".to_string(),
807        Expr::Block(exprs) => {
808            if let Some(first) = exprs.first() {
809                old_var_name(first)
810            } else {
811                "block".to_string()
812            }
813        }
814        Expr::Raw(tokens) => {
815            if let Some(first) = tokens.first() {
816                first.clone()
817            } else {
818                "raw".to_string()
819            }
820        }
821    }
822}
823
824/// Walk an expression tree and collect all `old(inner)` sub-expressions.
825/// Returns `(var_name, rust_expr)` pairs for generating pre-state snapshots.
826pub(crate) fn collect_old_exprs(expr: &SpExpr) -> Vec<(String, String)> {
827    let mut result = Vec::new();
828    collect_old_exprs_inner(expr, &mut result);
829    result
830}
831
832pub(crate) fn collect_old_exprs_inner(expr: &SpExpr, out: &mut Vec<(String, String)>) {
833    match &expr.node {
834        Expr::Old(inner) => {
835            let var = old_var_name(inner);
836            let rust = expr_to_rust(inner);
837            // Avoid duplicates
838            if !out.iter().any(|(v, _)| v == &var) {
839                out.push((var, rust));
840            }
841            // Also recurse into the inner expression (in case of nested old)
842            collect_old_exprs_inner(inner, out);
843        }
844        Expr::BinOp { lhs, rhs, .. } => {
845            collect_old_exprs_inner(lhs, out);
846            collect_old_exprs_inner(rhs, out);
847        }
848        Expr::UnaryOp { expr: e, .. } | Expr::Field(e, _) | Expr::Cast { expr: e, .. } => {
849            collect_old_exprs_inner(e, out);
850        }
851        Expr::Call { func, args } => {
852            collect_old_exprs_inner(func, out);
853            for a in args {
854                collect_old_exprs_inner(a, out);
855            }
856        }
857        Expr::MethodCall { receiver, args, .. } => {
858            collect_old_exprs_inner(receiver, out);
859            for a in args {
860                collect_old_exprs_inner(a, out);
861            }
862        }
863        Expr::Index { expr: e, index } => {
864            collect_old_exprs_inner(e, out);
865            collect_old_exprs_inner(index, out);
866        }
867        Expr::Forall { domain, body, .. } | Expr::Exists { domain, body, .. } => {
868            collect_old_exprs_inner(domain, out);
869            collect_old_exprs_inner(body, out);
870        }
871        Expr::If {
872            cond,
873            then_branch,
874            else_branch,
875        } => {
876            collect_old_exprs_inner(cond, out);
877            collect_old_exprs_inner(then_branch, out);
878            if let Some(eb) = else_branch {
879                collect_old_exprs_inner(eb, out);
880            }
881        }
882        Expr::List(items) | Expr::Block(items) => {
883            for item in items {
884                collect_old_exprs_inner(item, out);
885            }
886        }
887        Expr::Ghost(inner) => {
888            // Ghost blocks are erased but may reference old() in
889            // their verification expressions.
890            collect_old_exprs_inner(inner, out);
891        }
892        Expr::Apply { args, .. } => {
893            // Apply is erased but may reference old() in arguments.
894            for a in args {
895                collect_old_exprs_inner(a, out);
896            }
897        }
898        Expr::Match { scrutinee, arms } => {
899            collect_old_exprs_inner(scrutinee, out);
900            for arm in arms {
901                collect_old_exprs_inner(&arm.body, out);
902            }
903        }
904        Expr::Let { value, body, .. } => {
905            collect_old_exprs_inner(value, out);
906            collect_old_exprs_inner(body, out);
907        }
908        Expr::Tuple(elems) => {
909            for e in elems {
910                collect_old_exprs_inner(e, out);
911            }
912        }
913        // Leaf nodes: no old() inside
914        Expr::Literal(_) | Expr::Ident(_) | Expr::Raw(_) => {}
915    }
916}
917#[cfg(test)]
918#[path = "expr_tests.rs"]
919mod tests;