1use std::collections::HashSet;
4use std::fmt::Write;
5
6use super::*;
7
8pub(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
19fn 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
118pub(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 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 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 let float_vars: HashSet<String> = input_params
185 .iter()
186 .filter(|(_, ty)| ty == "f64")
187 .map(|(name, _)| name.clone())
188 .collect();
189
190 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 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 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 let mut feature_code = String::new();
253 crate::features::generate_all_feature_clauses(&c.clauses, &c.name, &mut feature_code);
254
255 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 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 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 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 let mut body: Vec<RustStmt> = Vec::new();
301
302 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 for req in &requires_exprs {
315 body.push(RustStmt::Assert {
316 cond: req.clone(),
317 label: "requires".into(),
318 });
319 }
320
321 if !feature_code.is_empty() {
323 body.push(RustStmt::Raw(feature_code));
324 }
325
326 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 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 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 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
498pub(crate) fn proptest_strategy_for_type(rust_type: &str) -> String {
504 match rust_type {
505 "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#[derive(Debug, Clone)]
527pub(crate) enum ParamBound {
528 GteVal(i64),
530 GtVal(i64),
532 LteVal(i64),
534 LtVal(i64),
536 NeqZero,
538}
539
540pub(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#[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 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 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
637pub(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
650pub(crate) fn generate_proptest_for_contract(c: &ContractDecl, code: &mut String) {
659 let fn_name = c.name.to_lowercase();
661 generate_proptest_impl(c, code, &format!("super::contract_{fn_name}::check"));
662}
663
664pub(crate) fn generate_proptest_for_contract_contents(c: &ContractDecl, code: &mut String) {
667 generate_proptest_impl(c, code, "super::check");
668}
669
670fn 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 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 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 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 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 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
792pub(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
831pub(crate) fn generate_interface_trait_from_contract(c: &ContractDecl, code: &mut String) {
836 generate_interface_trait(&c.name, &c.clauses, code);
837}
838
839pub 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 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
873pub 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
906fn 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 "i64".to_string()
924}
925
926fn infer_result_type_from_expr(expr: &SpExpr) -> Option<String> {
928 match &expr.node {
929 Expr::BinOp { lhs, op, rhs } => {
930 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 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 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 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
1002pub(crate) fn extract_output_type(body: &SpExpr) -> String {
1004 match &body.node {
1005 Expr::Call { args, .. } => {
1006 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 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 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 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 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
1073pub(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 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 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
1122pub(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 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
1165pub(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
1176pub(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;