1use std::collections::HashSet;
2
3use crate::ast::{
4 Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, StrPart, TailCallData,
5 TopLevel, TypeDef, TypeVariant, VerifyBlock, VerifyGivenDomain, VerifyKind,
6};
7use crate::codegen::CodegenContext;
8use crate::types::Type;
9
10#[derive(Debug, Clone)]
24pub struct RefinementInfo<'a> {
25 pub carrier_type: &'a str,
29 pub carrier_field: &'a str,
33 pub param_name: &'a str,
37 pub predicate: &'a Spanned<Expr>,
40}
41
42pub fn refinement_info_for<'a>(
53 type_name: &str,
54 inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
55) -> Option<RefinementInfo<'a>> {
56 refinement_info_for_walk(type_name, inputs, None)
57}
58
59pub fn refinement_info_for_in_scope<'a>(
72 type_name: &str,
73 inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
74 scope: Option<&str>,
75) -> Option<RefinementInfo<'a>> {
76 refinement_info_for_walk(type_name, inputs, Some(scope))
77}
78
79fn refinement_info_for_walk<'a>(
80 type_name: &str,
81 inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
82 scope_filter: Option<Option<&str>>,
86) -> Option<RefinementInfo<'a>> {
87 let entry_only = matches!(scope_filter, Some(None));
88 let module_only_prefix = match scope_filter {
89 Some(Some(p)) => Some(p),
90 _ => None,
91 };
92 let allow_entry = scope_filter.is_none() || entry_only;
93 let entry_typedefs: Vec<&'a TypeDef> = if allow_entry {
94 inputs
95 .entry_items
96 .iter()
97 .filter_map(|item| match item {
98 TopLevel::TypeDef(td) => Some(td),
99 _ => None,
100 })
101 .collect()
102 } else {
103 Vec::new()
104 };
105 let module_typedefs: Vec<&'a TypeDef> = inputs
106 .dep_modules
107 .iter()
108 .filter(|m| match (scope_filter, module_only_prefix) {
109 (None, _) => true,
110 (Some(None), _) => false,
111 (Some(Some(_)), Some(p)) => m.prefix == p,
112 _ => false,
113 })
114 .flat_map(|m| m.type_defs.iter())
115 .collect();
116 let (carrier_field, carrier_type) = entry_typedefs
117 .into_iter()
118 .chain(module_typedefs)
119 .find_map(|td| match td {
120 TypeDef::Product { name, fields, .. } if name == type_name && fields.len() == 1 => {
121 let (fname, ftype) = &fields[0];
122 Some((fname.as_str(), ftype.as_str()))
123 }
124 _ => None,
125 })?;
126
127 let entry_fns: Vec<&'a FnDef> = if allow_entry {
128 inputs
129 .entry_items
130 .iter()
131 .filter_map(|item| match item {
132 TopLevel::FnDef(fd) => Some(fd),
133 _ => None,
134 })
135 .collect()
136 } else {
137 Vec::new()
138 };
139 let module_fns: Vec<&'a FnDef> = inputs
140 .dep_modules
141 .iter()
142 .filter(|m| match (scope_filter, module_only_prefix) {
143 (None, _) => true,
144 (Some(None), _) => false,
145 (Some(Some(_)), Some(p)) => m.prefix == p,
146 _ => false,
147 })
148 .flat_map(|m| m.fn_defs.iter())
149 .collect();
150 for fd in entry_fns.into_iter().chain(module_fns) {
151 if !fd.return_type.starts_with("Result<") {
152 continue;
153 }
154 if !fd.return_type[7..].starts_with(type_name) {
155 continue;
156 }
157 if fd.params.len() != 1 {
158 continue;
159 }
160 let (param_name, _) = &fd.params[0];
161 let stmts = fd.body.stmts();
162 if stmts.len() != 1 {
163 continue;
164 }
165 let Stmt::Expr(body_expr) = &stmts[0] else {
166 continue;
167 };
168 let Expr::Match { subject, arms } = &body_expr.node else {
169 continue;
170 };
171 if !is_bool_ok_err_match(arms, type_name, carrier_field, param_name) {
172 continue;
173 }
174 return Some(RefinementInfo {
175 carrier_type,
176 carrier_field,
177 param_name,
178 predicate: subject,
179 });
180 }
181 None
182}
183
184fn is_bool_ok_err_match(
190 arms: &[MatchArm],
191 type_name: &str,
192 carrier_field: &str,
193 param_name: &str,
194) -> bool {
195 if arms.len() != 2 {
196 return false;
197 }
198 let mut true_ok = false;
199 let mut false_err = false;
200 for arm in arms {
201 match &arm.pattern {
202 Pattern::Literal(Literal::Bool(true)) => {
203 if is_ok_constructor_with_identity(&arm.body, type_name, carrier_field, param_name)
204 {
205 true_ok = true;
206 }
207 }
208 Pattern::Literal(Literal::Bool(false)) => {
209 if is_err_constructor(&arm.body) {
210 false_err = true;
211 }
212 }
213 _ => return false,
214 }
215 }
216 true_ok && false_err
217}
218
219fn is_ok_constructor_with_identity(
220 expr: &Spanned<Expr>,
221 type_name: &str,
222 carrier_field: &str,
223 param_name: &str,
224) -> bool {
225 let (ctor_name, ctor_arg_node) = match &expr.node {
227 Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
228 Expr::FnCall(callee, args) if args.len() == 1 => {
229 let Some(name) = expr_to_dotted_name(&callee.node) else {
230 return false;
231 };
232 (name, &args[0].node)
233 }
234 _ => return false,
235 };
236 if ctor_name != "Result.Ok" {
237 return false;
238 }
239 let (t, fields) = match ctor_arg_node {
240 Expr::RecordCreate {
241 type_name: t,
242 fields,
243 } => (t.as_str(), fields),
244 _ => return false,
245 };
246 if t != type_name || fields.len() != 1 {
247 return false;
248 }
249 let (fname, fvalue) = &fields[0];
250 if fname != carrier_field {
251 return false;
252 }
253 match &fvalue.node {
258 Expr::Ident(name) | Expr::Resolved { name, .. } => name == param_name,
259 _ => false,
260 }
261}
262
263pub fn refinement_lift_for_given(
273 given_name: &str,
274 given_type: &str,
275 lhs: &Spanned<Expr>,
276 rhs: &Spanned<Expr>,
277 ctx: &CodegenContext,
278) -> Option<String> {
279 if given_type == "Float" {
287 return None;
288 }
289 let mut result: Option<String> = None;
297 search_refinement_wrapper(lhs, given_name, given_type, ctx, &mut result);
298 search_refinement_wrapper(rhs, given_name, given_type, ctx, &mut result);
299 result
300}
301
302fn search_refinement_wrapper(
303 expr: &Spanned<Expr>,
304 given_name: &str,
305 given_type: &str,
306 ctx: &CodegenContext,
307 result: &mut Option<String>,
308) {
309 if result.is_some() {
310 return;
311 }
312 match &expr.node {
313 Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
314 let (_, fvalue) = &fields[0];
315 let matches_var = matches!(
316 &fvalue.node,
317 Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == given_name
318 );
319 if matches_var
320 && let Some((canonical_key, decl)) = find_refined_type_with_key(ctx, type_name)
321 && decl.carrier_type == given_type
322 {
323 *result = Some(canonical_key);
324 return;
325 }
326 for (_, v) in fields {
327 search_refinement_wrapper(v, given_name, given_type, ctx, result);
328 }
329 }
330 Expr::FnCall(callee, args) => {
331 search_refinement_wrapper(callee, given_name, given_type, ctx, result);
332 for a in args {
333 search_refinement_wrapper(a, given_name, given_type, ctx, result);
334 }
335 }
336 Expr::BinOp(_, l, r) => {
337 search_refinement_wrapper(l, given_name, given_type, ctx, result);
338 search_refinement_wrapper(r, given_name, given_type, ctx, result);
339 }
340 Expr::Attr(o, _) => search_refinement_wrapper(o, given_name, given_type, ctx, result),
341 Expr::Neg(i) | Expr::ErrorProp(i) => {
342 search_refinement_wrapper(i, given_name, given_type, ctx, result);
343 }
344 Expr::Match { subject, arms } => {
345 search_refinement_wrapper(subject, given_name, given_type, ctx, result);
346 for arm in arms {
347 search_refinement_wrapper(&arm.body, given_name, given_type, ctx, result);
348 }
349 }
350 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
351 for it in items {
352 search_refinement_wrapper(it, given_name, given_type, ctx, result);
353 }
354 }
355 Expr::Constructor(_, Some(arg)) => {
356 search_refinement_wrapper(arg, given_name, given_type, ctx, result);
357 }
358 _ => {}
359 }
360}
361
362pub fn project_lifted_idents_to_val(
389 expr: &Spanned<Expr>,
390 lifted_vars: &std::collections::HashMap<String, String>,
391) -> Spanned<Expr> {
392 if lifted_vars.is_empty() {
393 return expr.clone();
394 }
395 let new_node = match &expr.node {
396 Expr::BinOp(op, l, r) if is_comparator_binop(*op) => {
397 let l_proj = project_lifted_ident_leaf(l, lifted_vars);
398 let r_proj = project_lifted_ident_leaf(r, lifted_vars);
399 Expr::BinOp(*op, Box::new(l_proj), Box::new(r_proj))
400 }
401 Expr::FnCall(callee, args) => {
402 let name = expr_to_dotted_name(&callee.node);
403 if matches!(name.as_deref(), Some("Bool.and") | Some("Bool.or")) {
404 Expr::FnCall(
405 callee.clone(),
406 args.iter()
407 .map(|a| project_lifted_idents_to_val(a, lifted_vars))
408 .collect(),
409 )
410 } else {
411 return expr.clone();
412 }
413 }
414 _ => return expr.clone(),
415 };
416 Spanned::new(new_node, expr.line)
417}
418
419fn is_comparator_binop(op: crate::ast::BinOp) -> bool {
420 use crate::ast::BinOp::*;
421 matches!(op, Lt | Gt | Lte | Gte | Eq | Neq)
422}
423
424fn project_lifted_ident_leaf(
425 expr: &Spanned<Expr>,
426 lifted_vars: &std::collections::HashMap<String, String>,
427) -> Spanned<Expr> {
428 let target_name = match &expr.node {
429 Expr::Ident(n) | Expr::Resolved { name: n, .. } => n,
430 _ => return expr.clone(),
431 };
432 if lifted_vars.contains_key(target_name) {
433 Spanned::new(
434 Expr::Attr(
435 Box::new(Spanned::new(Expr::Ident(target_name.clone()), expr.line)),
436 "val".to_string(),
437 ),
438 expr.line,
439 )
440 } else {
441 expr.clone()
442 }
443}
444
445pub fn strip_refinement_wrappers(
446 expr: &Spanned<Expr>,
447 lifted_vars: &std::collections::HashMap<String, String>,
448 ctx: &CodegenContext,
449) -> Spanned<Expr> {
450 let new_node = match &expr.node {
451 Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
452 let (_, fvalue) = &fields[0];
453 let var_name = match &fvalue.node {
454 Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
455 _ => None,
456 };
457 let canonical_for_ast = find_refined_type_with_key(ctx, type_name).map(|(k, _)| k);
464 if let Some(name) = var_name
465 && let Some(refined) = lifted_vars.get(&name)
466 && canonical_for_ast.as_deref() == Some(refined.as_str())
467 {
468 return Spanned::new(Expr::Ident(name), expr.line);
469 }
470 let new_fields: Vec<(String, Spanned<Expr>)> = fields
471 .iter()
472 .map(|(n, v)| (n.clone(), strip_refinement_wrappers(v, lifted_vars, ctx)))
473 .collect();
474 Expr::RecordCreate {
475 type_name: type_name.clone(),
476 fields: new_fields,
477 }
478 }
479 Expr::FnCall(callee, args) => Expr::FnCall(
480 Box::new(strip_refinement_wrappers(callee, lifted_vars, ctx)),
481 args.iter()
482 .map(|a| strip_refinement_wrappers(a, lifted_vars, ctx))
483 .collect(),
484 ),
485 Expr::BinOp(op, l, r) => Expr::BinOp(
486 *op,
487 Box::new(strip_refinement_wrappers(l, lifted_vars, ctx)),
488 Box::new(strip_refinement_wrappers(r, lifted_vars, ctx)),
489 ),
490 Expr::Attr(o, f) => Expr::Attr(
491 Box::new(strip_refinement_wrappers(o, lifted_vars, ctx)),
492 f.clone(),
493 ),
494 Expr::Neg(i) => Expr::Neg(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx))),
495 Expr::ErrorProp(i) => {
496 Expr::ErrorProp(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx)))
497 }
498 _ => expr.node.clone(),
499 };
500 Spanned::new(new_node, expr.line)
501}
502
503pub fn swap_comparison_operands_op(op: &crate::ast::BinOp) -> Option<crate::ast::BinOp> {
509 use crate::ast::BinOp::*;
510 match op {
511 Lt => Some(Gt),
512 Gt => Some(Lt),
513 Lte => Some(Gte),
514 Gte => Some(Lte),
515 Eq => Some(Eq),
516 Neq => Some(Neq),
517 _ => None,
518 }
519}
520
521pub fn predicate_syntactic_eq(a: &Spanned<Expr>, b: &Spanned<Expr>) -> bool {
532 match (&a.node, &b.node) {
533 (Expr::BinOp(op_a, la, ra), Expr::BinOp(op_b, lb, rb)) => {
534 if op_a == op_b && predicate_syntactic_eq(la, lb) && predicate_syntactic_eq(ra, rb) {
535 return true;
536 }
537 if let Some(swapped) = swap_comparison_operands_op(op_a)
538 && &swapped == op_b
539 && predicate_syntactic_eq(la, rb)
540 && predicate_syntactic_eq(ra, lb)
541 {
542 return true;
543 }
544 false
545 }
546 _ => a.node == b.node,
547 }
548}
549
550pub fn flatten_bool_and_conjuncts(expr: &Spanned<Expr>) -> Vec<Spanned<Expr>> {
557 if let Expr::FnCall(callee, args) = &expr.node
558 && args.len() == 2
559 && let Some(name) = expr_to_dotted_name(&callee.node)
560 && name == "Bool.and"
561 {
562 let mut out = flatten_bool_and_conjuncts(&args[0]);
563 out.extend(flatten_bool_and_conjuncts(&args[1]));
564 return out;
565 }
566 vec![expr.clone()]
567}
568
569pub fn flatten_bool_and_conjuncts_resolved(
574 expr: &Spanned<crate::ir::hir::ResolvedExpr>,
575) -> Vec<Spanned<crate::ir::hir::ResolvedExpr>> {
576 use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
577 if let ResolvedExpr::Call(ResolvedCallee::Builtin(name), args) = &expr.node
578 && name == "Bool.and"
579 && args.len() == 2
580 {
581 let mut out = flatten_bool_and_conjuncts_resolved(&args[0]);
582 out.extend(flatten_bool_and_conjuncts_resolved(&args[1]));
583 return out;
584 }
585 vec![expr.clone()]
586}
587
588pub fn predicate_syntactic_eq_resolved(
590 a: &Spanned<crate::ir::hir::ResolvedExpr>,
591 b: &Spanned<crate::ir::hir::ResolvedExpr>,
592) -> bool {
593 use crate::ir::hir::ResolvedExpr;
594 match (&a.node, &b.node) {
595 (ResolvedExpr::BinOp(op_a, la, ra), ResolvedExpr::BinOp(op_b, lb, rb)) => {
596 if op_a == op_b
597 && predicate_syntactic_eq_resolved(la, lb)
598 && predicate_syntactic_eq_resolved(ra, rb)
599 {
600 return true;
601 }
602 if let Some(swapped) = swap_comparison_operands_op(op_a)
603 && &swapped == op_b
604 && predicate_syntactic_eq_resolved(la, rb)
605 && predicate_syntactic_eq_resolved(ra, lb)
606 {
607 return true;
608 }
609 false
610 }
611 _ => a.node == b.node,
612 }
613}
614
615pub fn substitute_ident_in_resolved_expr(
623 expr: &Spanned<crate::ir::hir::ResolvedExpr>,
624 from: &str,
625 to: &str,
626) -> Spanned<crate::ir::hir::ResolvedExpr> {
627 use crate::ir::hir::{ResolvedExpr, ResolvedMatchArm, ResolvedStrPart};
628 let line = expr.line;
629 let rec = |e: &Spanned<ResolvedExpr>| substitute_ident_in_resolved_expr(e, from, to);
630 let new_node = match &expr.node {
631 ResolvedExpr::Ident(name) | ResolvedExpr::Resolved { name, .. } if name == from => {
632 ResolvedExpr::Ident(to.to_string())
633 }
634 ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {
635 return expr.clone();
636 }
637 ResolvedExpr::Attr(obj, field) => ResolvedExpr::Attr(Box::new(rec(obj)), field.clone()),
638 ResolvedExpr::Call(callee, args) => {
639 ResolvedExpr::Call(callee.clone(), args.iter().map(&rec).collect())
640 }
641 ResolvedExpr::BinOp(op, left, right) => {
642 ResolvedExpr::BinOp(*op, Box::new(rec(left)), Box::new(rec(right)))
643 }
644 ResolvedExpr::Neg(inner) => ResolvedExpr::Neg(Box::new(rec(inner))),
645 ResolvedExpr::Match { subject, arms } => ResolvedExpr::Match {
646 subject: Box::new(rec(subject)),
647 arms: arms
648 .iter()
649 .map(|arm| ResolvedMatchArm {
650 pattern: arm.pattern.clone(),
651 body: Box::new(rec(&arm.body)),
652 binding_slots: std::sync::OnceLock::new(),
653 })
654 .collect(),
655 },
656 ResolvedExpr::Ctor(ctor, args) => {
657 ResolvedExpr::Ctor(ctor.clone(), args.iter().map(&rec).collect())
658 }
659 ResolvedExpr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(rec(inner))),
660 ResolvedExpr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
661 parts
662 .iter()
663 .map(|p| match p {
664 ResolvedStrPart::Literal(_) => p.clone(),
665 ResolvedStrPart::Parsed(inner) => ResolvedStrPart::Parsed(Box::new(rec(inner))),
666 })
667 .collect(),
668 ),
669 ResolvedExpr::List(items) => ResolvedExpr::List(items.iter().map(&rec).collect()),
670 ResolvedExpr::Tuple(items) => ResolvedExpr::Tuple(items.iter().map(&rec).collect()),
671 ResolvedExpr::IndependentProduct(items, flag) => {
672 ResolvedExpr::IndependentProduct(items.iter().map(&rec).collect(), *flag)
673 }
674 ResolvedExpr::MapLiteral(entries) => {
675 ResolvedExpr::MapLiteral(entries.iter().map(|(k, v)| (rec(k), rec(v))).collect())
676 }
677 ResolvedExpr::RecordCreate {
678 type_id,
679 type_name,
680 fields,
681 } => ResolvedExpr::RecordCreate {
682 type_id: *type_id,
683 type_name: type_name.clone(),
684 fields: fields.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
685 },
686 ResolvedExpr::RecordUpdate {
687 type_id,
688 type_name,
689 base,
690 updates,
691 } => ResolvedExpr::RecordUpdate {
692 type_id: *type_id,
693 type_name: type_name.clone(),
694 base: Box::new(rec(base)),
695 updates: updates.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
696 },
697 ResolvedExpr::TailCall { target, args } => ResolvedExpr::TailCall {
698 target: *target,
699 args: args.iter().map(&rec).collect(),
700 },
701 };
702 Spanned::new(new_node, line)
703}
704
705pub fn substitute_ident_in_expr(expr: &Spanned<Expr>, from: &str, to: &str) -> Spanned<Expr> {
714 use crate::ast::{MatchArm, StrPart, TailCallData};
715 let line = expr.line;
716 let new_node = match &expr.node {
717 Expr::Ident(name) | Expr::Resolved { name, .. } if name == from => {
718 Expr::Ident(to.to_string())
719 }
720 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => return expr.clone(),
721 Expr::Attr(obj, field) => Expr::Attr(
722 Box::new(substitute_ident_in_expr(obj, from, to)),
723 field.clone(),
724 ),
725 Expr::FnCall(callee, args) => Expr::FnCall(
726 Box::new(substitute_ident_in_expr(callee, from, to)),
727 args.iter()
728 .map(|a| substitute_ident_in_expr(a, from, to))
729 .collect(),
730 ),
731 Expr::BinOp(op, left, right) => Expr::BinOp(
732 *op,
733 Box::new(substitute_ident_in_expr(left, from, to)),
734 Box::new(substitute_ident_in_expr(right, from, to)),
735 ),
736 Expr::Neg(inner) => Expr::Neg(Box::new(substitute_ident_in_expr(inner, from, to))),
737 Expr::Match { subject, arms } => Expr::Match {
738 subject: Box::new(substitute_ident_in_expr(subject, from, to)),
739 arms: arms
740 .iter()
741 .map(|arm| MatchArm {
742 pattern: arm.pattern.clone(),
743 body: Box::new(substitute_ident_in_expr(&arm.body, from, to)),
744 binding_slots: std::sync::OnceLock::new(),
745 })
746 .collect(),
747 },
748 Expr::Constructor(name, arg) => Expr::Constructor(
749 name.clone(),
750 arg.as_ref()
751 .map(|inner| Box::new(substitute_ident_in_expr(inner, from, to))),
752 ),
753 Expr::ErrorProp(inner) => {
754 Expr::ErrorProp(Box::new(substitute_ident_in_expr(inner, from, to)))
755 }
756 Expr::InterpolatedStr(parts) => Expr::InterpolatedStr(
757 parts
758 .iter()
759 .map(|part| match part {
760 StrPart::Literal(_) => part.clone(),
761 StrPart::Parsed(inner) => {
762 StrPart::Parsed(Box::new(substitute_ident_in_expr(inner, from, to)))
763 }
764 })
765 .collect(),
766 ),
767 Expr::List(items) => Expr::List(
768 items
769 .iter()
770 .map(|item| substitute_ident_in_expr(item, from, to))
771 .collect(),
772 ),
773 Expr::Tuple(items) => Expr::Tuple(
774 items
775 .iter()
776 .map(|item| substitute_ident_in_expr(item, from, to))
777 .collect(),
778 ),
779 Expr::IndependentProduct(items, flag) => Expr::IndependentProduct(
780 items
781 .iter()
782 .map(|item| substitute_ident_in_expr(item, from, to))
783 .collect(),
784 *flag,
785 ),
786 Expr::MapLiteral(entries) => Expr::MapLiteral(
787 entries
788 .iter()
789 .map(|(k, v)| {
790 (
791 substitute_ident_in_expr(k, from, to),
792 substitute_ident_in_expr(v, from, to),
793 )
794 })
795 .collect(),
796 ),
797 Expr::RecordCreate { type_name, fields } => Expr::RecordCreate {
798 type_name: type_name.clone(),
799 fields: fields
800 .iter()
801 .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
802 .collect(),
803 },
804 Expr::RecordUpdate {
805 type_name,
806 base,
807 updates,
808 } => Expr::RecordUpdate {
809 type_name: type_name.clone(),
810 base: Box::new(substitute_ident_in_expr(base, from, to)),
811 updates: updates
812 .iter()
813 .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
814 .collect(),
815 },
816 Expr::TailCall(boxed) => Expr::TailCall(Box::new(TailCallData::new(
817 boxed.target.clone(),
818 boxed
819 .args
820 .iter()
821 .map(|a| substitute_ident_in_expr(a, from, to))
822 .collect(),
823 ))),
824 };
825 Spanned::new(new_node, line)
826}
827
828pub fn when_is_redundant_with_refinement_lifts(
844 when_expr: &Spanned<Expr>,
845 lifted_vars: &std::collections::HashMap<String, String>,
846 ctx: &CodegenContext,
847) -> bool {
848 if lifted_vars.is_empty() {
849 return false;
850 }
851 let resolved_when = ctx.resolve_expr(when_expr, ctx.active_module_scope().as_deref());
859 let when_conjuncts = flatten_bool_and_conjuncts_resolved(&resolved_when);
860 let mut lifted_predicates: Vec<Spanned<crate::ir::hir::ResolvedExpr>> = Vec::new();
874 for (given_name, refined_type) in lifted_vars {
875 let Some(decl) = find_refined_type(ctx, refined_type) else {
876 return false;
877 };
878 let substituted = substitute_ident_in_resolved_expr(
879 &decl.invariant.expr,
880 decl.predicate_param.as_str(),
881 given_name,
882 );
883 lifted_predicates.extend(flatten_bool_and_conjuncts_resolved(&substituted));
884 }
885 if when_conjuncts.len() != lifted_predicates.len() {
886 return false;
887 }
888 let mut matched = vec![false; lifted_predicates.len()];
889 for wc in &when_conjuncts {
890 let Some(idx) = (0..lifted_predicates.len())
891 .find(|&i| !matched[i] && predicate_syntactic_eq_resolved(wc, &lifted_predicates[i]))
892 else {
893 return false;
894 };
895 matched[idx] = true;
896 }
897 true
898}
899
900fn is_err_constructor(expr: &Spanned<Expr>) -> bool {
901 match &expr.node {
902 Expr::Constructor(name, Some(_)) => name == "Result.Err",
903 Expr::FnCall(callee, args) if args.len() == 1 => {
904 matches!(
905 expr_to_dotted_name(&callee.node),
906 Some(name) if name == "Result.Err"
907 )
908 }
909 _ => false,
910 }
911}
912
913pub fn is_pure_fn(fd: &FnDef) -> bool {
919 fd.effects.is_empty() && fd.name != "main"
920}
921
922pub fn find_refined_type<'a>(
949 ctx: &'a CodegenContext,
950 name: &str,
951) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
952 find_refined_type_with_key_scoped(ctx, name, None).map(|(_, d)| d)
953}
954
955pub fn backend_named_type_key(ctx: &CodegenContext, ty: &crate::types::Type) -> Option<String> {
984 let crate::types::Type::Named { id, name } = ty else {
985 return None;
986 };
987 if let Some(type_id) = id {
988 return Some(ctx.symbol_table.type_entry(*type_id).key.canonical());
989 }
990 Some(name.clone())
991}
992
993pub fn backend_type_def_key(ctx: &CodegenContext, td: &crate::ast::TypeDef) -> String {
1008 let key = type_key_for_decl(ctx, td);
1009 if ctx.symbol_table.type_id_of(&key).is_some() {
1010 key.canonical()
1011 } else {
1012 type_def_name(td).to_string()
1013 }
1014}
1015
1016pub fn find_refined_type_by_id(
1032 ctx: &CodegenContext,
1033 type_id: crate::ir::TypeId,
1034) -> Option<&crate::ir::proof_ir::RefinedTypeDecl> {
1035 ctx.proof_ir.refined_types.get(&type_id)
1036}
1037
1038pub fn find_refined_type_for_named<'a>(
1047 ctx: &'a CodegenContext,
1048 named: &crate::types::Type,
1049) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1050 let crate::types::Type::Named { id, name } = named else {
1051 return None;
1052 };
1053 match id {
1054 Some(type_id) => find_refined_type_by_id(ctx, *type_id),
1055 None => find_refined_type(ctx, name),
1056 }
1057}
1058
1059pub fn find_fn_contract_scoped<'a>(
1071 ctx: &'a CodegenContext,
1072 name: &str,
1073 scope: Option<&str>,
1074) -> Option<&'a crate::ir::proof_ir::FnContract> {
1075 let symbols = &ctx.symbol_table;
1083 let bare = name.rsplit('.').next().unwrap_or(name);
1084 let name_is_already_qualified = name.contains('.');
1085 let try_key = |key: crate::ir::FnKey| -> Option<&'a crate::ir::proof_ir::FnContract> {
1086 let id = symbols.fn_id_of(&key)?;
1087 ctx.proof_ir.fn_contracts.get(&id)
1088 };
1089 if let Some(prefix) = scope
1090 && !name_is_already_qualified
1091 && let Some(c) = try_key(crate::ir::FnKey::in_module(prefix.to_string(), bare))
1092 {
1093 return Some(c);
1094 }
1095 let direct_key =
1096 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1097 crate::ir::FnKey::in_module(prefix.to_string(), bare_part)
1098 } else {
1099 crate::ir::FnKey::entry(name)
1100 };
1101 if let Some(c) = try_key(direct_key) {
1102 return Some(c);
1103 }
1104 for m in &ctx.modules {
1105 for fd in &m.fn_defs {
1106 if fd.name == bare
1107 && let Some(c) = try_key(crate::ir::FnKey::in_module(m.prefix.clone(), bare))
1108 {
1109 return Some(c);
1110 }
1111 }
1112 }
1113 None
1114}
1115
1116pub fn fn_contract_exists_scoped(ctx: &CodegenContext, name: &str, scope: Option<&str>) -> bool {
1118 find_fn_contract_scoped(ctx, name, scope).is_some()
1119}
1120
1121pub fn fn_key_for_decl(ctx: &CodegenContext, fd: &FnDef) -> crate::ir::FnKey {
1129 match fn_owning_scope_for(ctx, fd) {
1130 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), fd.name.clone()),
1131 None => crate::ir::FnKey::entry(fd.name.clone()),
1132 }
1133}
1134
1135pub fn type_key_for_decl(ctx: &CodegenContext, td: &TypeDef) -> crate::ir::TypeKey {
1139 for m in &ctx.modules {
1140 for t in &m.type_defs {
1141 if std::ptr::eq(t, td) {
1142 return crate::ir::TypeKey::in_module(m.prefix.clone(), type_def_name(td));
1143 }
1144 }
1145 }
1146 crate::ir::TypeKey::entry(type_def_name(td))
1147}
1148
1149pub fn type_key_for_name(
1156 ctx: &CodegenContext,
1157 name: &str,
1158 scope: Option<&str>,
1159) -> crate::ir::TypeKey {
1160 let bare = name.rsplit('.').next().unwrap_or(name);
1161 let name_is_qualified = name.contains('.');
1162 if let Some(prefix) = scope
1163 && !name_is_qualified
1164 {
1165 for m in &ctx.modules {
1166 if m.prefix == prefix && m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1167 return crate::ir::TypeKey::in_module(prefix.to_string(), bare);
1168 }
1169 }
1170 }
1171 if !name_is_qualified && ctx.type_defs.iter().any(|td| type_def_name(td) == bare) {
1174 return crate::ir::TypeKey::entry(bare);
1175 }
1176 if name_is_qualified
1177 && let Some((prefix, bare_part)) = name.rsplit_once('.')
1178 && ctx.modules.iter().any(|m| m.prefix == prefix)
1179 {
1180 return crate::ir::TypeKey::in_module(prefix.to_string(), bare_part);
1181 }
1182 for m in &ctx.modules {
1183 if m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1184 return crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1185 }
1186 }
1187 crate::ir::TypeKey::entry(name)
1192}
1193
1194pub fn fn_owning_scope_for<'a>(ctx: &'a CodegenContext, fd: &FnDef) -> Option<&'a str> {
1204 for m in &ctx.modules {
1205 for f in &m.fn_defs {
1206 if std::ptr::eq(f, fd) {
1207 return Some(m.prefix.as_str());
1208 }
1209 }
1210 }
1211 None
1212}
1213
1214pub fn find_fn_contract_for_fn<'a>(
1220 ctx: &'a CodegenContext,
1221 fd: &FnDef,
1222) -> Option<&'a crate::ir::proof_ir::FnContract> {
1223 let symbols = &ctx.symbol_table;
1229 let fn_key = fn_key_for_decl(ctx, fd);
1230 let fn_id = symbols.fn_id_of(&fn_key)?;
1231 ctx.proof_ir.fn_contracts.get(&fn_id)
1232}
1233
1234pub fn fn_contract_exists_for_fn(ctx: &CodegenContext, fd: &FnDef) -> bool {
1236 find_fn_contract_for_fn(ctx, fd).is_some()
1237}
1238
1239pub fn fn_id_for_decl(ctx: &CodegenContext, fd: &FnDef) -> Option<crate::ir::FnId> {
1245 let fn_key = fn_key_for_decl(ctx, fd);
1246 ctx.symbol_table.fn_id_of(&fn_key)
1247}
1248
1249pub fn fn_id_for_dotted_name(ctx: &CodegenContext, dotted: &str) -> Option<crate::ir::FnId> {
1255 let key = if let Some((prefix, bare)) = dotted.rsplit_once('.') {
1256 crate::ir::FnKey::in_module(prefix.to_string(), bare)
1257 } else {
1258 crate::ir::FnKey::entry(dotted)
1259 };
1260 ctx.symbol_table.fn_id_of(&key)
1261}
1262
1263pub fn find_refined_type_with_key<'a>(
1268 ctx: &'a CodegenContext,
1269 name: &str,
1270) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1271 find_refined_type_with_key_scoped(ctx, name, None)
1272}
1273
1274pub fn find_refined_type_scoped<'a>(
1292 ctx: &'a CodegenContext,
1293 name: &str,
1294 scope: Option<&str>,
1295) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1296 find_refined_type_with_key_scoped(ctx, name, scope).map(|(_, d)| d)
1297}
1298
1299pub fn find_refined_type_with_key_scoped<'a>(
1314 ctx: &'a CodegenContext,
1315 name: &str,
1316 scope: Option<&str>,
1317) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1318 let symbols = &ctx.symbol_table;
1319 let bare = name.rsplit('.').next().unwrap_or(name);
1320 let name_is_already_qualified = name.contains('.');
1321 let try_key =
1322 |key: crate::ir::TypeKey| -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1323 let id = symbols.type_id_of(&key)?;
1324 let decl = ctx.proof_ir.refined_types.get(&id)?;
1325 Some((key.canonical(), decl))
1326 };
1327 if let Some(prefix) = scope
1328 && !name_is_already_qualified
1329 && let Some(hit) = try_key(crate::ir::TypeKey::in_module(prefix.to_string(), bare))
1330 {
1331 return Some(hit);
1332 }
1333 let direct_key =
1334 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1335 crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1336 } else {
1337 crate::ir::TypeKey::entry(name)
1338 };
1339 if let Some(hit) = try_key(direct_key) {
1340 return Some(hit);
1341 }
1342 for m in &ctx.modules {
1343 for td in &m.type_defs {
1344 if type_def_name(td) == bare
1345 && let Some(hit) = try_key(crate::ir::TypeKey::in_module(m.prefix.clone(), bare))
1346 {
1347 return Some(hit);
1348 }
1349 }
1350 }
1351 None
1352}
1353
1354pub fn resolve_refined_type_in<'a>(
1360 refined_types: &'a std::collections::HashMap<
1361 crate::ir::TypeId,
1362 crate::ir::proof_ir::RefinedTypeDecl,
1363 >,
1364 symbols: &crate::ir::SymbolTable,
1365 modules: &[crate::codegen::ModuleInfo],
1366 name: &str,
1367) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1368 resolve_refined_type_in_with_key(refined_types, symbols, modules, name).map(|(_, d)| d)
1369}
1370
1371pub fn resolve_refined_type_in_with_key<'a>(
1377 refined_types: &'a std::collections::HashMap<
1378 crate::ir::TypeId,
1379 crate::ir::proof_ir::RefinedTypeDecl,
1380 >,
1381 symbols: &crate::ir::SymbolTable,
1382 modules: &[crate::codegen::ModuleInfo],
1383 name: &str,
1384) -> Option<(crate::ir::TypeId, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1385 let bare = name.rsplit('.').next().unwrap_or(name);
1386 let name_is_already_qualified = name.contains('.');
1387 let direct_key =
1388 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1389 crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1390 } else {
1391 crate::ir::TypeKey::entry(name)
1392 };
1393 if let Some(id) = symbols.type_id_of(&direct_key)
1394 && let Some(decl) = refined_types.get(&id)
1395 {
1396 return Some((id, decl));
1397 }
1398 for m in modules {
1399 for td in &m.type_defs {
1400 if type_def_name(td) == bare {
1401 let canonical = crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1402 if let Some(id) = symbols.type_id_of(&canonical)
1403 && let Some(decl) = refined_types.get(&id)
1404 {
1405 return Some((id, decl));
1406 }
1407 }
1408 }
1409 }
1410 None
1411}
1412
1413pub fn all_givens_are_singletons(law: &crate::ast::VerifyLaw) -> bool {
1422 !law.givens.is_empty()
1423 && law.givens.iter().all(|g| match &g.domain {
1424 VerifyGivenDomain::Explicit(values) => values.len() == 1,
1425 VerifyGivenDomain::IntRange { start, end } => start == end,
1426 })
1427}
1428
1429pub fn unclassified_fn_names(ctx: &CodegenContext) -> HashSet<String> {
1436 ctx.proof_ir
1437 .unclassified_fns
1438 .iter()
1439 .filter_map(|uf| {
1440 let s = &uf.message;
1441 let start = s.find('\'')?;
1442 let rest = &s[start + 1..];
1443 let end = rest.find('\'')?;
1444 Some(rest[..end].to_string())
1445 })
1446 .collect()
1447}
1448
1449pub fn law_calls_unclassified_fn(
1461 law: &crate::ast::VerifyLaw,
1462 unclassified: &HashSet<String>,
1463) -> bool {
1464 if unclassified.is_empty() {
1465 return false;
1466 }
1467 expr_calls_named(&law.lhs, unclassified) || expr_calls_named(&law.rhs, unclassified)
1468}
1469
1470fn expr_calls_named(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1471 match &expr.node {
1472 Expr::FnCall(callee, args) => {
1473 let direct = expr_to_dotted_name(&callee.node)
1484 .map(|n| {
1485 let bare = n.rsplit('.').next().unwrap_or(n.as_str());
1486 names.contains(n.as_str()) || names.contains(bare)
1487 })
1488 .unwrap_or(false);
1489 direct
1490 || expr_calls_named(callee, names)
1491 || args.iter().any(|a| expr_calls_named(a, names))
1492 }
1493 Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1494 expr_calls_named(inner, names)
1495 }
1496 Expr::BinOp(_, l, r) => expr_calls_named(l, names) || expr_calls_named(r, names),
1497 Expr::Match { subject, arms } => {
1498 expr_calls_named(subject, names)
1499 || arms.iter().any(|a| expr_calls_named(&a.body, names))
1500 }
1501 Expr::Constructor(_, Some(arg)) => expr_calls_named(arg, names),
1502 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1503 items.iter().any(|i| expr_calls_named(i, names))
1504 }
1505 Expr::MapLiteral(entries) => entries
1506 .iter()
1507 .any(|(k, v)| expr_calls_named(k, names) || expr_calls_named(v, names)),
1508 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| expr_calls_named(v, names)),
1509 Expr::RecordUpdate { base, updates, .. } => {
1510 expr_calls_named(base, names) || updates.iter().any(|(_, v)| expr_calls_named(v, names))
1511 }
1512 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1513 crate::ast::StrPart::Parsed(inner) => expr_calls_named(inner, names),
1514 crate::ast::StrPart::Literal(_) => false,
1515 }),
1516 Expr::TailCall(boxed) => {
1517 let crate::ast::TailCallData { target, args, .. } = boxed.as_ref();
1518 names.contains(target) || args.iter().any(|a| expr_calls_named(a, names))
1519 }
1520 _ => false,
1521 }
1522}
1523
1524pub fn law_rhs_is_independent_of_givens(law: &crate::ast::VerifyLaw) -> bool {
1534 let given_names: HashSet<&str> = law.givens.iter().map(|g| g.name.as_str()).collect();
1535 if given_names.is_empty() {
1536 return true;
1537 }
1538 !expr_references_any_ident(&law.rhs, &given_names)
1539}
1540
1541fn expr_references_any_ident(expr: &Spanned<Expr>, names: &HashSet<&str>) -> bool {
1542 match &expr.node {
1543 Expr::Ident(name) | Expr::Resolved { name, .. } => names.contains(name.as_str()),
1544 Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1545 expr_references_any_ident(inner, names)
1546 }
1547 Expr::FnCall(callee, args) => {
1548 expr_references_any_ident(callee, names)
1549 || args.iter().any(|a| expr_references_any_ident(a, names))
1550 }
1551 Expr::BinOp(_, l, r) => {
1552 expr_references_any_ident(l, names) || expr_references_any_ident(r, names)
1553 }
1554 Expr::Match { subject, arms } => {
1555 expr_references_any_ident(subject, names)
1556 || arms
1557 .iter()
1558 .any(|a| expr_references_any_ident(&a.body, names))
1559 }
1560 Expr::Constructor(_, Some(arg)) => expr_references_any_ident(arg, names),
1561 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1562 items.iter().any(|i| expr_references_any_ident(i, names))
1563 }
1564 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
1565 expr_references_any_ident(k, names) || expr_references_any_ident(v, names)
1566 }),
1567 Expr::RecordCreate { fields, .. } => fields
1568 .iter()
1569 .any(|(_, v)| expr_references_any_ident(v, names)),
1570 Expr::RecordUpdate { base, updates, .. } => {
1571 expr_references_any_ident(base, names)
1572 || updates
1573 .iter()
1574 .any(|(_, v)| expr_references_any_ident(v, names))
1575 }
1576 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1577 crate::ast::StrPart::Parsed(inner) => expr_references_any_ident(inner, names),
1578 crate::ast::StrPart::Literal(_) => false,
1579 }),
1580 Expr::TailCall(boxed) => {
1581 let crate::ast::TailCallData { args, .. } = boxed.as_ref();
1582 args.iter().any(|a| expr_references_any_ident(a, names))
1583 }
1584 _ => false,
1585 }
1586}
1587
1588pub fn law_lhs_has_trace_projection(expr: &Spanned<Expr>) -> bool {
1609 expr_has_trace_field(expr) && expr_has_trace_api_call(expr)
1610}
1611
1612fn expr_has_trace_field(expr: &Spanned<Expr>) -> bool {
1613 match &expr.node {
1614 Expr::Attr(inner, field) => field == "trace" || expr_has_trace_field(inner),
1615 Expr::FnCall(callee, args) => {
1616 expr_has_trace_field(callee) || args.iter().any(expr_has_trace_field)
1617 }
1618 Expr::BinOp(_, l, r) => expr_has_trace_field(l) || expr_has_trace_field(r),
1619 Expr::Match { subject, arms } => {
1620 expr_has_trace_field(subject) || arms.iter().any(|a| expr_has_trace_field(&a.body))
1621 }
1622 Expr::ErrorProp(inner) => expr_has_trace_field(inner),
1623 Expr::Constructor(_, Some(arg)) => expr_has_trace_field(arg),
1624 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1625 items.iter().any(expr_has_trace_field)
1626 }
1627 _ => false,
1628 }
1629}
1630
1631const TRACE_API_METHODS: &[&str] = &["event", "group", "branch", "length", "contains", "count"];
1636
1637fn expr_has_trace_api_call(expr: &Spanned<Expr>) -> bool {
1638 match &expr.node {
1639 Expr::FnCall(callee, args) => {
1640 let direct = matches!(
1641 &callee.node,
1642 Expr::Attr(_, method) if TRACE_API_METHODS.contains(&method.as_str())
1643 );
1644 direct || expr_has_trace_api_call(callee) || args.iter().any(expr_has_trace_api_call)
1645 }
1646 Expr::Attr(inner, _) | Expr::ErrorProp(inner) => expr_has_trace_api_call(inner),
1647 Expr::BinOp(_, l, r) => expr_has_trace_api_call(l) || expr_has_trace_api_call(r),
1648 Expr::Match { subject, arms } => {
1649 expr_has_trace_api_call(subject)
1650 || arms.iter().any(|a| expr_has_trace_api_call(&a.body))
1651 }
1652 Expr::Constructor(_, Some(arg)) => expr_has_trace_api_call(arg),
1653 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1654 items.iter().any(expr_has_trace_api_call)
1655 }
1656 _ => false,
1657 }
1658}
1659
1660pub fn is_recursive_type_def(td: &TypeDef) -> bool {
1663 match td {
1664 TypeDef::Sum { name, variants, .. } => is_recursive_sum(name, variants),
1665 TypeDef::Product { name, fields, .. } => is_recursive_product(name, fields),
1666 }
1667}
1668
1669pub fn type_def_name(td: &TypeDef) -> &str {
1671 match td {
1672 TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
1673 }
1674}
1675
1676pub fn is_recursive_sum(name: &str, variants: &[TypeVariant]) -> bool {
1680 variants
1681 .iter()
1682 .any(|v| v.fields.iter().any(|f| type_ref_contains(f, name)))
1683}
1684
1685pub fn is_recursive_product(name: &str, fields: &[(String, String)]) -> bool {
1687 fields.iter().any(|(_, ty)| type_ref_contains(ty, name))
1688}
1689
1690fn type_ref_contains(annotation: &str, type_name: &str) -> bool {
1691 annotation == type_name
1694 || annotation.contains(&format!("<{}", type_name))
1695 || annotation.contains(&format!("{}>", type_name))
1696 || annotation.contains(&format!(", {}", type_name))
1697 || annotation.contains(&format!("{},", type_name))
1698}
1699
1700pub(crate) fn is_user_type(name: &str, ctx: &CodegenContext) -> bool {
1702 let check_td = |td: &TypeDef| match td {
1703 TypeDef::Sum { name: n, .. } => n == name,
1704 TypeDef::Product { name: n, .. } => n == name,
1705 };
1706 ctx.type_defs.iter().any(check_td)
1707 || ctx.modules.iter().any(|m| m.type_defs.iter().any(check_td))
1708}
1709
1710pub(crate) fn resolve_module_call<'a>(
1713 dotted_name: &'a str,
1714 ctx: &'a CodegenContext,
1715) -> Option<(&'a str, &'a str)> {
1716 let mut best: Option<&str> = None;
1717 for prefix in &ctx.module_prefixes {
1718 let dotted_prefix = format!("{}.", prefix);
1719 if dotted_name.starts_with(&dotted_prefix) && best.is_none_or(|b| prefix.len() > b.len()) {
1720 best = Some(prefix.as_str());
1721 }
1722 }
1723 best.map(|prefix| (prefix, &dotted_name[prefix.len() + 1..]))
1724}
1725
1726pub(crate) fn module_prefix_to_rust_segments(prefix: &str) -> Vec<String> {
1727 prefix.split('.').map(module_segment_to_rust).collect()
1728}
1729
1730pub(crate) fn module_prefix_to_filename(prefix: &str) -> String {
1735 prefix.replace('.', "/")
1736}
1737
1738pub(crate) struct DeclaredEffects {
1750 pub bare_namespaces: HashSet<String>,
1751 pub methods: HashSet<String>,
1752}
1753
1754impl DeclaredEffects {
1755 pub fn includes(&self, c_method: &str) -> bool {
1758 if self.methods.contains(c_method) {
1759 return true;
1760 }
1761 if let Some((ns, _)) = c_method.split_once('.') {
1762 return self.bare_namespaces.contains(ns);
1763 }
1764 false
1765 }
1766}
1767
1768pub(crate) fn collect_declared_effects(ctx: &CodegenContext) -> DeclaredEffects {
1772 let mut bare_namespaces: HashSet<String> = HashSet::new();
1773 let mut methods: HashSet<String> = HashSet::new();
1774 let mut record = |effect: &str| {
1775 if effect.contains('.') {
1776 methods.insert(effect.to_string());
1777 } else {
1778 bare_namespaces.insert(effect.to_string());
1779 }
1780 };
1781 for item in &ctx.items {
1782 if let TopLevel::FnDef(fd) = item {
1783 for eff in &fd.effects {
1784 record(&eff.node);
1785 }
1786 }
1787 }
1788 for module in &ctx.modules {
1789 for fd in &module.fn_defs {
1790 for eff in &fd.effects {
1791 record(&eff.node);
1792 }
1793 }
1794 }
1795 DeclaredEffects {
1796 bare_namespaces,
1797 methods,
1798 }
1799}
1800
1801pub fn entry_basename(ctx: &CodegenContext) -> String {
1809 ctx.items
1810 .iter()
1811 .find_map(|item| match item {
1812 TopLevel::Module(m) => Some(m.name.clone()),
1813 _ => None,
1814 })
1815 .unwrap_or_else(|| {
1816 let mut chars = ctx.project_name.chars();
1817 match chars.next() {
1818 None => String::new(),
1819 Some(c) => c.to_uppercase().chain(chars).collect(),
1820 }
1821 })
1822}
1823
1824pub(crate) fn module_prefix_to_rust_path(prefix: &str) -> String {
1834 format!(
1835 "crate::aver_generated::{}",
1836 module_prefix_to_rust_segments(prefix).join("::")
1837 )
1838}
1839
1840fn module_segment_to_rust(segment: &str) -> String {
1841 let chars = segment.chars().collect::<Vec<_>>();
1842 let mut out = String::new();
1843
1844 for (idx, ch) in chars.iter().enumerate() {
1845 if ch.is_ascii_alphanumeric() {
1846 if ch.is_ascii_uppercase() {
1847 let prev_is_lower_or_digit = idx > 0
1848 && (chars[idx - 1].is_ascii_lowercase() || chars[idx - 1].is_ascii_digit());
1849 let next_is_lower = chars
1850 .get(idx + 1)
1851 .is_some_and(|next| next.is_ascii_lowercase());
1852 if idx > 0 && (prev_is_lower_or_digit || next_is_lower) && !out.ends_with('_') {
1853 out.push('_');
1854 }
1855 out.push(ch.to_ascii_lowercase());
1856 } else {
1857 out.push(ch.to_ascii_lowercase());
1858 }
1859 } else if !out.ends_with('_') {
1860 out.push('_');
1861 }
1862 }
1863
1864 let trimmed = out.trim_matches('_');
1865 let mut normalized = if trimmed.is_empty() {
1866 "module".to_string()
1867 } else {
1868 trimmed.to_string()
1869 };
1870
1871 if matches!(
1872 normalized.as_str(),
1873 "as" | "break"
1874 | "const"
1875 | "continue"
1876 | "crate"
1877 | "else"
1878 | "enum"
1879 | "extern"
1880 | "false"
1881 | "fn"
1882 | "for"
1883 | "if"
1884 | "impl"
1885 | "in"
1886 | "let"
1887 | "loop"
1888 | "match"
1889 | "mod"
1890 | "move"
1891 | "mut"
1892 | "pub"
1893 | "ref"
1894 | "return"
1895 | "self"
1896 | "Self"
1897 | "static"
1898 | "struct"
1899 | "super"
1900 | "trait"
1901 | "true"
1902 | "type"
1903 | "unsafe"
1904 | "use"
1905 | "where"
1906 | "while"
1907 ) {
1908 normalized.push_str("_mod");
1909 }
1910
1911 normalized
1912}
1913
1914pub(crate) fn split_type_params(s: &str, delim: char) -> Vec<String> {
1919 let mut parts = Vec::new();
1920 let mut depth = 0usize;
1921 let mut current = String::new();
1922 for ch in s.chars() {
1923 match ch {
1924 '<' | '(' => {
1925 depth += 1;
1926 current.push(ch);
1927 }
1928 '>' | ')' => {
1929 depth = depth.saturating_sub(1);
1930 current.push(ch);
1931 }
1932 _ if ch == delim && depth == 0 => {
1933 parts.push(current.trim().to_string());
1934 current.clear();
1935 }
1936 _ => current.push(ch),
1937 }
1938 }
1939 let rest = current.trim().to_string();
1940 if !rest.is_empty() {
1941 parts.push(rest);
1942 }
1943 parts
1944}
1945
1946pub(crate) fn escape_string_literal_ext(s: &str, unicode_escapes: bool) -> String {
1953 let mut out = String::with_capacity(s.len());
1954 for ch in s.chars() {
1955 match ch {
1956 '\\' => out.push_str("\\\\"),
1957 '"' => out.push_str("\\\""),
1958 '\n' => out.push_str("\\n"),
1959 '\r' => out.push_str("\\r"),
1960 '\t' => out.push_str("\\t"),
1961 '\0' => out.push_str("\\0"),
1962 c if c.is_control() => {
1963 if unicode_escapes {
1964 out.push_str(&format!("\\U{{{:06x}}}", c as u32));
1966 } else {
1967 out.push_str(&format!("\\x{:02x}", c as u32));
1968 }
1969 }
1970 c => out.push(c),
1971 }
1972 }
1973 out
1974}
1975
1976pub(crate) fn escape_string_literal(s: &str) -> String {
1978 escape_string_literal_ext(s, false)
1979}
1980
1981pub(crate) fn escape_string_literal_unicode(s: &str) -> String {
1983 escape_string_literal_ext(s, true)
1984}
1985
1986pub(crate) fn parse_type_annotation(ann: &str) -> Type {
1990 crate::types::parse_type_str(ann)
1991}
1992
1993pub(crate) fn is_set_type(ty: &Type) -> bool {
1999 matches!(ty, Type::Map(_, v) if matches!(v.as_ref(), Type::Unit))
2000}
2001
2002pub(crate) fn is_set_annotation(ann: &str) -> bool {
2004 is_set_type(&parse_type_annotation(ann))
2005}
2006
2007pub(crate) fn is_unit_expr_resolved(expr: &crate::ir::hir::ResolvedExpr) -> bool {
2010 matches!(
2011 expr,
2012 crate::ir::hir::ResolvedExpr::Literal(crate::ast::Literal::Unit)
2013 )
2014}
2015
2016pub(crate) fn escape_reserved_word(name: &str, reserved: &[&str], suffix: &str) -> String {
2021 if reserved.contains(&name) {
2022 format!("{}{}", name, suffix)
2023 } else {
2024 name.to_string()
2025 }
2026}
2027
2028pub(crate) fn escape_reserved_word_prefix(name: &str, reserved: &[&str], prefix: &str) -> String {
2031 if reserved.contains(&name) {
2032 format!("{}{}", prefix, name)
2033 } else {
2034 name.to_string()
2035 }
2036}
2037
2038pub(crate) fn to_lower_first(s: &str) -> String {
2042 let mut chars = s.chars();
2043 match chars.next() {
2044 None => String::new(),
2045 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
2046 }
2047}
2048
2049pub(crate) fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
2052 crate::ir::expr_to_dotted_name(expr)
2053}
2054
2055#[derive(Debug, Clone)]
2069pub(crate) enum OracleInjectionMode<'a> {
2070 LemmaBinding,
2071 LemmaBindingProjected,
2081 #[allow(dead_code)]
2082 SampleValue,
2083 SampleCaseBinding(&'a [(String, crate::ast::Spanned<Expr>)]),
2084}
2085
2086pub(crate) fn rewrite_effectful_calls_in_law<'fd, F>(
2095 expr: &crate::ast::Spanned<Expr>,
2096 law: &crate::ast::VerifyLaw,
2097 find_fn_def: F,
2098 mode: OracleInjectionMode,
2099) -> crate::ast::Spanned<Expr>
2100where
2101 F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2102{
2103 use crate::ast::{Spanned, VerifyGivenDomain};
2104
2105 let injection_by_effect: std::collections::HashMap<String, Spanned<Expr>> = law
2106 .givens
2107 .iter()
2108 .filter_map(|g| {
2109 let arg_expr = match &mode {
2110 OracleInjectionMode::LemmaBinding => {
2111 Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2112 }
2113 OracleInjectionMode::LemmaBindingProjected => {
2114 Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2122 }
2123 OracleInjectionMode::SampleValue => match &g.domain {
2124 VerifyGivenDomain::Explicit(vals) => vals.first().cloned()?,
2125 _ => return None,
2126 },
2127 OracleInjectionMode::SampleCaseBinding(case_bindings) => case_bindings
2128 .iter()
2129 .find(|(name, _)| name == &g.name)
2130 .map(|(_, v)| v.clone())?,
2131 };
2132 Some((g.type_name.clone(), arg_expr))
2133 })
2134 .collect();
2135 let rewritten = rewrite_effectful_call(expr, &injection_by_effect, find_fn_def);
2136
2137 if matches!(mode, OracleInjectionMode::LemmaBindingProjected) {
2146 let oracle_names: std::collections::HashSet<String> = law
2156 .givens
2157 .iter()
2158 .filter(|g| crate::types::checker::oracle_subtypes::has_bounded_subtype(&g.type_name))
2159 .map(|g| g.name.clone())
2160 .collect();
2161 if !oracle_names.is_empty() {
2162 return project_oracle_direct_calls(&rewritten, &oracle_names);
2163 }
2164 }
2165 rewritten
2166}
2167
2168fn project_oracle_direct_calls(
2181 expr: &crate::ast::Spanned<Expr>,
2182 oracle_names: &std::collections::HashSet<String>,
2183) -> crate::ast::Spanned<Expr> {
2184 use crate::ast::Spanned;
2185 let line = expr.line;
2186 let project_ident = |name: &str, line: usize| -> Spanned<Expr> {
2187 Spanned::new(
2188 Expr::Attr(
2189 Box::new(Spanned::new(Expr::Ident(name.to_string()), line)),
2190 "val".to_string(),
2191 ),
2192 line,
2193 )
2194 };
2195 let new_node = match &expr.node {
2196 Expr::Ident(name) if oracle_names.contains(name) => {
2200 return project_ident(name, line);
2201 }
2202 Expr::FnCall(callee, args) => {
2203 let new_args: Vec<Spanned<Expr>> = args
2204 .iter()
2205 .map(|a| project_oracle_direct_calls(a, oracle_names))
2206 .collect();
2207 let new_callee = if let Expr::Ident(name) = &callee.node
2209 && oracle_names.contains(name)
2210 {
2211 project_ident(name, callee.line)
2212 } else {
2213 project_oracle_direct_calls(callee, oracle_names)
2214 };
2215 Expr::FnCall(Box::new(new_callee), new_args)
2216 }
2217 Expr::Constructor(name, Some(arg)) => Expr::Constructor(
2218 name.clone(),
2219 Some(Box::new(project_oracle_direct_calls(arg, oracle_names))),
2220 ),
2221 Expr::Attr(obj, field) => Expr::Attr(
2222 Box::new(project_oracle_direct_calls(obj, oracle_names)),
2223 field.clone(),
2224 ),
2225 Expr::BinOp(op, l, r) => Expr::BinOp(
2226 *op,
2227 Box::new(project_oracle_direct_calls(l, oracle_names)),
2228 Box::new(project_oracle_direct_calls(r, oracle_names)),
2229 ),
2230 other => other.clone(),
2231 };
2232 Spanned::new(new_node, line)
2233}
2234
2235fn rewrite_effectful_call<'fd, F>(
2236 expr: &crate::ast::Spanned<Expr>,
2237 injection_by_effect: &std::collections::HashMap<String, crate::ast::Spanned<Expr>>,
2238 find_fn_def: F,
2239) -> crate::ast::Spanned<Expr>
2240where
2241 F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2242{
2243 use crate::ast::Spanned;
2244 use crate::types::checker::effect_classification::{EffectDimension, classify};
2245
2246 match &expr.node {
2247 Expr::FnCall(callee, args) => {
2248 let rewritten_args: Vec<Spanned<Expr>> = args
2249 .iter()
2250 .map(|a| rewrite_effectful_call(a, injection_by_effect, find_fn_def))
2251 .collect();
2252 let rewritten_callee = Box::new(rewrite_effectful_call(
2253 callee,
2254 injection_by_effect,
2255 find_fn_def,
2256 ));
2257
2258 let callee_name = match &callee.node {
2259 Expr::Ident(name) => Some(name.clone()),
2260 Expr::Resolved { name, .. } => Some(name.clone()),
2261 _ => None,
2262 };
2263
2264 if let Some(name) = callee_name
2265 && let Some(fd) = find_fn_def(&name)
2266 && !fd.effects.is_empty()
2267 && fd
2268 .effects
2269 .iter()
2270 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
2271 {
2272 let mut injected: Vec<Spanned<Expr>> = Vec::new();
2273 let needs_path = fd.effects.iter().any(|e| {
2274 matches!(
2275 classify(&e.node).map(|c| c.dimension),
2276 Some(EffectDimension::Generative | EffectDimension::GenerativeOutput)
2277 )
2278 });
2279 if needs_path {
2280 injected.push(Spanned::new(
2281 Expr::Attr(
2285 Box::new(Spanned::new(
2286 Expr::Ident("BranchPath".to_string()),
2287 expr.line,
2288 )),
2289 "Root".to_string(),
2290 ),
2291 expr.line,
2292 ));
2293 }
2294 let mut seen = std::collections::HashSet::new();
2295 for e in &fd.effects {
2296 if !seen.insert(e.node.clone()) {
2297 continue;
2298 }
2299 let Some(c) = classify(&e.node) else { continue };
2300 if matches!(c.dimension, EffectDimension::Output) {
2301 continue;
2302 }
2303 if let Some(inj) = injection_by_effect.get(&e.node) {
2304 injected.push(inj.clone());
2305 }
2306 }
2307 injected.extend(rewritten_args);
2308 return Spanned::new(Expr::FnCall(rewritten_callee, injected), expr.line);
2309 }
2310
2311 Spanned::new(Expr::FnCall(rewritten_callee, rewritten_args), expr.line)
2312 }
2313 Expr::BinOp(op, l, r) => Spanned::new(
2314 Expr::BinOp(
2315 *op,
2316 Box::new(rewrite_effectful_call(l, injection_by_effect, find_fn_def)),
2317 Box::new(rewrite_effectful_call(r, injection_by_effect, find_fn_def)),
2318 ),
2319 expr.line,
2320 ),
2321 Expr::Tuple(items) => Spanned::new(
2322 Expr::Tuple(
2323 items
2324 .iter()
2325 .map(|i| rewrite_effectful_call(i, injection_by_effect, find_fn_def))
2326 .collect(),
2327 ),
2328 expr.line,
2329 ),
2330 _ => expr.clone(),
2331 }
2332}
2333
2334pub(crate) fn verify_reachable_fn_names(items: &[TopLevel]) -> HashSet<String> {
2344 let mut reachable: HashSet<String> = HashSet::new();
2345 for item in items {
2346 if let TopLevel::Verify(vb) = item {
2347 collect_verify_block_refs(vb, &mut reachable);
2348 }
2349 }
2350 loop {
2352 let mut changed = false;
2353 for item in items {
2354 if let TopLevel::FnDef(fd) = item
2355 && reachable.contains(&fd.name)
2356 {
2357 let mut called = HashSet::new();
2358 collect_called_idents_in_body(&fd.body, &mut called);
2359 for name in called {
2360 if reachable.insert(name) {
2361 changed = true;
2362 }
2363 }
2364 }
2365 }
2366 if !changed {
2367 break;
2368 }
2369 }
2370 reachable
2371}
2372
2373fn collect_verify_block_refs(vb: &VerifyBlock, out: &mut HashSet<String>) {
2374 out.insert(vb.fn_name.clone());
2375 for (lhs, rhs) in &vb.cases {
2376 collect_called_idents(lhs, out);
2377 collect_called_idents(rhs, out);
2378 }
2379 if let VerifyKind::Law(law) = &vb.kind {
2380 collect_called_idents(&law.lhs, out);
2381 collect_called_idents(&law.rhs, out);
2382 if let Some(when) = &law.when {
2383 collect_called_idents(when, out);
2384 }
2385 for given in &law.givens {
2386 if let VerifyGivenDomain::Explicit(values) = &given.domain {
2387 for v in values {
2388 collect_called_idents(v, out);
2389 }
2390 }
2391 }
2392 }
2393 for given in &vb.cases_givens {
2394 if let VerifyGivenDomain::Explicit(values) = &given.domain {
2395 for v in values {
2396 collect_called_idents(v, out);
2397 }
2398 }
2399 }
2400}
2401
2402fn collect_called_idents_in_body(body: &FnBody, out: &mut HashSet<String>) {
2403 for stmt in body.stmts() {
2404 match stmt {
2405 Stmt::Binding(_, _, e) | Stmt::Expr(e) => collect_called_idents(e, out),
2406 }
2407 }
2408}
2409
2410fn collect_called_idents(expr: &Spanned<Expr>, out: &mut HashSet<String>) {
2411 match &expr.node {
2412 Expr::FnCall(callee, args) => {
2413 if let Expr::Ident(name) | Expr::Resolved { name, .. } = &callee.node {
2414 out.insert(name.clone());
2415 } else {
2416 collect_called_idents(callee, out);
2417 }
2418 for a in args {
2419 collect_called_idents(a, out);
2420 }
2421 }
2422 Expr::TailCall(boxed) => {
2423 let TailCallData { target, args, .. } = boxed.as_ref();
2424 out.insert(target.clone());
2425 for a in args {
2426 collect_called_idents(a, out);
2427 }
2428 }
2429 Expr::Ident(name) | Expr::Resolved { name, .. } => {
2430 out.insert(name.clone());
2431 }
2432 Expr::BinOp(_, l, r) => {
2433 collect_called_idents(l, out);
2434 collect_called_idents(r, out);
2435 }
2436 Expr::Neg(inner) => collect_called_idents(inner, out),
2437 Expr::Match { subject, arms, .. } => {
2438 collect_called_idents(subject, out);
2439 for arm in arms {
2440 collect_called_idents(&arm.body, out);
2441 }
2442 }
2443 Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
2444 collect_called_idents(inner, out);
2445 }
2446 Expr::Constructor(_, Some(inner)) => {
2447 collect_called_idents(inner, out);
2448 }
2449 Expr::InterpolatedStr(parts) => {
2450 for part in parts {
2451 if let StrPart::Parsed(inner) = part {
2452 collect_called_idents(inner, out);
2453 }
2454 }
2455 }
2456 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
2457 for i in items {
2458 collect_called_idents(i, out);
2459 }
2460 }
2461 Expr::MapLiteral(entries) => {
2462 for (k, v) in entries {
2463 collect_called_idents(k, out);
2464 collect_called_idents(v, out);
2465 }
2466 }
2467 Expr::RecordCreate { fields, .. } => {
2468 for (_, v) in fields {
2469 collect_called_idents(v, out);
2470 }
2471 }
2472 Expr::RecordUpdate { base, updates, .. } => {
2473 collect_called_idents(base, out);
2474 for (_, v) in updates {
2475 collect_called_idents(v, out);
2476 }
2477 }
2478 Expr::Literal(_) | Expr::Constructor(_, None) => {}
2479 }
2480}
2481
2482pub(crate) struct PerScopeSections {
2486 pub by_scope: std::collections::HashMap<String, Vec<String>>,
2487}
2488
2489impl PerScopeSections {
2490 pub(crate) fn take(&mut self, scope: &str) -> Vec<String> {
2491 self.by_scope.remove(scope).unwrap_or_default()
2492 }
2493}
2494
2495pub(crate) fn route_pure_components_per_scope<F, G>(
2504 ctx: &CodegenContext,
2505 is_pure: F,
2506 mut emit: G,
2507) -> PerScopeSections
2508where
2509 F: Fn(&FnDef) -> bool,
2510 G: FnMut(&[&FnDef], &str) -> Vec<String>,
2511{
2512 let mut by_scope: std::collections::HashMap<String, Vec<String>> =
2513 std::collections::HashMap::new();
2514
2515 let mut process =
2516 |fns: Vec<&FnDef>,
2517 scope: String,
2518 by_scope: &mut std::collections::HashMap<String, Vec<String>>| {
2519 let comps = crate::call_graph::ordered_fn_components(&fns, &ctx.module_prefixes);
2520 let bucket = by_scope.entry(scope.clone()).or_default();
2521 for comp in comps {
2522 bucket.extend(emit(&comp, scope.as_str()));
2523 }
2524 };
2525
2526 for module in &ctx.modules {
2527 let pure: Vec<&FnDef> = module.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2528 process(pure, module.prefix.clone(), &mut by_scope);
2529 }
2530 let entry_pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2531 process(entry_pure, String::new(), &mut by_scope);
2532
2533 PerScopeSections { by_scope }
2534}
2535
2536#[cfg(test)]
2537mod tests {
2538 use super::*;
2539 use crate::ast::{Literal, VerifyGiven, VerifyGivenDomain, VerifyLaw};
2540
2541 fn sb(node: Expr) -> Spanned<Expr> {
2542 Spanned::new(node, 1)
2543 }
2544
2545 fn bsb(node: Expr) -> Box<Spanned<Expr>> {
2546 Box::new(sb(node))
2547 }
2548
2549 fn law_with(lhs: Spanned<Expr>, rhs: Spanned<Expr>) -> VerifyLaw {
2550 VerifyLaw {
2551 name: "test".to_string(),
2552 givens: vec![VerifyGiven {
2553 name: "xs".to_string(),
2554 type_name: "List<String>".to_string(),
2555 domain: VerifyGivenDomain::Explicit(vec![sb(Expr::List(vec![]))]),
2556 }],
2557 when: None,
2558 lhs,
2559 rhs,
2560 sample_guards: Vec::new(),
2561 }
2562 }
2563
2564 #[test]
2565 fn law_calls_unclassified_fn_detects_dotted_callee() {
2566 let lhs = sb(Expr::FnCall(
2577 bsb(Expr::Attr(
2578 bsb(Expr::Ident("Dep".to_string())),
2579 "toSorted".to_string(),
2580 )),
2581 vec![sb(Expr::Ident("xs".to_string()))],
2582 ));
2583 let rhs = sb(Expr::Ident("xs".to_string()));
2584 let law = law_with(lhs, rhs);
2585
2586 let mut canonical = HashSet::new();
2589 canonical.insert("Dep.toSorted".to_string());
2590 assert!(law_calls_unclassified_fn(&law, &canonical));
2591
2592 let mut bare_only = HashSet::new();
2595 bare_only.insert("toSorted".to_string());
2596 assert!(
2597 law_calls_unclassified_fn(&law, &bare_only),
2598 "bare unclassified name must catch a dotted callsite via suffix match"
2599 );
2600
2601 let mut unrelated = HashSet::new();
2604 unrelated.insert("somethingElse".to_string());
2605 assert!(!law_calls_unclassified_fn(&law, &unrelated));
2606 }
2607
2608 #[test]
2609 fn law_lhs_has_trace_projection_skips_user_record_field() {
2610 let user_field_lhs = sb(Expr::Attr(
2618 bsb(Expr::Ident("log".to_string())),
2619 "trace".to_string(),
2620 ));
2621 assert!(
2622 !law_lhs_has_trace_projection(&user_field_lhs),
2623 "bare user-record `.trace` field must not trigger the gate"
2624 );
2625
2626 let runtime_trace_lhs = sb(Expr::FnCall(
2628 bsb(Expr::Attr(
2629 bsb(Expr::Attr(
2630 bsb(Expr::FnCall(bsb(Expr::Ident("fn".to_string())), vec![])),
2631 "trace".to_string(),
2632 )),
2633 "event".to_string(),
2634 )),
2635 vec![sb(Expr::Literal(Literal::Int(0)))],
2636 ));
2637 assert!(
2638 law_lhs_has_trace_projection(&runtime_trace_lhs),
2639 "Oracle `.trace.event(0)` projection must trigger the gate"
2640 );
2641 }
2642
2643 #[test]
2644 fn resolve_refined_type_disambiguates_cross_module_same_bare_name() {
2645 use crate::ast::{TypeDef, TypeVariant};
2654 use crate::codegen::ModuleInfo;
2655 use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
2656 use std::collections::HashMap;
2657 let _ = TypeVariant {
2658 name: String::new(),
2659 fields: Vec::new(),
2660 }; let make_module = |prefix: &str| ModuleInfo {
2663 prefix: prefix.to_string(),
2664 depends: Vec::new(),
2665 type_defs: vec![TypeDef::Product {
2666 name: "Natural".to_string(),
2667 fields: vec![("value".to_string(), "Int".to_string())],
2668 line: 1,
2669 }],
2670 fn_defs: Vec::new(),
2671 analysis: None,
2672 };
2673 let modules = vec![make_module("A"), make_module("B")];
2674
2675 let make_decl = |predicate_param: &str, witness: i64| RefinedTypeDecl {
2676 name: "Natural".to_string(),
2677 carrier_type: "Int".to_string(),
2678 carrier_field: "value".to_string(),
2679 predicate_param: predicate_param.to_string(),
2680 invariant: Predicate {
2681 free_vars: vec![(
2682 predicate_param.to_string(),
2683 QuantifierType::Plain("Int".to_string()),
2684 )],
2685 expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
2686 Literal::Bool(true),
2687 )),
2688 },
2689 witness: Some(witness.to_string()),
2690 };
2691 let symbols = crate::ir::SymbolTable::build(&[], &modules);
2692 let a_id = symbols
2693 .type_id_of(&crate::ir::TypeKey::in_module("A", "Natural"))
2694 .expect("A.Natural TypeId");
2695 let b_id = symbols
2696 .type_id_of(&crate::ir::TypeKey::in_module("B", "Natural"))
2697 .expect("B.Natural TypeId");
2698
2699 let mut refined_types: HashMap<crate::ir::TypeId, RefinedTypeDecl> = HashMap::new();
2700 refined_types.insert(a_id, make_decl("a", 0));
2701 refined_types.insert(b_id, make_decl("b", 10));
2702
2703 let a = resolve_refined_type_in(&refined_types, &symbols, &modules, "A.Natural")
2705 .expect("A.Natural canonical lookup");
2706 assert_eq!(a.predicate_param, "a");
2707 assert_eq!(a.witness.as_deref(), Some("0"));
2708 let b = resolve_refined_type_in(&refined_types, &symbols, &modules, "B.Natural")
2709 .expect("B.Natural canonical lookup");
2710 assert_eq!(b.predicate_param, "b");
2711 assert_eq!(b.witness.as_deref(), Some("10"));
2712
2713 let bare = resolve_refined_type_in(&refined_types, &symbols, &modules, "Natural")
2717 .expect("bare Natural resolves via module walk");
2718 assert!(
2719 bare.predicate_param == "a" || bare.predicate_param == "b",
2720 "bare Natural must resolve to one of the canonical decls"
2721 );
2722
2723 assert!(resolve_refined_type_in(&refined_types, &symbols, &modules, "Unrelated").is_none());
2726 }
2727
2728 #[test]
2729 fn find_refined_type_scoped_prefers_current_module_over_entry_collision() {
2730 use crate::ast::{TopLevel, TypeDef};
2738 use crate::codegen::{CodegenContext, ModuleInfo};
2739 use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
2740 use std::collections::{HashMap, HashSet};
2741
2742 let entry_natural = TypeDef::Product {
2743 name: "Natural".to_string(),
2744 fields: vec![("value".to_string(), "Int".to_string())],
2745 line: 1,
2746 };
2747 let module = ModuleInfo {
2748 prefix: "Mod".to_string(),
2749 depends: Vec::new(),
2750 type_defs: vec![TypeDef::Product {
2751 name: "Natural".to_string(),
2752 fields: vec![("value".to_string(), "Int".to_string())],
2753 line: 1,
2754 }],
2755 fn_defs: Vec::new(),
2756 analysis: None,
2757 };
2758
2759 let make_decl = |param: &str, witness: &str| RefinedTypeDecl {
2760 name: "Natural".to_string(),
2761 carrier_type: "Int".to_string(),
2762 carrier_field: "value".to_string(),
2763 predicate_param: param.to_string(),
2764 invariant: Predicate {
2765 free_vars: vec![(param.to_string(), QuantifierType::Plain("Int".to_string()))],
2766 expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
2767 Literal::Bool(true),
2768 )),
2769 },
2770 witness: Some(witness.to_string()),
2771 };
2772
2773 let items = vec![TopLevel::TypeDef(entry_natural)];
2774 let modules = vec![module];
2775 let symbol_table = crate::ir::SymbolTable::build(&items, &modules);
2776 let entry_id = symbol_table
2777 .type_id_of(&crate::ir::TypeKey::entry("Natural"))
2778 .expect("entry Natural id");
2779 let mod_id = symbol_table
2780 .type_id_of(&crate::ir::TypeKey::in_module("Mod", "Natural"))
2781 .expect("Mod.Natural id");
2782
2783 let mut ctx = CodegenContext {
2784 items,
2785 memo_fns: HashSet::new(),
2786 memo_safe_types: HashSet::new(),
2787 type_defs: Vec::new(),
2788 fn_defs: Vec::new(),
2789 project_name: "scope-test".to_string(),
2790 modules,
2791 module_prefixes: HashSet::new(),
2792 #[cfg(feature = "runtime")]
2793 policy: None,
2794 emit_replay_runtime: false,
2795 runtime_policy_from_env: false,
2796 guest_entry: None,
2797 emit_self_host_support: false,
2798 extra_fn_defs: Vec::new(),
2799 mutual_tco_members: HashSet::new(),
2800 recursive_fns: HashSet::new(),
2801 buffer_build_sinks: HashMap::new(),
2802 buffer_fusion_sites: Vec::new(),
2803 synthesized_buffered_fns: Vec::new(),
2804 proof_ir: crate::ir::ProofIR::default(),
2805 symbol_table,
2806 resolved_fn_defs: Vec::new(),
2807 resolved_module_fn_defs: Vec::new(),
2808 current_module_scope: std::cell::RefCell::new(None),
2809 resolved_program: crate::codegen::program_view::ResolvedProgramView::default(),
2810 };
2811 ctx.proof_ir
2812 .refined_types
2813 .insert(entry_id, make_decl("entry_n", "0"));
2814 ctx.proof_ir
2815 .refined_types
2816 .insert(mod_id, make_decl("mod_n", "10"));
2817
2818 let from_module = find_refined_type_scoped(&ctx, "Natural", Some("Mod"))
2819 .expect("Mod-scoped Natural lookup");
2820 assert_eq!(
2821 from_module.predicate_param, "mod_n",
2822 "scope=Some(\"Mod\") + bare `Natural` must resolve to Mod.Natural, \
2823 not entry's bare-keyed slot"
2824 );
2825
2826 let from_entry =
2828 find_refined_type_scoped(&ctx, "Natural", None).expect("entry Natural lookup");
2829 assert_eq!(from_entry.predicate_param, "entry_n");
2830 }
2831}