Skip to main content

assura_codegen/
contract.rs

1//! Contract, enum, proptest, and error type code generation.
2
3use std::collections::HashSet;
4use std::fmt::Write;
5
6use super::*;
7
8// ---------------------------------------------------------------------------
9// Enum definitions
10// ---------------------------------------------------------------------------
11
12pub(crate) fn generate_enum_def(e: &EnumDef, code: &mut String) {
13    let items = crate::hir::build_enum_def(e);
14    for item in &items {
15        code.push_str(&crate::hir::render_item_raw(item));
16    }
17}
18
19// ---------------------------------------------------------------------------
20// Contract declarations
21// ---------------------------------------------------------------------------
22
23/// Collect free identifier names from an expression tree.
24///
25/// Walks the expression recursively and collects all `Expr::Ident` names,
26/// excluding `result` (handled separately by codegen) and quantifier-bound
27/// variables. Used to synthesize input parameters for contracts that reference
28/// free variables without an explicit `input()` clause.
29fn collect_free_idents(expr: &SpExpr, idents: &mut HashSet<String>) {
30    match &expr.node {
31        Expr::Ident(name) if name != "result" && name != "true" && name != "false" => {
32            idents.insert(name.clone());
33        }
34        Expr::Ident(_) => {}
35        Expr::BinOp { lhs, rhs, .. } => {
36            collect_free_idents(lhs, idents);
37            collect_free_idents(rhs, idents);
38        }
39        Expr::UnaryOp { expr: inner, .. }
40        | Expr::Old(inner)
41        | Expr::Ghost(inner)
42        | Expr::Cast { expr: inner, .. } => collect_free_idents(inner, idents),
43        Expr::If {
44            cond,
45            then_branch,
46            else_branch,
47        } => {
48            collect_free_idents(cond, idents);
49            collect_free_idents(then_branch, idents);
50            if let Some(e) = else_branch {
51                collect_free_idents(e, idents);
52            }
53        }
54        Expr::Forall {
55            var, body, domain, ..
56        }
57        | Expr::Exists {
58            var, body, domain, ..
59        } => {
60            collect_free_idents(body, idents);
61            collect_free_idents(domain, idents);
62            idents.remove(var);
63        }
64        Expr::Call { args, .. } => {
65            for arg in args {
66                collect_free_idents(arg, idents);
67            }
68        }
69        Expr::Field(receiver, _) => collect_free_idents(receiver, idents),
70        Expr::MethodCall { receiver, args, .. } => {
71            collect_free_idents(receiver, idents);
72            for arg in args {
73                collect_free_idents(arg, idents);
74            }
75        }
76        Expr::Index { expr, index } => {
77            collect_free_idents(expr, idents);
78            collect_free_idents(index, idents);
79        }
80        Expr::Let { value, body, .. } => {
81            collect_free_idents(value, idents);
82            collect_free_idents(body, idents);
83        }
84        Expr::Match { scrutinee, arms } => {
85            collect_free_idents(scrutinee, idents);
86            for arm in arms {
87                collect_free_idents(&arm.body, idents);
88            }
89        }
90        Expr::List(items) | Expr::Tuple(items) | Expr::Block(items) => {
91            for item in items {
92                collect_free_idents(item, idents);
93            }
94        }
95        Expr::Apply { args, .. } => {
96            for arg in args {
97                collect_free_idents(arg, idents);
98            }
99        }
100        Expr::Literal(_) => {}
101        Expr::Raw(tokens) => {
102            for tok in tokens {
103                if tok
104                    .chars()
105                    .next()
106                    .is_some_and(|c| c.is_alphabetic() || c == '_')
107                    && tok != "result"
108                    && tok != "true"
109                    && tok != "false"
110                {
111                    idents.insert(tok.clone());
112                }
113            }
114        }
115    }
116}
117
118/// Generate the body of a contract as standalone module contents (no `pub mod`
119/// wrapper). Used in multi-file mode where each contract gets its own `.rs` file.
120///
121/// When `ir_bodies` is provided and contains a body for this contract's name,
122/// the IR-generated Rust code replaces the `todo!()` placeholder.
123pub(crate) fn generate_contract_contents(
124    c: &ContractDecl,
125    code: &mut String,
126    ir_bodies: Option<&std::collections::HashMap<String, String>>,
127) {
128    generate_contract_contents_opts(c, code, ir_bodies, false);
129}
130
131pub(crate) fn generate_contract_contents_opts(
132    c: &ContractDecl,
133    code: &mut String,
134    ir_bodies: Option<&std::collections::HashMap<String, String>>,
135    runtime_checks: bool,
136) {
137    use crate::hir::*;
138
139    // Interface contracts become traits even in multi-file mode
140    let is_interface = c
141        .clauses
142        .iter()
143        .any(|cl| matches!(&cl.kind, ClauseKind::Other(k) if k == "interface"));
144    if is_interface {
145        generate_interface_trait_from_contract(c, code);
146        return;
147    }
148
149    let implements: Vec<String> = c
150        .clauses
151        .iter()
152        .filter(|cl| matches!(&cl.kind, ClauseKind::Other(k) if k == "implements"))
153        .filter_map(|cl| match &cl.body.node {
154            Expr::Ident(name) => Some(name.clone()),
155            Expr::Raw(tokens) if tokens.len() == 1 => Some(tokens[0].clone()),
156            _ => None,
157        })
158        .collect();
159
160    let mut input_params: Vec<(String, String)> = Vec::new();
161    let mut output_type = "()".to_string();
162    let mut output_name: Option<String> = None;
163    let mut requires_exprs: Vec<String> = Vec::new();
164    let mut ensures_exprs: Vec<String> = Vec::new();
165    let mut effects: Vec<String> = Vec::new();
166    let mut modifies: Vec<String> = Vec::new();
167    let mut invariants: Vec<String> = Vec::new();
168
169    // First pass: collect input params and output type so we know which
170    // variables are float-typed before converting clause bodies.
171    for clause in &c.clauses {
172        match &clause.kind {
173            ClauseKind::Input => extract_input_params(&clause.body, &mut input_params),
174            ClauseKind::Output => {
175                output_type = extract_output_type(&clause.body);
176                output_name = extract_output_name(&clause.body);
177            }
178            _ => {}
179        }
180    }
181
182    // Collect float-typed parameter names so the expression folder skips
183    // i128::from() wrapping for them (f64 does not implement Into<i128>).
184    let float_vars: HashSet<String> = input_params
185        .iter()
186        .filter(|(_, ty)| ty == "f64")
187        .map(|(name, _)| name.clone())
188        .collect();
189
190    // Second pass: convert clause bodies to Rust expressions.
191    for clause in &c.clauses {
192        match &clause.kind {
193            ClauseKind::Requires => {
194                requires_exprs.push(expr_to_rust_with_floats(&clause.body, float_vars.clone()))
195            }
196            ClauseKind::Ensures => {
197                ensures_exprs.push(expr_to_rust_with_floats(&clause.body, float_vars.clone()))
198            }
199            ClauseKind::Effects => effects.push(expr_to_rust(&clause.body)),
200            ClauseKind::Modifies => modifies.push(expr_to_rust(&clause.body)),
201            ClauseKind::Invariant => {
202                invariants.push(expr_to_rust_with_floats(&clause.body, float_vars.clone()))
203            }
204            ClauseKind::Input
205            | ClauseKind::Output
206            | ClauseKind::Errors
207            | ClauseKind::Rule
208            | ClauseKind::DataFlow
209            | ClauseKind::MustNot
210            | ClauseKind::Decreases
211            | ClauseKind::Ordering
212            | ClauseKind::Other(_) => {}
213        }
214    }
215
216    // Infer output type when no output() clause exists but ensures/invariant
217    // clauses reference `result`. Without this, codegen produces
218    // `let __assura_result: () = todo!(...)` and then `i128::from(())` or
219    // `() == true` which fails to compile.
220    if output_type == "()" {
221        let refs_result = c.clauses.iter().any(|cl| {
222            matches!(cl.kind, ClauseKind::Ensures | ClauseKind::Invariant)
223                && assura_ast::expr_references_result(&cl.body)
224        });
225        if refs_result {
226            output_type = infer_result_type_from_clauses(&c.clauses);
227        }
228    }
229
230    // Synthesize input parameters from free variables when no input() clause
231    // exists. Contracts like `requires { x > 0 } ensures { x > 0 }` reference
232    // `x` as a free variable; without this, codegen produces `fn check()` with
233    // no parameters and the generated Rust fails to compile.
234    if input_params.is_empty() {
235        let mut free = HashSet::new();
236        for clause in &c.clauses {
237            match &clause.kind {
238                ClauseKind::Requires | ClauseKind::Ensures | ClauseKind::Invariant => {
239                    collect_free_idents(&clause.body, &mut free);
240                }
241                _ => {}
242            }
243        }
244        let mut sorted: Vec<String> = free.into_iter().collect();
245        sorted.sort();
246        for name in sorted {
247            input_params.push((name, "i64".to_string()));
248        }
249    }
250
251    // Collect feature-specific annotation code (CORE/SEC/MEM/CONC/FMT/etc.)
252    let mut feature_code = String::new();
253    crate::features::generate_all_feature_clauses(&c.clauses, &c.name, &mut feature_code);
254
255    // Generate error enum if errors clause is present
256    let error_variants = collect_error_variants(&c.clauses);
257    let error_enum_name = if !error_variants.is_empty() {
258        let name = format!("{}Error", c.name);
259        generate_error_enum(&c.name, &error_variants, code);
260        Some(name)
261    } else {
262        None
263    };
264
265    // Determine return type: wrap in Result when errors are declared
266    let return_type = if let Some(ref err_name) = error_enum_name {
267        format!("Result<{output_type}, {err_name}>")
268    } else {
269        output_type.clone()
270    };
271
272    // Build doc comments
273    let mut doc: Vec<String> = Vec::new();
274    for req in &requires_exprs {
275        doc.push(format!("Requires: {req}"));
276    }
277    for eff in &effects {
278        doc.push(format!("Effects: {eff}"));
279    }
280    for m in &modifies {
281        doc.push(format!("Modifies: {m}"));
282    }
283
284    // Build params
285    let params: Vec<RustParam> = input_params
286        .iter()
287        .map(|(name, ty)| RustParam {
288            name: name.clone(),
289            ty: RustType::Raw(ty.clone()),
290        })
291        .collect();
292
293    let ret = if return_type == "()" {
294        None
295    } else {
296        Some(RustType::Raw(return_type.clone()))
297    };
298
299    // Build function body
300    let mut body: Vec<RustStmt> = Vec::new();
301
302    // old() variable snapshots for ensures clauses
303    for clause in &c.clauses {
304        if clause.kind == ClauseKind::Ensures {
305            for (var, rust_expr) in collect_old_exprs(&clause.body) {
306                body.push(RustStmt::Raw(format!(
307                    "let {OLD_VAR_PREFIX}{var} = {rust_expr}.clone();"
308                )));
309            }
310        }
311    }
312
313    // Requires assertions
314    for req in &requires_exprs {
315        body.push(RustStmt::Assert {
316            cond: req.clone(),
317            label: "requires".into(),
318        });
319    }
320
321    // Feature-specific annotations
322    if !feature_code.is_empty() {
323        body.push(RustStmt::Raw(feature_code));
324    }
325
326    // Check for IR-generated body to replace todo!() placeholder
327    let ir_body = ir_bodies.and_then(|m| m.get(&c.name));
328
329    if ensures_exprs.is_empty() && invariants.is_empty() {
330        if let Some(ir) = ir_body {
331            body.push(RustStmt::Raw(ir.clone()));
332        } else {
333            body.push(RustStmt::Expr(RustExpr::Todo(
334                "implementation provided by AI agent".into(),
335            )));
336        }
337    } else {
338        if let Some(ir) = ir_body {
339            body.push(RustStmt::Raw(ir.clone()));
340        } else {
341            body.push(RustStmt::Raw(
342                crate::metadata::implementation_guidance_comment(c),
343            ));
344            body.push(RustStmt::Raw(format!(
345                "let {RESULT_VAR}: {output_type} = todo!(\"implementation provided by AI agent\");"
346            )));
347        }
348        if let Some(ref name) = output_name {
349            body.push(RustStmt::Raw(format!("let {name} = {RESULT_VAR}.clone();")));
350        }
351        for ens in &ensures_exprs {
352            body.push(RustStmt::Assert {
353                cond: ens.clone(),
354                label: "ensures".into(),
355            });
356        }
357        for inv in &invariants {
358            body.push(RustStmt::Assert {
359                cond: inv.clone(),
360                label: "invariant".into(),
361            });
362        }
363        if error_enum_name.is_some() {
364            body.push(RustStmt::Expr(RustExpr::Ok(Box::new(RustExpr::Ident(
365                RESULT_VAR.into(),
366            )))));
367        } else {
368            body.push(RustStmt::Expr(RustExpr::Ident(RESULT_VAR.into())));
369        }
370    }
371
372    let check_fn = RustFn {
373        name: "check".into(),
374        type_params: c.type_params.clone(),
375        params,
376        ret,
377        body,
378        doc,
379        ..RustFn::default()
380    };
381    if runtime_checks {
382        let opts = crate::hir::RenderOpts {
383            runtime_checks: true,
384            contract_name: c.name.clone(),
385        };
386        code.push_str(&crate::hir::render_item_raw_with_opts(
387            &RustItem::Fn(check_fn),
388            &opts,
389        ));
390    } else {
391        code.push_str(&render_item_raw(&RustItem::Fn(check_fn)));
392    }
393
394    // Generate implements blocks
395    if !implements.is_empty() {
396        code.push_str(&render_item_raw(&RustItem::Struct(RustStruct {
397            name: c.name.clone(),
398            type_params: c.type_params.clone(),
399            derives: vec![],
400            ..RustStruct::default()
401        })));
402
403        for iface in &implements {
404            let mut impl_methods: Vec<RustFn> = Vec::new();
405            for clause in &c.clauses {
406                if let ClauseKind::Other(k) = &clause.kind
407                    && k == "method"
408                {
409                    let method_name = match &clause.body.node {
410                        Expr::Ident(n) => Some(n.as_str()),
411                        Expr::Raw(tokens) if tokens.len() == 1 => Some(tokens[0].as_str()),
412                        _ => None,
413                    };
414                    if let Some(method_name) = method_name {
415                        impl_methods.push(RustFn {
416                            name: method_name.to_string(),
417                            params: vec![RustParam {
418                                name: "&self".into(),
419                                ty: RustType::Raw("&Self".into()),
420                            }],
421                            body: vec![RustStmt::Expr(RustExpr::Todo(String::new()))],
422                            is_pub: false,
423                            ..RustFn::default()
424                        });
425                    }
426                }
427            }
428            code.push_str(&render_item_raw(&RustItem::Impl(RustImpl {
429                trait_name: Some(iface.clone()),
430                target: c.name.clone(),
431                type_params: c.type_params.clone(),
432                methods: impl_methods,
433            })));
434        }
435    }
436}
437
438pub(crate) fn generate_contract_runtime(
439    c: &ContractDecl,
440    code: &mut String,
441    ir_bodies: Option<&std::collections::HashMap<String, String>>,
442) {
443    use crate::hir::*;
444
445    let is_interface = c
446        .clauses
447        .iter()
448        .any(|cl| matches!(&cl.kind, ClauseKind::Other(k) if k == "interface"));
449    if is_interface {
450        generate_interface_trait_from_contract(c, code);
451        return;
452    }
453
454    let mut inner = String::new();
455    generate_contract_contents_opts(c, &mut inner, ir_bodies, true);
456
457    let m = RustItem::Mod(RustMod {
458        name: format!("contract_{}", c.name.to_lowercase()),
459        items: vec![RustItem::Raw(inner)],
460        is_pub: true,
461        doc: vec![format!("Contract: {}", c.name)],
462    });
463    code.push_str(&render_item_raw(&m));
464}
465
466pub(crate) fn generate_contract(
467    c: &ContractDecl,
468    code: &mut String,
469    ir_bodies: Option<&std::collections::HashMap<String, String>>,
470) {
471    use crate::hir::*;
472
473    // Interface contracts become traits (no wrapping module needed)
474    let is_interface = c
475        .clauses
476        .iter()
477        .any(|cl| matches!(&cl.kind, ClauseKind::Other(k) if k == "interface"));
478    if is_interface {
479        generate_interface_trait_from_contract(c, code);
480        return;
481    }
482
483    // Single-file mode: wrap contents in a pub mod. Import crate-level types
484    // (structs, enums) so params like `p: Point` resolve inside the module.
485    let mut inner = String::new();
486    inner.push_str("use super::*;\n\n");
487    generate_contract_contents(c, &mut inner, ir_bodies);
488
489    let m = RustItem::Mod(RustMod {
490        name: format!("contract_{}", c.name.to_lowercase()),
491        items: vec![RustItem::Raw(inner)],
492        is_pub: true,
493        doc: vec![format!("Contract: {}", c.name)],
494    });
495    code.push_str(&render_item_raw(&m));
496}
497
498// ---------------------------------------------------------------------------
499// S009: Proptest generation from contracts
500// ---------------------------------------------------------------------------
501
502/// Map a Rust type to a proptest strategy expression.
503pub(crate) fn proptest_strategy_for_type(rust_type: &str) -> String {
504    match rust_type {
505        // Prefer i32-range for i64/u64 so generated `+`/`*` proptest tests do not
506        // panic on debug overflow while still covering wide values. Full-range
507        // i64 + i64 overflows in debug Rust; Assura Int is mathematical in SMT.
508        "i64" => "proptest::prelude::any::<i32>().prop_map(|n| i64::from(n))".to_string(),
509        "u64" => "proptest::prelude::any::<u32>().prop_map(|n| u64::from(n))".to_string(),
510        "i32" => "proptest::prelude::any::<i32>()".to_string(),
511        "u32" => "proptest::prelude::any::<u32>()".to_string(),
512        "i16" => "proptest::prelude::any::<i16>()".to_string(),
513        "u16" => "proptest::prelude::any::<u16>()".to_string(),
514        "i8" => "proptest::prelude::any::<i8>()".to_string(),
515        "u8" => "proptest::prelude::any::<u8>()".to_string(),
516        "f64" => "proptest::prelude::any::<f64>()".to_string(),
517        "f32" => "proptest::prelude::any::<f32>()".to_string(),
518        "bool" => "proptest::prelude::any::<bool>()".to_string(),
519        "usize" => "proptest::prelude::any::<u16>().prop_map(|n| n as usize)".to_string(),
520        "isize" => "proptest::prelude::any::<i16>().prop_map(|n| n as isize)".to_string(),
521        _ => format!("proptest::prelude::any::<{rust_type}>()"),
522    }
523}
524
525/// A single bound extracted from a requires constraint.
526#[derive(Debug, Clone)]
527pub(crate) enum ParamBound {
528    /// x >= val (inclusive lower)
529    GteVal(i64),
530    /// x > val  (exclusive lower, stored as val+1 inclusive)
531    GtVal(i64),
532    /// x <= val (inclusive upper)
533    LteVal(i64),
534    /// x < val  (exclusive upper, stored as val-1 inclusive)
535    LtVal(i64),
536    /// x != val (non-equality; currently only x != 0 uses lower bound 1)
537    NeqZero,
538}
539
540/// Try to extract a bound on a parameter from a requires constraint.
541///
542/// Recognizes patterns like:
543///   - `x != 0` -> lower bound 1
544///   - `x > 0` / `x >= 1` -> lower bound
545///   - `x < N` / `x <= N` -> upper bound
546///
547/// Returns `Some((param_name, bound))` if the constraint is a simple
548/// comparison on a single param, or `None` if it should remain a
549/// filter/assumption.
550pub(crate) fn try_extract_bound(requires_expr: &SpExpr) -> Option<(String, ParamBound)> {
551    if let Expr::BinOp { lhs, op, rhs } = &requires_expr.node {
552        let param = match &lhs.node {
553            Expr::Ident(name) => name.clone(),
554            _ => return None,
555        };
556
557        match (op, &rhs.node) {
558            (BinOp::Neq, Expr::Literal(Literal::Int(val))) if val == "0" => {
559                Some((param, ParamBound::NeqZero))
560            }
561            (BinOp::Gt, Expr::Literal(Literal::Int(val))) => {
562                let v = val.parse::<i64>().ok()?;
563                Some((param, ParamBound::GtVal(v)))
564            }
565            (BinOp::Gte, Expr::Literal(Literal::Int(val))) => {
566                let v = val.parse::<i64>().ok()?;
567                Some((param, ParamBound::GteVal(v)))
568            }
569            (BinOp::Lt, Expr::Literal(Literal::Int(val))) => {
570                let v = val.parse::<i64>().ok()?;
571                Some((param, ParamBound::LtVal(v)))
572            }
573            (BinOp::Lte, Expr::Literal(Literal::Int(val))) => {
574                let v = val.parse::<i64>().ok()?;
575                Some((param, ParamBound::LteVal(v)))
576            }
577            _ => None,
578        }
579    } else {
580        None
581    }
582}
583
584/// Collected lower and upper bounds for a single parameter.
585#[derive(Debug, Default)]
586struct ParamRange {
587    lower: Option<i64>,
588    upper: Option<i64>,
589    neq_zero: bool,
590}
591
592impl ParamRange {
593    fn apply(&mut self, bound: &ParamBound) {
594        match bound {
595            ParamBound::GteVal(v) => {
596                self.lower = Some(self.lower.map_or(*v, |cur| cur.max(*v)));
597            }
598            ParamBound::GtVal(v) => {
599                let inclusive = v.saturating_add(1);
600                self.lower = Some(self.lower.map_or(inclusive, |cur| cur.max(inclusive)));
601            }
602            ParamBound::LteVal(v) => {
603                self.upper = Some(self.upper.map_or(*v, |cur| cur.min(*v)));
604            }
605            ParamBound::LtVal(v) => {
606                let inclusive = v.saturating_sub(1);
607                self.upper = Some(self.upper.map_or(inclusive, |cur| cur.min(inclusive)));
608            }
609            ParamBound::NeqZero => {
610                self.neq_zero = true;
611            }
612        }
613    }
614
615    fn to_strategy(&self) -> String {
616        // #709: contradictory bounds (lo > hi) fall back to any()
617        let base = match (self.lower, self.upper) {
618            (Some(lo), Some(hi)) if lo > hi => "proptest::prelude::any::<i64>()".to_string(),
619            (Some(lo), Some(hi)) => format!("({lo}i64..={hi}i64)"),
620            (Some(lo), None) => format!("({lo}i64..=i64::MAX)"),
621            (None, Some(hi)) => format!("(i64::MIN..={hi}i64)"),
622            (None, None) => "proptest::prelude::any::<i64>()".to_string(),
623        };
624        // #710: neq_zero uses a filter to preserve both positive and negative domain
625        if self.neq_zero {
626            format!("{base}.prop_filter(\"!= 0\", |&v| v != 0)")
627        } else {
628            base
629        }
630    }
631
632    fn has_bounds(&self) -> bool {
633        self.lower.is_some() || self.upper.is_some() || self.neq_zero
634    }
635}
636
637/// Check if a contract has testable content (inputs + ensures/requires).
638pub(crate) fn contract_is_testable(c: &ContractDecl) -> bool {
639    let has_input = c
640        .clauses
641        .iter()
642        .any(|cl| matches!(cl.kind, ClauseKind::Input));
643    let has_ensures = c
644        .clauses
645        .iter()
646        .any(|cl| matches!(cl.kind, ClauseKind::Ensures));
647    has_input && has_ensures
648}
649
650/// Generate proptest property-based tests for a contract.
651///
652/// For each contract with input params and ensures clauses, generates a
653/// `proptest!` block that:
654/// - Uses the contract's input types as proptest strategies
655/// - Refines strategies based on requires constraints where possible
656/// - Falls back to `prop_assume!` for complex requires constraints
657/// - Asserts ensures clauses with `prop_assert!`
658pub(crate) fn generate_proptest_for_contract(c: &ContractDecl, code: &mut String) {
659    // Single-file mode: call path is super::contract_<name>::check()
660    let fn_name = c.name.to_lowercase();
661    generate_proptest_impl(c, code, &format!("super::contract_{fn_name}::check"));
662}
663
664/// Generate proptest for a contract in multi-file mode (the test module
665/// is inside the contract's own .rs file, so the call is `super::check()`).
666pub(crate) fn generate_proptest_for_contract_contents(c: &ContractDecl, code: &mut String) {
667    generate_proptest_impl(c, code, "super::check");
668}
669
670/// Shared proptest generation. `check_call_path` is the path to the
671/// contract's check function from inside the test module.
672fn generate_proptest_impl(c: &ContractDecl, code: &mut String, check_call_path: &str) {
673    use crate::hir::*;
674
675    if !contract_is_testable(c) {
676        return;
677    }
678
679    let mut input_params: Vec<(String, String)> = Vec::new();
680    let mut requires_exprs: Vec<String> = Vec::new();
681    let mut requires_ast: Vec<&SpExpr> = Vec::new();
682    let mut ensures_exprs: Vec<String> = Vec::new();
683    let mut output_name: Option<String> = None;
684
685    for clause in &c.clauses {
686        match &clause.kind {
687            ClauseKind::Input => extract_input_params(&clause.body, &mut input_params),
688            ClauseKind::Requires => {
689                requires_exprs.push(expr_to_rust_static(&clause.body));
690                requires_ast.push(&clause.body);
691            }
692            ClauseKind::Ensures => {
693                ensures_exprs.push(expr_to_rust_static(&clause.body));
694            }
695            ClauseKind::Output => {
696                output_name = extract_output_name(&clause.body);
697            }
698            _ => {}
699        }
700    }
701
702    if input_params.is_empty() || ensures_exprs.is_empty() {
703        return;
704    }
705
706    // Collect all bounds per parameter, then merge into one range each.
707    let mut param_ranges: std::collections::HashMap<String, ParamRange> =
708        std::collections::HashMap::new();
709    let mut unrefined_requires: Vec<String> = Vec::new();
710    for (i, ast) in requires_ast.iter().enumerate() {
711        if let Some((param, bound)) = try_extract_bound(ast) {
712            param_ranges.entry(param).or_default().apply(&bound);
713        } else {
714            unrefined_requires.push(requires_exprs[i].clone());
715        }
716    }
717    let refined: std::collections::HashMap<String, String> = param_ranges
718        .iter()
719        .filter(|(_, range)| range.has_bounds())
720        .map(|(param, range)| (param.clone(), range.to_strategy()))
721        .collect();
722
723    let fn_name = c.name.to_lowercase();
724
725    // Build the proptest macro body as raw code since proptest! is a macro
726    // and not representable as a plain RustFn
727    let param_strs: Vec<String> = input_params
728        .iter()
729        .map(|(name, ty)| {
730            if let Some(strategy) = refined.get(name) {
731                format!("{name} in {strategy}")
732            } else {
733                let strategy = proptest_strategy_for_type(ty);
734                format!("{name} in {strategy}")
735            }
736        })
737        .collect();
738
739    let mut test_body = String::new();
740    for req in &unrefined_requires {
741        let _ = writeln!(test_body, "            prop_assume!({req});");
742    }
743
744    // Clone args so ensures can still use input params after check() moves them.
745    let call_args: Vec<String> = input_params
746        .iter()
747        .map(|(n, _)| format!("{n}.clone()"))
748        .collect();
749    let _ = writeln!(
750        test_body,
751        "            let result = {check_call_path}({});",
752        call_args.join(", ")
753    );
754    if let Some(ref name) = output_name {
755        let _ = writeln!(test_body, "            let {name} = result.clone();");
756    }
757    for (i, ens) in ensures_exprs.iter().enumerate() {
758        // Evaluate ensures to a bool first so braces in `if { } else { }`
759        // expressions do not break prop_assert!'s format-string expansion.
760        let _ = writeln!(test_body, "            let __ensures_{i} = {ens};");
761        let _ = writeln!(
762            test_body,
763            "            prop_assert!(__ensures_{i}, \"ensures clause {i} failed\");"
764        );
765    }
766
767    // Emit as a RustMod with #[cfg(test)] + raw proptest! macro inside
768    let inner_raw = format!(
769        "use super::*;\n\
770         use proptest::prelude::*;\n\n\
771         proptest! {{\n\
772         {indent}#[test]\n\
773         {indent}fn test_{fn_name}({params}) {{\n\
774         {test_body}\
775         {indent}}}\n\
776         }}\n",
777        indent = "    ",
778        params = param_strs.join(", "),
779    );
780
781    code.push_str(&render_item_raw(&RustItem::Raw(
782        "#[cfg(test)]\n".to_string(),
783    )));
784    code.push_str(&render_item_raw(&RustItem::Mod(RustMod {
785        name: format!("proptest_{fn_name}"),
786        items: vec![RustItem::Raw(inner_raw)],
787        is_pub: false,
788        doc: vec![],
789    })));
790}
791
792/// Check if any contract in the source is testable (needs proptest).
793/// Check if any declaration has an `errors` clause that will generate error types.
794pub(crate) fn source_has_error_types(source: &assura_ast::SourceFile) -> bool {
795    use assura_ast::{ContractDecl, DeclVisitor, FnDef};
796
797    struct HasErrors(bool);
798    impl DeclVisitor for HasErrors {
799        fn visit_contract(&mut self, c: &ContractDecl) {
800            if c.clauses.iter().any(|cl| cl.kind == ClauseKind::Errors) {
801                self.0 = true;
802            }
803        }
804        fn visit_fn_def(&mut self, f: &FnDef) {
805            if f.clauses.iter().any(|cl| cl.kind == ClauseKind::Errors) {
806                self.0 = true;
807            }
808        }
809    }
810    let mut v = HasErrors(false);
811    assura_ast::walk_decls(&mut v, &source.decls);
812    v.0
813}
814
815pub(crate) fn source_has_testable_contracts(source: &assura_ast::SourceFile) -> bool {
816    use assura_ast::{ContractDecl, DeclVisitor};
817
818    struct HasTestable(bool);
819    impl DeclVisitor for HasTestable {
820        fn visit_contract(&mut self, c: &ContractDecl) {
821            if contract_is_testable(c) {
822                self.0 = true;
823            }
824        }
825    }
826    let mut v = HasTestable(false);
827    assura_ast::walk_decls(&mut v, &source.decls);
828    v.0
829}
830
831/// Generate a Rust trait from a contract that has an `interface` clause.
832///
833/// Delegates to the shared `generate_interface_trait` which already builds
834/// a `RustItem::Trait` from clause bodies.
835pub(crate) fn generate_interface_trait_from_contract(c: &ContractDecl, code: &mut String) {
836    generate_interface_trait(&c.name, &c.clauses, code);
837}
838
839/// Extract `(name, rust_type)` pairs from an input clause body.
840///
841/// Uses the shared `extract_clause_params` from assura-parser, then maps
842/// Assura type tokens to Rust types via `map_type_token`/`map_type_tokens`.
843pub fn extract_input_params(body: &SpExpr, params: &mut Vec<(String, String)>) {
844    use assura_ast::extract_clause_params;
845    for param in extract_clause_params(body) {
846        let rust_ty = if param.ty.is_none() {
847            "i64".to_string()
848        } else {
849            // Convert TypeExpr to tokens and filter out type qualifiers
850            // (linear, secret, tainted, etc.) that have no Rust equivalent
851            let tokens = param.ty.as_ref().map(|t| t.to_tokens()).unwrap_or_default();
852            let filtered: Vec<String> = tokens
853                .into_iter()
854                .filter(|t| {
855                    !matches!(
856                        t.as_str(),
857                        "linear" | "secret" | "tainted" | "taint" | "untrusted" | "validated"
858                    )
859                })
860                .collect();
861            if filtered.is_empty() {
862                "i64".to_string()
863            } else if filtered.len() == 1 {
864                map_type_token(&filtered[0]).to_string()
865            } else {
866                map_type_tokens(&filtered)
867            }
868        };
869        params.push((param.name, rust_ty));
870    }
871}
872
873/// Collect the effective input parameters for a contract declaration.
874///
875/// First tries to extract explicit `input()` clause params. If none exist,
876/// synthesizes params from free variables in requires/ensures/invariant
877/// clauses (all typed as `i64`). This mirrors the logic in
878/// `generate_contract_contents_opts` so callers like `--bin` main.rs
879/// generation see the same signature as the generated `check()` function.
880pub fn collect_contract_params(c: &ContractDecl) -> Vec<(String, String)> {
881    let mut params = Vec::new();
882    for clause in &c.clauses {
883        if clause.kind == ClauseKind::Input {
884            extract_input_params(&clause.body, &mut params);
885        }
886    }
887    if params.is_empty() {
888        let mut free = HashSet::new();
889        for clause in &c.clauses {
890            match &clause.kind {
891                ClauseKind::Requires | ClauseKind::Ensures | ClauseKind::Invariant => {
892                    collect_free_idents(&clause.body, &mut free);
893                }
894                _ => {}
895            }
896        }
897        let mut sorted: Vec<String> = free.into_iter().collect();
898        sorted.sort();
899        for name in sorted {
900            params.push((name, "i64".to_string()));
901        }
902    }
903    params
904}
905
906/// Infer the result type from ensures/invariant clauses when no output() exists.
907///
908/// Walks ensures/invariant clause bodies looking for how `result` is used:
909/// - `result == true` or `result == false` -> `bool`
910/// - `result > x` or numeric comparison -> `i64`
911/// - `result.length()` (string/bytes method) -> `String`
912/// - fallback -> `i64` (most common in math contracts)
913fn infer_result_type_from_clauses(clauses: &[Clause]) -> String {
914    for clause in clauses {
915        if !matches!(clause.kind, ClauseKind::Ensures | ClauseKind::Invariant) {
916            continue;
917        }
918        if let Some(ty) = infer_result_type_from_expr(&clause.body) {
919            return ty;
920        }
921    }
922    // Default: numeric contracts are the most common case
923    "i64".to_string()
924}
925
926/// Walk an expression to infer what type `result` should be based on usage.
927fn infer_result_type_from_expr(expr: &SpExpr) -> Option<String> {
928    match &expr.node {
929        Expr::BinOp { lhs, op, rhs } => {
930            // `result == true` / `result == false` -> bool
931            if matches!(op, BinOp::Eq | BinOp::Neq) {
932                if is_result_ident(lhs) && is_bool_literal(rhs) {
933                    return Some("bool".to_string());
934                }
935                if is_result_ident(rhs) && is_bool_literal(lhs) {
936                    return Some("bool".to_string());
937                }
938            }
939            // `result > x` or `result >= x` etc -> i64 (numeric)
940            if (op.is_comparison() || op.is_arithmetic())
941                && (is_result_ident(lhs) || is_result_ident(rhs))
942            {
943                return Some("i64".to_string());
944            }
945            // Recurse into both sides (e.g. `result > x && result < y`)
946            if let Some(ty) = infer_result_type_from_expr(lhs) {
947                return Some(ty);
948            }
949            infer_result_type_from_expr(rhs)
950        }
951        Expr::MethodCall {
952            receiver, method, ..
953        } => {
954            // `result.length()` -> String
955            if is_result_ident(receiver) && matches!(method.as_str(), "length" | "len" | "size") {
956                return Some("String".to_string());
957            }
958            infer_result_type_from_expr(receiver)
959        }
960        Expr::UnaryOp { expr: inner, .. }
961        | Expr::Old(inner)
962        | Expr::Ghost(inner)
963        | Expr::Cast { expr: inner, .. } => infer_result_type_from_expr(inner),
964        Expr::If {
965            cond,
966            then_branch,
967            else_branch,
968        } => {
969            if let Some(ty) = infer_result_type_from_expr(cond) {
970                return Some(ty);
971            }
972            if let Some(ty) = infer_result_type_from_expr(then_branch) {
973                return Some(ty);
974            }
975            if let Some(eb) = else_branch {
976                return infer_result_type_from_expr(eb);
977            }
978            None
979        }
980        Expr::Forall { body, domain, .. } | Expr::Exists { body, domain, .. } => {
981            if let Some(ty) = infer_result_type_from_expr(domain) {
982                return Some(ty);
983            }
984            infer_result_type_from_expr(body)
985        }
986        _ => None,
987    }
988}
989
990fn is_result_ident(expr: &SpExpr) -> bool {
991    matches!(&expr.node, Expr::Ident(name) if name == "result")
992}
993
994fn is_bool_literal(expr: &SpExpr) -> bool {
995    match &expr.node {
996        Expr::Literal(Literal::Bool(_)) => true,
997        Expr::Ident(name) => matches!(name.as_str(), "true" | "false"),
998        _ => false,
999    }
1000}
1001
1002/// Extract the Rust return type from an output clause body.
1003pub(crate) fn extract_output_type(body: &SpExpr) -> String {
1004    match &body.node {
1005        Expr::Call { args, .. } => {
1006            // output(result: Int) => parse the cast or ident in args
1007            for arg in args {
1008                match &arg.node {
1009                    Expr::Cast { ty, .. } => return map_type_token(ty).to_string(),
1010                    Expr::Ident(name) => return map_type_token(name).to_string(),
1011                    _ => {
1012                        let ty = extract_output_type(arg);
1013                        if ty != "()" {
1014                            return ty;
1015                        }
1016                    }
1017                }
1018            }
1019            "()".to_string()
1020        }
1021        Expr::Cast { ty, .. } => map_type_token(ty).to_string(),
1022        Expr::Ident(name) => map_type_token(name).to_string(),
1023        Expr::Tuple(items) | Expr::Block(items) => {
1024            // First typed element wins (e.g., (result: Int) parsed as tuple)
1025            for item in items {
1026                let ty = extract_output_type(item);
1027                if ty != "()" {
1028                    return ty;
1029                }
1030            }
1031            "()".to_string()
1032        }
1033        Expr::Raw(tokens) => {
1034            // Look for the type after ":" or "as"
1035            for (i, tok) in tokens.iter().enumerate() {
1036                if (tok == ":" || tok == "as") && i + 1 < tokens.len() {
1037                    let type_tokens = &tokens[i + 1..];
1038                    return map_type_tokens(type_tokens);
1039                }
1040            }
1041            if tokens.len() == 1 {
1042                return map_type_token(&tokens[0]).to_string();
1043            }
1044            "()".to_string()
1045        }
1046        // Expressions that can carry type info through structure
1047        Expr::If { then_branch, .. } => extract_output_type(then_branch),
1048        Expr::Let { body, .. } => extract_output_type(body),
1049        Expr::Match { arms, .. } => {
1050            if let Some(arm) = arms.first() {
1051                extract_output_type(&arm.body)
1052            } else {
1053                "()".to_string()
1054            }
1055        }
1056        Expr::Old(inner) | Expr::Ghost(inner) | Expr::UnaryOp { expr: inner, .. } => {
1057            extract_output_type(inner)
1058        }
1059        // These expression forms do not carry type annotations;
1060        // the output clause type cannot be determined from them.
1061        Expr::Literal(_)
1062        | Expr::Field(_, _)
1063        | Expr::MethodCall { .. }
1064        | Expr::Index { .. }
1065        | Expr::BinOp { .. }
1066        | Expr::Forall { .. }
1067        | Expr::Exists { .. }
1068        | Expr::List(_)
1069        | Expr::Apply { .. } => "()".to_string(),
1070    }
1071}
1072
1073/// Extract the variable name from an output clause body.
1074///
1075/// Given `output(value: Nat)`, the AST has a `Call { args: [Cast { expr: Ident("value"), .. }] }`.
1076/// Returns `Some("value")` if a name is found and it differs from `result`, which is already
1077/// aliased to the compiler-generated result variable by codegen. Returns `None` if the output clause has no named
1078/// binding or uses `result`.
1079pub(crate) fn extract_output_name(body: &SpExpr) -> Option<String> {
1080    match &body.node {
1081        Expr::Call { args, .. } => {
1082            for arg in args {
1083                if let Some(name) = extract_output_name(arg) {
1084                    return Some(name);
1085                }
1086            }
1087            None
1088        }
1089        Expr::Cast { expr, .. } => {
1090            // output(value: Nat) parses as Cast { expr: Ident("value"), ty: "Nat" }
1091            if let Expr::Ident(name) = &expr.node
1092                && name != "result"
1093            {
1094                return Some(name.clone());
1095            }
1096            None
1097        }
1098        Expr::Tuple(items) | Expr::Block(items) => {
1099            for item in items {
1100                if let Some(name) = extract_output_name(item) {
1101                    return Some(name);
1102                }
1103            }
1104            None
1105        }
1106        Expr::Raw(tokens) => {
1107            // Look for "name : Type" pattern
1108            for (i, tok) in tokens.iter().enumerate() {
1109                if (tok == ":" || tok == "as") && i > 0 {
1110                    let name = &tokens[i - 1];
1111                    if name != "result" {
1112                        return Some(name.clone());
1113                    }
1114                }
1115            }
1116            None
1117        }
1118        _ => None,
1119    }
1120}
1121
1122// ---------------------------------------------------------------------------
1123// Error type generation (P004)
1124// ---------------------------------------------------------------------------
1125
1126/// Extract error variant names from an `errors` clause body.
1127///
1128/// The errors clause body may be:
1129/// - `Expr::Raw(["DivByZero", ",", "Overflow"])` -> vec!["DivByZero", "Overflow"]
1130/// - `Expr::Ident("DivByZero")` -> vec!["DivByZero"]
1131/// - `Expr::Tuple([Ident("A"), Ident("B")])` -> vec!["A", "B"]
1132pub(crate) fn extract_error_variants(body: &SpExpr) -> Vec<String> {
1133    match &body.node {
1134        Expr::Ident(name) => vec![name.clone()],
1135        Expr::Tuple(items) | Expr::List(items) | Expr::Block(items) => {
1136            items.iter().flat_map(extract_error_variants).collect()
1137        }
1138        Expr::Raw(tokens) => tokens
1139            .iter()
1140            .filter(|t| {
1141                let s = t.as_str();
1142                s != "," && s != "(" && s != ")" && s != "{" && s != "}"
1143            })
1144            .cloned()
1145            .collect(),
1146        Expr::Ghost(inner) | Expr::Old(inner) => extract_error_variants(inner),
1147        Expr::Call { args, .. } => args.iter().flat_map(extract_error_variants).collect(),
1148        // These expression forms cannot meaningfully contain error variant names
1149        Expr::Literal(_)
1150        | Expr::Field(_, _)
1151        | Expr::MethodCall { .. }
1152        | Expr::Index { .. }
1153        | Expr::BinOp { .. }
1154        | Expr::UnaryOp { .. }
1155        | Expr::Cast { .. }
1156        | Expr::Forall { .. }
1157        | Expr::Exists { .. }
1158        | Expr::If { .. }
1159        | Expr::Let { .. }
1160        | Expr::Match { .. }
1161        | Expr::Apply { .. } => vec![],
1162    }
1163}
1164
1165/// Collect all error variants from a set of clauses.
1166pub(crate) fn collect_error_variants(clauses: &[Clause]) -> Vec<String> {
1167    let mut errors = Vec::new();
1168    for clause in clauses {
1169        if clause.kind == ClauseKind::Errors {
1170            errors.extend(extract_error_variants(&clause.body));
1171        }
1172    }
1173    errors
1174}
1175
1176/// Generate a `#[derive(Debug, thiserror::Error)]` enum for contract errors.
1177pub(crate) fn generate_error_enum(contract_name: &str, variants: &[String], code: &mut String) {
1178    let item = crate::hir::build_error_enum(contract_name, variants);
1179    code.push_str(&crate::hir::render_item_raw(&item));
1180}
1181#[cfg(test)]
1182#[path = "contract_tests.rs"]
1183mod tests;