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 if let Some(shape) = inputs.program_shape
94 && let Some(info) = refinement_info_from_shape(shape, inputs, type_name, scope_filter)
95 {
96 return Some(info);
97 }
98 refinement_info_for_walk_legacy(type_name, inputs, scope_filter)
99}
100
101fn refinement_info_from_shape<'a>(
108 shape: &crate::analysis::shape::ProgramShape,
109 inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
110 type_name: &str,
111 scope_filter: Option<Option<&str>>,
112) -> Option<RefinementInfo<'a>> {
113 use crate::analysis::shape::ModulePattern;
114
115 let pattern_match = shape.patterns.iter().find_map(|p| {
116 let ModulePattern::RefinementSmartConstructor {
117 scope,
118 type_name: ptn,
119 constructor_fn,
120 ..
121 } = p
122 else {
123 return None;
124 };
125 if ptn != type_name {
126 return None;
127 }
128 let scope_ok = match (scope_filter, scope.as_deref()) {
129 (None, _) => true,
130 (Some(None), None) => true,
131 (Some(None), Some(_)) => false,
132 (Some(Some(want)), Some(have)) => want == have,
133 (Some(Some(_)), None) => false,
134 };
135 if !scope_ok {
136 return None;
137 }
138 Some((scope.clone(), constructor_fn.clone()))
139 })?;
140
141 let (scope, constructor_fn) = pattern_match;
142
143 let (carrier_field, carrier_type) = match scope.as_deref() {
144 None => inputs.entry_items.iter().find_map(|i| match i {
145 TopLevel::TypeDef(TypeDef::Product { name, fields, .. })
146 if name == type_name && fields.len() == 1 =>
147 {
148 let (fname, ftype) = &fields[0];
149 Some((fname.as_str(), ftype.as_str()))
150 }
151 _ => None,
152 }),
153 Some(prefix) => inputs
154 .dep_modules
155 .iter()
156 .find(|m| m.prefix == prefix)
157 .and_then(|m| {
158 m.type_defs.iter().find_map(|td| match td {
159 TypeDef::Product { name, fields, .. }
160 if name == type_name && fields.len() == 1 =>
161 {
162 let (fname, ftype) = &fields[0];
163 Some((fname.as_str(), ftype.as_str()))
164 }
165 _ => None,
166 })
167 }),
168 }?;
169
170 let constructor_fd: &'a FnDef = match scope.as_deref() {
171 None => inputs.entry_items.iter().find_map(|i| match i {
172 TopLevel::FnDef(fd) if fd.name == constructor_fn => Some(fd),
173 _ => None,
174 }),
175 Some(prefix) => inputs
176 .dep_modules
177 .iter()
178 .find(|m| m.prefix == prefix)
179 .and_then(|m| m.fn_defs.iter().find(|fd| fd.name == constructor_fn)),
180 }?;
181
182 let (param_name, _) = constructor_fd.params.first()?;
183 let stmts = constructor_fd.body.stmts();
184 let Stmt::Expr(body_expr) = stmts.first()? else {
185 return None;
186 };
187 let Expr::Match { subject, .. } = &body_expr.node else {
188 return None;
189 };
190
191 Some(RefinementInfo {
192 carrier_type,
193 carrier_field,
194 param_name,
195 predicate: subject,
196 })
197}
198
199fn refinement_info_for_walk_legacy<'a>(
200 type_name: &str,
201 inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
202 scope_filter: Option<Option<&str>>,
203) -> Option<RefinementInfo<'a>> {
204 let entry_only = matches!(scope_filter, Some(None));
205 let module_only_prefix = match scope_filter {
206 Some(Some(p)) => Some(p),
207 _ => None,
208 };
209 let allow_entry = scope_filter.is_none() || entry_only;
210 let entry_typedefs: Vec<&'a TypeDef> = if allow_entry {
211 inputs
212 .entry_items
213 .iter()
214 .filter_map(|item| match item {
215 TopLevel::TypeDef(td) => Some(td),
216 _ => None,
217 })
218 .collect()
219 } else {
220 Vec::new()
221 };
222 let module_typedefs: Vec<&'a TypeDef> = inputs
223 .dep_modules
224 .iter()
225 .filter(|m| match (scope_filter, module_only_prefix) {
226 (None, _) => true,
227 (Some(None), _) => false,
228 (Some(Some(_)), Some(p)) => m.prefix == p,
229 _ => false,
230 })
231 .flat_map(|m| m.type_defs.iter())
232 .collect();
233 let (carrier_field, carrier_type) = entry_typedefs
234 .into_iter()
235 .chain(module_typedefs)
236 .find_map(|td| match td {
237 TypeDef::Product { name, fields, .. } if name == type_name && fields.len() == 1 => {
238 let (fname, ftype) = &fields[0];
239 Some((fname.as_str(), ftype.as_str()))
240 }
241 _ => None,
242 })?;
243
244 let entry_fns: Vec<&'a FnDef> = if allow_entry {
245 inputs
246 .entry_items
247 .iter()
248 .filter_map(|item| match item {
249 TopLevel::FnDef(fd) => Some(fd),
250 _ => None,
251 })
252 .collect()
253 } else {
254 Vec::new()
255 };
256 let module_fns: Vec<&'a FnDef> = inputs
257 .dep_modules
258 .iter()
259 .filter(|m| match (scope_filter, module_only_prefix) {
260 (None, _) => true,
261 (Some(None), _) => false,
262 (Some(Some(_)), Some(p)) => m.prefix == p,
263 _ => false,
264 })
265 .flat_map(|m| m.fn_defs.iter())
266 .collect();
267 for fd in entry_fns.into_iter().chain(module_fns) {
268 if !fd.return_type.starts_with("Result<") {
269 continue;
270 }
271 if !fd.return_type[7..].starts_with(type_name) {
272 continue;
273 }
274 if fd.params.len() != 1 {
275 continue;
276 }
277 let (param_name, _) = &fd.params[0];
278 let stmts = fd.body.stmts();
279 if stmts.len() != 1 {
280 continue;
281 }
282 let Stmt::Expr(body_expr) = &stmts[0] else {
283 continue;
284 };
285 let Expr::Match { subject, arms } = &body_expr.node else {
286 continue;
287 };
288 if !is_bool_ok_err_match(arms, type_name, carrier_field, param_name) {
289 continue;
290 }
291 return Some(RefinementInfo {
292 carrier_type,
293 carrier_field,
294 param_name,
295 predicate: subject,
296 });
297 }
298 None
299}
300
301pub fn is_refinement_bool_ok_err_match(
307 arms: &[MatchArm],
308 type_name: &str,
309 carrier_field: &str,
310 param_name: &str,
311) -> bool {
312 is_bool_ok_err_match(arms, type_name, carrier_field, param_name)
313}
314
315fn is_bool_ok_err_match(
316 arms: &[MatchArm],
317 type_name: &str,
318 carrier_field: &str,
319 param_name: &str,
320) -> bool {
321 if arms.len() != 2 {
322 return false;
323 }
324 let mut true_ok = false;
325 let mut false_err = false;
326 for arm in arms {
327 match &arm.pattern {
328 Pattern::Literal(Literal::Bool(true)) => {
329 if is_ok_constructor_with_identity(&arm.body, type_name, carrier_field, param_name)
330 {
331 true_ok = true;
332 }
333 }
334 Pattern::Literal(Literal::Bool(false)) => {
335 if is_err_constructor(&arm.body) {
336 false_err = true;
337 }
338 }
339 _ => return false,
340 }
341 }
342 true_ok && false_err
343}
344
345fn is_ok_constructor_with_identity(
346 expr: &Spanned<Expr>,
347 type_name: &str,
348 carrier_field: &str,
349 param_name: &str,
350) -> bool {
351 let (ctor_name, ctor_arg_node) = match &expr.node {
353 Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
354 Expr::FnCall(callee, args) if args.len() == 1 => {
355 let Some(name) = expr_to_dotted_name(&callee.node) else {
356 return false;
357 };
358 (name, &args[0].node)
359 }
360 _ => return false,
361 };
362 if ctor_name != "Result.Ok" {
363 return false;
364 }
365 let (t, fields) = match ctor_arg_node {
366 Expr::RecordCreate {
367 type_name: t,
368 fields,
369 } => (t.as_str(), fields),
370 _ => return false,
371 };
372 if t != type_name || fields.len() != 1 {
373 return false;
374 }
375 let (fname, fvalue) = &fields[0];
376 if fname != carrier_field {
377 return false;
378 }
379 match &fvalue.node {
384 Expr::Ident(name) | Expr::Resolved { name, .. } => name == param_name,
385 _ => false,
386 }
387}
388
389pub fn refinement_lift_for_given(
399 given_name: &str,
400 given_type: &str,
401 lhs: &Spanned<Expr>,
402 rhs: &Spanned<Expr>,
403 ctx: &CodegenContext,
404) -> Option<String> {
405 if given_type == "Float" {
413 return None;
414 }
415 let mut result: Option<String> = None;
423 search_refinement_wrapper(lhs, given_name, given_type, ctx, &mut result);
424 search_refinement_wrapper(rhs, given_name, given_type, ctx, &mut result);
425 result
426}
427
428fn search_refinement_wrapper(
429 expr: &Spanned<Expr>,
430 given_name: &str,
431 given_type: &str,
432 ctx: &CodegenContext,
433 result: &mut Option<String>,
434) {
435 if result.is_some() {
436 return;
437 }
438 match &expr.node {
439 Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
440 let (_, fvalue) = &fields[0];
441 let matches_var = matches!(
442 &fvalue.node,
443 Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == given_name
444 );
445 if matches_var
446 && let Some((canonical_key, decl)) = find_refined_type_with_key(ctx, type_name)
447 && decl.carrier_type == given_type
448 {
449 *result = Some(canonical_key);
450 return;
451 }
452 for (_, v) in fields {
453 search_refinement_wrapper(v, given_name, given_type, ctx, result);
454 }
455 }
456 Expr::FnCall(callee, args) => {
457 search_refinement_wrapper(callee, given_name, given_type, ctx, result);
458 for a in args {
459 search_refinement_wrapper(a, given_name, given_type, ctx, result);
460 }
461 }
462 Expr::BinOp(_, l, r) => {
463 search_refinement_wrapper(l, given_name, given_type, ctx, result);
464 search_refinement_wrapper(r, given_name, given_type, ctx, result);
465 }
466 Expr::Attr(o, _) => search_refinement_wrapper(o, given_name, given_type, ctx, result),
467 Expr::Neg(i) | Expr::ErrorProp(i) => {
468 search_refinement_wrapper(i, given_name, given_type, ctx, result);
469 }
470 Expr::Match { subject, arms } => {
471 search_refinement_wrapper(subject, given_name, given_type, ctx, result);
472 for arm in arms {
473 search_refinement_wrapper(&arm.body, given_name, given_type, ctx, result);
474 }
475 }
476 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
477 for it in items {
478 search_refinement_wrapper(it, given_name, given_type, ctx, result);
479 }
480 }
481 Expr::Constructor(_, Some(arg)) => {
482 search_refinement_wrapper(arg, given_name, given_type, ctx, result);
483 }
484 _ => {}
485 }
486}
487
488pub fn project_lifted_idents_to_val(
515 expr: &Spanned<Expr>,
516 lifted_vars: &std::collections::HashMap<String, String>,
517) -> Spanned<Expr> {
518 if lifted_vars.is_empty() {
519 return expr.clone();
520 }
521 let new_node = match &expr.node {
522 Expr::BinOp(op, l, r) if is_comparator_binop(*op) => {
523 let l_proj = project_lifted_ident_leaf(l, lifted_vars);
524 let r_proj = project_lifted_ident_leaf(r, lifted_vars);
525 Expr::BinOp(*op, Box::new(l_proj), Box::new(r_proj))
526 }
527 Expr::FnCall(callee, args) => {
528 let name = expr_to_dotted_name(&callee.node);
529 if matches!(name.as_deref(), Some("Bool.and") | Some("Bool.or")) {
530 Expr::FnCall(
531 callee.clone(),
532 args.iter()
533 .map(|a| project_lifted_idents_to_val(a, lifted_vars))
534 .collect(),
535 )
536 } else {
537 return expr.clone();
538 }
539 }
540 _ => return expr.clone(),
541 };
542 Spanned::new(new_node, expr.line)
543}
544
545fn is_comparator_binop(op: crate::ast::BinOp) -> bool {
546 use crate::ast::BinOp::*;
547 matches!(op, Lt | Gt | Lte | Gte | Eq | Neq)
548}
549
550fn project_lifted_ident_leaf(
551 expr: &Spanned<Expr>,
552 lifted_vars: &std::collections::HashMap<String, String>,
553) -> Spanned<Expr> {
554 let target_name = match &expr.node {
555 Expr::Ident(n) | Expr::Resolved { name: n, .. } => n,
556 _ => return expr.clone(),
557 };
558 if lifted_vars.contains_key(target_name) {
559 Spanned::new(
560 Expr::Attr(
561 Box::new(Spanned::new(Expr::Ident(target_name.clone()), expr.line)),
562 "val".to_string(),
563 ),
564 expr.line,
565 )
566 } else {
567 expr.clone()
568 }
569}
570
571pub fn strip_refinement_wrappers(
572 expr: &Spanned<Expr>,
573 lifted_vars: &std::collections::HashMap<String, String>,
574 ctx: &CodegenContext,
575) -> Spanned<Expr> {
576 let new_node = match &expr.node {
577 Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
578 let (_, fvalue) = &fields[0];
579 let var_name = match &fvalue.node {
580 Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
581 _ => None,
582 };
583 let canonical_for_ast = find_refined_type_with_key(ctx, type_name).map(|(k, _)| k);
590 if let Some(name) = var_name
591 && let Some(refined) = lifted_vars.get(&name)
592 && canonical_for_ast.as_deref() == Some(refined.as_str())
593 {
594 return Spanned::new(Expr::Ident(name), expr.line);
595 }
596 let new_fields: Vec<(String, Spanned<Expr>)> = fields
597 .iter()
598 .map(|(n, v)| (n.clone(), strip_refinement_wrappers(v, lifted_vars, ctx)))
599 .collect();
600 Expr::RecordCreate {
601 type_name: type_name.clone(),
602 fields: new_fields,
603 }
604 }
605 Expr::FnCall(callee, args) => Expr::FnCall(
606 Box::new(strip_refinement_wrappers(callee, lifted_vars, ctx)),
607 args.iter()
608 .map(|a| strip_refinement_wrappers(a, lifted_vars, ctx))
609 .collect(),
610 ),
611 Expr::BinOp(op, l, r) => Expr::BinOp(
612 *op,
613 Box::new(strip_refinement_wrappers(l, lifted_vars, ctx)),
614 Box::new(strip_refinement_wrappers(r, lifted_vars, ctx)),
615 ),
616 Expr::Attr(o, f) => Expr::Attr(
617 Box::new(strip_refinement_wrappers(o, lifted_vars, ctx)),
618 f.clone(),
619 ),
620 Expr::Neg(i) => Expr::Neg(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx))),
621 Expr::ErrorProp(i) => {
622 Expr::ErrorProp(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx)))
623 }
624 _ => expr.node.clone(),
625 };
626 Spanned::new(new_node, expr.line)
627}
628
629pub fn swap_comparison_operands_op(op: &crate::ast::BinOp) -> Option<crate::ast::BinOp> {
635 use crate::ast::BinOp::*;
636 match op {
637 Lt => Some(Gt),
638 Gt => Some(Lt),
639 Lte => Some(Gte),
640 Gte => Some(Lte),
641 Eq => Some(Eq),
642 Neq => Some(Neq),
643 _ => None,
644 }
645}
646
647pub fn predicate_syntactic_eq(a: &Spanned<Expr>, b: &Spanned<Expr>) -> bool {
658 match (&a.node, &b.node) {
659 (Expr::BinOp(op_a, la, ra), Expr::BinOp(op_b, lb, rb)) => {
660 if op_a == op_b && predicate_syntactic_eq(la, lb) && predicate_syntactic_eq(ra, rb) {
661 return true;
662 }
663 if let Some(swapped) = swap_comparison_operands_op(op_a)
664 && &swapped == op_b
665 && predicate_syntactic_eq(la, rb)
666 && predicate_syntactic_eq(ra, lb)
667 {
668 return true;
669 }
670 false
671 }
672 _ => a.node == b.node,
673 }
674}
675
676pub fn flatten_bool_and_conjuncts(expr: &Spanned<Expr>) -> Vec<Spanned<Expr>> {
683 if let Expr::FnCall(callee, args) = &expr.node
684 && args.len() == 2
685 && let Some(name) = expr_to_dotted_name(&callee.node)
686 && name == "Bool.and"
687 {
688 let mut out = flatten_bool_and_conjuncts(&args[0]);
689 out.extend(flatten_bool_and_conjuncts(&args[1]));
690 return out;
691 }
692 vec![expr.clone()]
693}
694
695pub fn flatten_bool_and_conjuncts_resolved(
700 expr: &Spanned<crate::ir::hir::ResolvedExpr>,
701) -> Vec<Spanned<crate::ir::hir::ResolvedExpr>> {
702 use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
703 if let ResolvedExpr::Call(ResolvedCallee::Builtin(name), args) = &expr.node
704 && name == "Bool.and"
705 && args.len() == 2
706 {
707 let mut out = flatten_bool_and_conjuncts_resolved(&args[0]);
708 out.extend(flatten_bool_and_conjuncts_resolved(&args[1]));
709 return out;
710 }
711 vec![expr.clone()]
712}
713
714pub fn predicate_syntactic_eq_resolved(
716 a: &Spanned<crate::ir::hir::ResolvedExpr>,
717 b: &Spanned<crate::ir::hir::ResolvedExpr>,
718) -> bool {
719 use crate::ir::hir::ResolvedExpr;
720 match (&a.node, &b.node) {
721 (ResolvedExpr::BinOp(op_a, la, ra), ResolvedExpr::BinOp(op_b, lb, rb)) => {
722 if op_a == op_b
723 && predicate_syntactic_eq_resolved(la, lb)
724 && predicate_syntactic_eq_resolved(ra, rb)
725 {
726 return true;
727 }
728 if let Some(swapped) = swap_comparison_operands_op(op_a)
729 && &swapped == op_b
730 && predicate_syntactic_eq_resolved(la, rb)
731 && predicate_syntactic_eq_resolved(ra, lb)
732 {
733 return true;
734 }
735 false
736 }
737 _ => a.node == b.node,
738 }
739}
740
741pub fn substitute_ident_in_resolved_expr(
749 expr: &Spanned<crate::ir::hir::ResolvedExpr>,
750 from: &str,
751 to: &str,
752) -> Spanned<crate::ir::hir::ResolvedExpr> {
753 use crate::ir::hir::{ResolvedExpr, ResolvedMatchArm, ResolvedStrPart};
754 let line = expr.line;
755 let rec = |e: &Spanned<ResolvedExpr>| substitute_ident_in_resolved_expr(e, from, to);
756 let new_node = match &expr.node {
757 ResolvedExpr::Ident(name) | ResolvedExpr::Resolved { name, .. } if name == from => {
758 ResolvedExpr::Ident(to.to_string())
759 }
760 ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {
761 return expr.clone();
762 }
763 ResolvedExpr::Attr(obj, field) => ResolvedExpr::Attr(Box::new(rec(obj)), field.clone()),
764 ResolvedExpr::Call(callee, args) => {
765 ResolvedExpr::Call(callee.clone(), args.iter().map(&rec).collect())
766 }
767 ResolvedExpr::BinOp(op, left, right) => {
768 ResolvedExpr::BinOp(*op, Box::new(rec(left)), Box::new(rec(right)))
769 }
770 ResolvedExpr::Neg(inner) => ResolvedExpr::Neg(Box::new(rec(inner))),
771 ResolvedExpr::Match { subject, arms } => ResolvedExpr::Match {
772 subject: Box::new(rec(subject)),
773 arms: arms
774 .iter()
775 .map(|arm| ResolvedMatchArm {
776 pattern: arm.pattern.clone(),
777 body: Box::new(rec(&arm.body)),
778 binding_slots: std::sync::OnceLock::new(),
779 })
780 .collect(),
781 },
782 ResolvedExpr::Ctor(ctor, args) => {
783 ResolvedExpr::Ctor(ctor.clone(), args.iter().map(&rec).collect())
784 }
785 ResolvedExpr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(rec(inner))),
786 ResolvedExpr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
787 parts
788 .iter()
789 .map(|p| match p {
790 ResolvedStrPart::Literal(_) => p.clone(),
791 ResolvedStrPart::Parsed(inner) => ResolvedStrPart::Parsed(Box::new(rec(inner))),
792 })
793 .collect(),
794 ),
795 ResolvedExpr::List(items) => ResolvedExpr::List(items.iter().map(&rec).collect()),
796 ResolvedExpr::Tuple(items) => ResolvedExpr::Tuple(items.iter().map(&rec).collect()),
797 ResolvedExpr::IndependentProduct(items, flag) => {
798 ResolvedExpr::IndependentProduct(items.iter().map(&rec).collect(), *flag)
799 }
800 ResolvedExpr::MapLiteral(entries) => {
801 ResolvedExpr::MapLiteral(entries.iter().map(|(k, v)| (rec(k), rec(v))).collect())
802 }
803 ResolvedExpr::RecordCreate {
804 type_id,
805 type_name,
806 fields,
807 } => ResolvedExpr::RecordCreate {
808 type_id: *type_id,
809 type_name: type_name.clone(),
810 fields: fields.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
811 },
812 ResolvedExpr::RecordUpdate {
813 type_id,
814 type_name,
815 base,
816 updates,
817 } => ResolvedExpr::RecordUpdate {
818 type_id: *type_id,
819 type_name: type_name.clone(),
820 base: Box::new(rec(base)),
821 updates: updates.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
822 },
823 ResolvedExpr::TailCall { target, args } => ResolvedExpr::TailCall {
824 target: *target,
825 args: args.iter().map(&rec).collect(),
826 },
827 };
828 Spanned::new(new_node, line)
829}
830
831pub fn substitute_ident_in_expr(expr: &Spanned<Expr>, from: &str, to: &str) -> Spanned<Expr> {
840 use crate::ast::{MatchArm, StrPart, TailCallData};
841 let line = expr.line;
842 let new_node = match &expr.node {
843 Expr::Ident(name) | Expr::Resolved { name, .. } if name == from => {
844 Expr::Ident(to.to_string())
845 }
846 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => return expr.clone(),
847 Expr::Attr(obj, field) => Expr::Attr(
848 Box::new(substitute_ident_in_expr(obj, from, to)),
849 field.clone(),
850 ),
851 Expr::FnCall(callee, args) => Expr::FnCall(
852 Box::new(substitute_ident_in_expr(callee, from, to)),
853 args.iter()
854 .map(|a| substitute_ident_in_expr(a, from, to))
855 .collect(),
856 ),
857 Expr::BinOp(op, left, right) => Expr::BinOp(
858 *op,
859 Box::new(substitute_ident_in_expr(left, from, to)),
860 Box::new(substitute_ident_in_expr(right, from, to)),
861 ),
862 Expr::Neg(inner) => Expr::Neg(Box::new(substitute_ident_in_expr(inner, from, to))),
863 Expr::Match { subject, arms } => Expr::Match {
864 subject: Box::new(substitute_ident_in_expr(subject, from, to)),
865 arms: arms
866 .iter()
867 .map(|arm| MatchArm {
868 pattern: arm.pattern.clone(),
869 body: Box::new(substitute_ident_in_expr(&arm.body, from, to)),
870 binding_slots: std::sync::OnceLock::new(),
871 })
872 .collect(),
873 },
874 Expr::Constructor(name, arg) => Expr::Constructor(
875 name.clone(),
876 arg.as_ref()
877 .map(|inner| Box::new(substitute_ident_in_expr(inner, from, to))),
878 ),
879 Expr::ErrorProp(inner) => {
880 Expr::ErrorProp(Box::new(substitute_ident_in_expr(inner, from, to)))
881 }
882 Expr::InterpolatedStr(parts) => Expr::InterpolatedStr(
883 parts
884 .iter()
885 .map(|part| match part {
886 StrPart::Literal(_) => part.clone(),
887 StrPart::Parsed(inner) => {
888 StrPart::Parsed(Box::new(substitute_ident_in_expr(inner, from, to)))
889 }
890 })
891 .collect(),
892 ),
893 Expr::List(items) => Expr::List(
894 items
895 .iter()
896 .map(|item| substitute_ident_in_expr(item, from, to))
897 .collect(),
898 ),
899 Expr::Tuple(items) => Expr::Tuple(
900 items
901 .iter()
902 .map(|item| substitute_ident_in_expr(item, from, to))
903 .collect(),
904 ),
905 Expr::IndependentProduct(items, flag) => Expr::IndependentProduct(
906 items
907 .iter()
908 .map(|item| substitute_ident_in_expr(item, from, to))
909 .collect(),
910 *flag,
911 ),
912 Expr::MapLiteral(entries) => Expr::MapLiteral(
913 entries
914 .iter()
915 .map(|(k, v)| {
916 (
917 substitute_ident_in_expr(k, from, to),
918 substitute_ident_in_expr(v, from, to),
919 )
920 })
921 .collect(),
922 ),
923 Expr::RecordCreate { type_name, fields } => Expr::RecordCreate {
924 type_name: type_name.clone(),
925 fields: fields
926 .iter()
927 .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
928 .collect(),
929 },
930 Expr::RecordUpdate {
931 type_name,
932 base,
933 updates,
934 } => Expr::RecordUpdate {
935 type_name: type_name.clone(),
936 base: Box::new(substitute_ident_in_expr(base, from, to)),
937 updates: updates
938 .iter()
939 .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
940 .collect(),
941 },
942 Expr::TailCall(boxed) => Expr::TailCall(Box::new(TailCallData::new(
943 boxed.target.clone(),
944 boxed
945 .args
946 .iter()
947 .map(|a| substitute_ident_in_expr(a, from, to))
948 .collect(),
949 ))),
950 };
951 Spanned::new(new_node, line)
952}
953
954pub fn when_is_redundant_with_refinement_lifts(
970 when_expr: &Spanned<Expr>,
971 lifted_vars: &std::collections::HashMap<String, String>,
972 ctx: &CodegenContext,
973) -> bool {
974 if lifted_vars.is_empty() {
975 return false;
976 }
977 let resolved_when = ctx.resolve_expr(when_expr, ctx.active_module_scope().as_deref());
985 let when_conjuncts = flatten_bool_and_conjuncts_resolved(&resolved_when);
986 let mut lifted_predicates: Vec<Spanned<crate::ir::hir::ResolvedExpr>> = Vec::new();
1000 for (given_name, refined_type) in lifted_vars {
1001 let Some(decl) = find_refined_type(ctx, refined_type) else {
1002 return false;
1003 };
1004 let substituted = substitute_ident_in_resolved_expr(
1005 &decl.invariant.expr,
1006 decl.predicate_param.as_str(),
1007 given_name,
1008 );
1009 lifted_predicates.extend(flatten_bool_and_conjuncts_resolved(&substituted));
1010 }
1011 if when_conjuncts.len() != lifted_predicates.len() {
1012 return false;
1013 }
1014 let mut matched = vec![false; lifted_predicates.len()];
1015 for wc in &when_conjuncts {
1016 let Some(idx) = (0..lifted_predicates.len())
1017 .find(|&i| !matched[i] && predicate_syntactic_eq_resolved(wc, &lifted_predicates[i]))
1018 else {
1019 return false;
1020 };
1021 matched[idx] = true;
1022 }
1023 true
1024}
1025
1026fn is_err_constructor(expr: &Spanned<Expr>) -> bool {
1027 match &expr.node {
1028 Expr::Constructor(name, Some(_)) => name == "Result.Err",
1029 Expr::FnCall(callee, args) if args.len() == 1 => {
1030 matches!(
1031 expr_to_dotted_name(&callee.node),
1032 Some(name) if name == "Result.Err"
1033 )
1034 }
1035 _ => false,
1036 }
1037}
1038
1039pub fn is_pure_fn(fd: &FnDef) -> bool {
1045 fd.effects.is_empty() && fd.name != "main"
1046}
1047
1048pub fn find_refined_type<'a>(
1075 ctx: &'a CodegenContext,
1076 name: &str,
1077) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1078 find_refined_type_with_key_scoped(ctx, name, None).map(|(_, d)| d)
1079}
1080
1081pub fn backend_named_type_key(ctx: &CodegenContext, ty: &crate::types::Type) -> Option<String> {
1110 let crate::types::Type::Named { id, name } = ty else {
1111 return None;
1112 };
1113 if let Some(type_id) = id {
1114 return Some(ctx.symbol_table.type_entry(*type_id).key.canonical());
1115 }
1116 Some(name.clone())
1117}
1118
1119pub fn backend_type_def_key(ctx: &CodegenContext, td: &crate::ast::TypeDef) -> String {
1134 let key = type_key_for_decl(ctx, td);
1135 if ctx.symbol_table.type_id_of(&key).is_some() {
1136 key.canonical()
1137 } else {
1138 type_def_name(td).to_string()
1139 }
1140}
1141
1142pub fn find_refined_type_by_id(
1158 ctx: &CodegenContext,
1159 type_id: crate::ir::TypeId,
1160) -> Option<&crate::ir::proof_ir::RefinedTypeDecl> {
1161 ctx.proof_ir.refined_types.get(&type_id)
1162}
1163
1164pub fn find_refined_type_for_named<'a>(
1173 ctx: &'a CodegenContext,
1174 named: &crate::types::Type,
1175) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1176 let crate::types::Type::Named { id, name } = named else {
1177 return None;
1178 };
1179 match id {
1180 Some(type_id) => find_refined_type_by_id(ctx, *type_id),
1181 None => find_refined_type(ctx, name),
1182 }
1183}
1184
1185pub fn find_fn_contract_scoped<'a>(
1197 ctx: &'a CodegenContext,
1198 name: &str,
1199 scope: Option<&str>,
1200) -> Option<&'a crate::ir::proof_ir::FnContract> {
1201 let symbols = &ctx.symbol_table;
1209 let bare = name.rsplit('.').next().unwrap_or(name);
1210 let name_is_already_qualified = name.contains('.');
1211 let try_key = |key: crate::ir::FnKey| -> Option<&'a crate::ir::proof_ir::FnContract> {
1212 let id = symbols.fn_id_of(&key)?;
1213 ctx.proof_ir.fn_contracts.get(&id)
1214 };
1215 if let Some(prefix) = scope
1216 && !name_is_already_qualified
1217 && let Some(c) = try_key(crate::ir::FnKey::in_module(prefix.to_string(), bare))
1218 {
1219 return Some(c);
1220 }
1221 let direct_key =
1222 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1223 crate::ir::FnKey::in_module(prefix.to_string(), bare_part)
1224 } else {
1225 crate::ir::FnKey::entry(name)
1226 };
1227 if let Some(c) = try_key(direct_key) {
1228 return Some(c);
1229 }
1230 for m in &ctx.modules {
1231 for fd in &m.fn_defs {
1232 if fd.name == bare
1233 && let Some(c) = try_key(crate::ir::FnKey::in_module(m.prefix.clone(), bare))
1234 {
1235 return Some(c);
1236 }
1237 }
1238 }
1239 None
1240}
1241
1242pub fn fn_contract_exists_scoped(ctx: &CodegenContext, name: &str, scope: Option<&str>) -> bool {
1244 find_fn_contract_scoped(ctx, name, scope).is_some()
1245}
1246
1247pub fn fn_key_for_decl(ctx: &CodegenContext, fd: &FnDef) -> crate::ir::FnKey {
1255 match fn_owning_scope_for(ctx, fd) {
1256 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), fd.name.clone()),
1257 None => crate::ir::FnKey::entry(fd.name.clone()),
1258 }
1259}
1260
1261pub fn type_key_for_decl(ctx: &CodegenContext, td: &TypeDef) -> crate::ir::TypeKey {
1265 for m in &ctx.modules {
1266 for t in &m.type_defs {
1267 if std::ptr::eq(t, td) {
1268 return crate::ir::TypeKey::in_module(m.prefix.clone(), type_def_name(td));
1269 }
1270 }
1271 }
1272 crate::ir::TypeKey::entry(type_def_name(td))
1273}
1274
1275pub fn type_key_for_name(
1282 ctx: &CodegenContext,
1283 name: &str,
1284 scope: Option<&str>,
1285) -> crate::ir::TypeKey {
1286 let bare = name.rsplit('.').next().unwrap_or(name);
1287 let name_is_qualified = name.contains('.');
1288 if let Some(prefix) = scope
1289 && !name_is_qualified
1290 {
1291 for m in &ctx.modules {
1292 if m.prefix == prefix && m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1293 return crate::ir::TypeKey::in_module(prefix.to_string(), bare);
1294 }
1295 }
1296 }
1297 if !name_is_qualified && ctx.type_defs.iter().any(|td| type_def_name(td) == bare) {
1300 return crate::ir::TypeKey::entry(bare);
1301 }
1302 if name_is_qualified
1303 && let Some((prefix, bare_part)) = name.rsplit_once('.')
1304 && ctx.modules.iter().any(|m| m.prefix == prefix)
1305 {
1306 return crate::ir::TypeKey::in_module(prefix.to_string(), bare_part);
1307 }
1308 for m in &ctx.modules {
1309 if m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1310 return crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1311 }
1312 }
1313 crate::ir::TypeKey::entry(name)
1318}
1319
1320pub fn fn_owning_scope_for<'a>(ctx: &'a CodegenContext, fd: &FnDef) -> Option<&'a str> {
1330 for m in &ctx.modules {
1331 for f in &m.fn_defs {
1332 if std::ptr::eq(f, fd) {
1333 return Some(m.prefix.as_str());
1334 }
1335 }
1336 }
1337 None
1338}
1339
1340pub fn find_fn_contract_for_fn<'a>(
1346 ctx: &'a CodegenContext,
1347 fd: &FnDef,
1348) -> Option<&'a crate::ir::proof_ir::FnContract> {
1349 let symbols = &ctx.symbol_table;
1355 let fn_key = fn_key_for_decl(ctx, fd);
1356 let fn_id = symbols.fn_id_of(&fn_key)?;
1357 ctx.proof_ir.fn_contracts.get(&fn_id)
1358}
1359
1360pub fn fn_contract_exists_for_fn(ctx: &CodegenContext, fd: &FnDef) -> bool {
1362 find_fn_contract_for_fn(ctx, fd).is_some()
1363}
1364
1365pub fn fn_id_for_decl(ctx: &CodegenContext, fd: &FnDef) -> Option<crate::ir::FnId> {
1371 let fn_key = fn_key_for_decl(ctx, fd);
1372 ctx.symbol_table.fn_id_of(&fn_key)
1373}
1374
1375pub fn fn_id_for_dotted_name(ctx: &CodegenContext, dotted: &str) -> Option<crate::ir::FnId> {
1381 let key = if let Some((prefix, bare)) = dotted.rsplit_once('.') {
1382 crate::ir::FnKey::in_module(prefix.to_string(), bare)
1383 } else {
1384 crate::ir::FnKey::entry(dotted)
1385 };
1386 ctx.symbol_table.fn_id_of(&key)
1387}
1388
1389pub fn find_refined_type_with_key<'a>(
1394 ctx: &'a CodegenContext,
1395 name: &str,
1396) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1397 find_refined_type_with_key_scoped(ctx, name, None)
1398}
1399
1400pub fn find_refined_type_scoped<'a>(
1418 ctx: &'a CodegenContext,
1419 name: &str,
1420 scope: Option<&str>,
1421) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1422 find_refined_type_with_key_scoped(ctx, name, scope).map(|(_, d)| d)
1423}
1424
1425pub fn find_refined_type_with_key_scoped<'a>(
1440 ctx: &'a CodegenContext,
1441 name: &str,
1442 scope: Option<&str>,
1443) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1444 let symbols = &ctx.symbol_table;
1445 let bare = name.rsplit('.').next().unwrap_or(name);
1446 let name_is_already_qualified = name.contains('.');
1447 let try_key =
1448 |key: crate::ir::TypeKey| -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1449 let id = symbols.type_id_of(&key)?;
1450 let decl = ctx.proof_ir.refined_types.get(&id)?;
1451 Some((key.canonical(), decl))
1452 };
1453 if let Some(prefix) = scope
1454 && !name_is_already_qualified
1455 && let Some(hit) = try_key(crate::ir::TypeKey::in_module(prefix.to_string(), bare))
1456 {
1457 return Some(hit);
1458 }
1459 let direct_key =
1460 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1461 crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1462 } else {
1463 crate::ir::TypeKey::entry(name)
1464 };
1465 if let Some(hit) = try_key(direct_key) {
1466 return Some(hit);
1467 }
1468 for m in &ctx.modules {
1469 for td in &m.type_defs {
1470 if type_def_name(td) == bare
1471 && let Some(hit) = try_key(crate::ir::TypeKey::in_module(m.prefix.clone(), bare))
1472 {
1473 return Some(hit);
1474 }
1475 }
1476 }
1477 None
1478}
1479
1480pub fn resolve_refined_type_in<'a>(
1486 refined_types: &'a std::collections::HashMap<
1487 crate::ir::TypeId,
1488 crate::ir::proof_ir::RefinedTypeDecl,
1489 >,
1490 symbols: &crate::ir::SymbolTable,
1491 modules: &[crate::codegen::ModuleInfo],
1492 name: &str,
1493) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1494 resolve_refined_type_in_with_key(refined_types, symbols, modules, name).map(|(_, d)| d)
1495}
1496
1497pub fn resolve_refined_type_in_with_key<'a>(
1503 refined_types: &'a std::collections::HashMap<
1504 crate::ir::TypeId,
1505 crate::ir::proof_ir::RefinedTypeDecl,
1506 >,
1507 symbols: &crate::ir::SymbolTable,
1508 modules: &[crate::codegen::ModuleInfo],
1509 name: &str,
1510) -> Option<(crate::ir::TypeId, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1511 let bare = name.rsplit('.').next().unwrap_or(name);
1512 let name_is_already_qualified = name.contains('.');
1513 let direct_key =
1514 if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1515 crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1516 } else {
1517 crate::ir::TypeKey::entry(name)
1518 };
1519 if let Some(id) = symbols.type_id_of(&direct_key)
1520 && let Some(decl) = refined_types.get(&id)
1521 {
1522 return Some((id, decl));
1523 }
1524 for m in modules {
1525 for td in &m.type_defs {
1526 if type_def_name(td) == bare {
1527 let canonical = crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1528 if let Some(id) = symbols.type_id_of(&canonical)
1529 && let Some(decl) = refined_types.get(&id)
1530 {
1531 return Some((id, decl));
1532 }
1533 }
1534 }
1535 }
1536 None
1537}
1538
1539pub fn all_givens_are_singletons(law: &crate::ast::VerifyLaw) -> bool {
1548 !law.givens.is_empty()
1549 && law.givens.iter().all(|g| match &g.domain {
1550 VerifyGivenDomain::Explicit(values) => values.len() == 1,
1551 VerifyGivenDomain::IntRange { start, end } => start == end,
1552 })
1553}
1554
1555pub fn unclassified_fn_names(ctx: &CodegenContext) -> HashSet<String> {
1562 ctx.proof_ir
1563 .unclassified_fns
1564 .iter()
1565 .filter_map(|uf| {
1566 let s = &uf.message;
1567 let start = s.find('\'')?;
1568 let rest = &s[start + 1..];
1569 let end = rest.find('\'')?;
1570 Some(rest[..end].to_string())
1571 })
1572 .collect()
1573}
1574
1575pub fn law_calls_unclassified_fn(
1587 law: &crate::ast::VerifyLaw,
1588 unclassified: &HashSet<String>,
1589) -> bool {
1590 if unclassified.is_empty() {
1591 return false;
1592 }
1593 expr_calls_named(&law.lhs, unclassified) || expr_calls_named(&law.rhs, unclassified)
1594}
1595
1596fn expr_calls_named(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1597 match &expr.node {
1598 Expr::FnCall(callee, args) => {
1599 let direct = expr_to_dotted_name(&callee.node)
1610 .map(|n| {
1611 let bare = n.rsplit('.').next().unwrap_or(n.as_str());
1612 names.contains(n.as_str()) || names.contains(bare)
1613 })
1614 .unwrap_or(false);
1615 direct
1616 || expr_calls_named(callee, names)
1617 || args.iter().any(|a| expr_calls_named(a, names))
1618 }
1619 Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1620 expr_calls_named(inner, names)
1621 }
1622 Expr::BinOp(_, l, r) => expr_calls_named(l, names) || expr_calls_named(r, names),
1623 Expr::Match { subject, arms } => {
1624 expr_calls_named(subject, names)
1625 || arms.iter().any(|a| expr_calls_named(&a.body, names))
1626 }
1627 Expr::Constructor(_, Some(arg)) => expr_calls_named(arg, names),
1628 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1629 items.iter().any(|i| expr_calls_named(i, names))
1630 }
1631 Expr::MapLiteral(entries) => entries
1632 .iter()
1633 .any(|(k, v)| expr_calls_named(k, names) || expr_calls_named(v, names)),
1634 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| expr_calls_named(v, names)),
1635 Expr::RecordUpdate { base, updates, .. } => {
1636 expr_calls_named(base, names) || updates.iter().any(|(_, v)| expr_calls_named(v, names))
1637 }
1638 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1639 crate::ast::StrPart::Parsed(inner) => expr_calls_named(inner, names),
1640 crate::ast::StrPart::Literal(_) => false,
1641 }),
1642 Expr::TailCall(boxed) => {
1643 let crate::ast::TailCallData { target, args, .. } = boxed.as_ref();
1644 names.contains(target) || args.iter().any(|a| expr_calls_named(a, names))
1645 }
1646 _ => false,
1647 }
1648}
1649
1650pub fn law_rhs_is_independent_of_givens(law: &crate::ast::VerifyLaw) -> bool {
1660 let given_names: HashSet<&str> = law.givens.iter().map(|g| g.name.as_str()).collect();
1661 if given_names.is_empty() {
1662 return true;
1663 }
1664 !expr_references_any_ident(&law.rhs, &given_names)
1665}
1666
1667fn expr_references_any_ident(expr: &Spanned<Expr>, names: &HashSet<&str>) -> bool {
1668 match &expr.node {
1669 Expr::Ident(name) | Expr::Resolved { name, .. } => names.contains(name.as_str()),
1670 Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1671 expr_references_any_ident(inner, names)
1672 }
1673 Expr::FnCall(callee, args) => {
1674 expr_references_any_ident(callee, names)
1675 || args.iter().any(|a| expr_references_any_ident(a, names))
1676 }
1677 Expr::BinOp(_, l, r) => {
1678 expr_references_any_ident(l, names) || expr_references_any_ident(r, names)
1679 }
1680 Expr::Match { subject, arms } => {
1681 expr_references_any_ident(subject, names)
1682 || arms
1683 .iter()
1684 .any(|a| expr_references_any_ident(&a.body, names))
1685 }
1686 Expr::Constructor(_, Some(arg)) => expr_references_any_ident(arg, names),
1687 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1688 items.iter().any(|i| expr_references_any_ident(i, names))
1689 }
1690 Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
1691 expr_references_any_ident(k, names) || expr_references_any_ident(v, names)
1692 }),
1693 Expr::RecordCreate { fields, .. } => fields
1694 .iter()
1695 .any(|(_, v)| expr_references_any_ident(v, names)),
1696 Expr::RecordUpdate { base, updates, .. } => {
1697 expr_references_any_ident(base, names)
1698 || updates
1699 .iter()
1700 .any(|(_, v)| expr_references_any_ident(v, names))
1701 }
1702 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1703 crate::ast::StrPart::Parsed(inner) => expr_references_any_ident(inner, names),
1704 crate::ast::StrPart::Literal(_) => false,
1705 }),
1706 Expr::TailCall(boxed) => {
1707 let crate::ast::TailCallData { args, .. } = boxed.as_ref();
1708 args.iter().any(|a| expr_references_any_ident(a, names))
1709 }
1710 _ => false,
1711 }
1712}
1713
1714pub fn law_lhs_has_trace_projection(expr: &Spanned<Expr>) -> bool {
1735 expr_has_trace_field(expr) && expr_has_trace_api_call(expr)
1736}
1737
1738fn expr_has_trace_field(expr: &Spanned<Expr>) -> bool {
1739 match &expr.node {
1740 Expr::Attr(inner, field) => field == "trace" || expr_has_trace_field(inner),
1741 Expr::FnCall(callee, args) => {
1742 expr_has_trace_field(callee) || args.iter().any(expr_has_trace_field)
1743 }
1744 Expr::BinOp(_, l, r) => expr_has_trace_field(l) || expr_has_trace_field(r),
1745 Expr::Match { subject, arms } => {
1746 expr_has_trace_field(subject) || arms.iter().any(|a| expr_has_trace_field(&a.body))
1747 }
1748 Expr::ErrorProp(inner) => expr_has_trace_field(inner),
1749 Expr::Constructor(_, Some(arg)) => expr_has_trace_field(arg),
1750 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1751 items.iter().any(expr_has_trace_field)
1752 }
1753 _ => false,
1754 }
1755}
1756
1757const TRACE_API_METHODS: &[&str] = &["event", "group", "branch", "length", "contains", "count"];
1762
1763fn expr_has_trace_api_call(expr: &Spanned<Expr>) -> bool {
1764 match &expr.node {
1765 Expr::FnCall(callee, args) => {
1766 let direct = matches!(
1767 &callee.node,
1768 Expr::Attr(_, method) if TRACE_API_METHODS.contains(&method.as_str())
1769 );
1770 direct || expr_has_trace_api_call(callee) || args.iter().any(expr_has_trace_api_call)
1771 }
1772 Expr::Attr(inner, _) | Expr::ErrorProp(inner) => expr_has_trace_api_call(inner),
1773 Expr::BinOp(_, l, r) => expr_has_trace_api_call(l) || expr_has_trace_api_call(r),
1774 Expr::Match { subject, arms } => {
1775 expr_has_trace_api_call(subject)
1776 || arms.iter().any(|a| expr_has_trace_api_call(&a.body))
1777 }
1778 Expr::Constructor(_, Some(arg)) => expr_has_trace_api_call(arg),
1779 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1780 items.iter().any(expr_has_trace_api_call)
1781 }
1782 _ => false,
1783 }
1784}
1785
1786pub fn is_recursive_type_def(td: &TypeDef) -> bool {
1789 match td {
1790 TypeDef::Sum { name, variants, .. } => is_recursive_sum(name, variants),
1791 TypeDef::Product { name, fields, .. } => is_recursive_product(name, fields),
1792 }
1793}
1794
1795pub fn type_def_name(td: &TypeDef) -> &str {
1797 match td {
1798 TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
1799 }
1800}
1801
1802pub fn is_recursive_sum(name: &str, variants: &[TypeVariant]) -> bool {
1806 variants
1807 .iter()
1808 .any(|v| v.fields.iter().any(|f| type_ref_contains(f, name)))
1809}
1810
1811pub fn is_recursive_product(name: &str, fields: &[(String, String)]) -> bool {
1813 fields.iter().any(|(_, ty)| type_ref_contains(ty, name))
1814}
1815
1816fn type_ref_contains(annotation: &str, type_name: &str) -> bool {
1817 annotation == type_name
1820 || annotation.contains(&format!("<{}", type_name))
1821 || annotation.contains(&format!("{}>", type_name))
1822 || annotation.contains(&format!(", {}", type_name))
1823 || annotation.contains(&format!("{},", type_name))
1824}
1825
1826pub(crate) fn is_user_type(name: &str, ctx: &CodegenContext) -> bool {
1828 let check_td = |td: &TypeDef| match td {
1829 TypeDef::Sum { name: n, .. } => n == name,
1830 TypeDef::Product { name: n, .. } => n == name,
1831 };
1832 ctx.type_defs.iter().any(check_td)
1833 || ctx.modules.iter().any(|m| m.type_defs.iter().any(check_td))
1834}
1835
1836pub(crate) fn resolve_module_call<'a>(
1839 dotted_name: &'a str,
1840 ctx: &'a CodegenContext,
1841) -> Option<(&'a str, &'a str)> {
1842 let mut best: Option<&str> = None;
1843 for prefix in &ctx.module_prefixes {
1844 let dotted_prefix = format!("{}.", prefix);
1845 if dotted_name.starts_with(&dotted_prefix) && best.is_none_or(|b| prefix.len() > b.len()) {
1846 best = Some(prefix.as_str());
1847 }
1848 }
1849 best.map(|prefix| (prefix, &dotted_name[prefix.len() + 1..]))
1850}
1851
1852pub(crate) fn module_prefix_to_rust_segments(prefix: &str) -> Vec<String> {
1853 prefix.split('.').map(module_segment_to_rust).collect()
1854}
1855
1856pub(crate) fn module_prefix_to_filename(prefix: &str) -> String {
1861 prefix.replace('.', "/")
1862}
1863
1864pub(crate) struct DeclaredEffects {
1876 pub bare_namespaces: HashSet<String>,
1877 pub methods: HashSet<String>,
1878}
1879
1880impl DeclaredEffects {
1881 pub fn includes(&self, c_method: &str) -> bool {
1884 if self.methods.contains(c_method) {
1885 return true;
1886 }
1887 if let Some((ns, _)) = c_method.split_once('.') {
1888 return self.bare_namespaces.contains(ns);
1889 }
1890 false
1891 }
1892}
1893
1894pub(crate) fn collect_declared_effects(ctx: &CodegenContext) -> DeclaredEffects {
1898 let mut bare_namespaces: HashSet<String> = HashSet::new();
1899 let mut methods: HashSet<String> = HashSet::new();
1900 let mut record = |effect: &str| {
1901 if effect.contains('.') {
1902 methods.insert(effect.to_string());
1903 } else {
1904 bare_namespaces.insert(effect.to_string());
1905 }
1906 };
1907 for item in &ctx.items {
1908 if let TopLevel::FnDef(fd) = item {
1909 for eff in &fd.effects {
1910 record(&eff.node);
1911 }
1912 }
1913 }
1914 for module in &ctx.modules {
1915 for fd in &module.fn_defs {
1916 for eff in &fd.effects {
1917 record(&eff.node);
1918 }
1919 }
1920 }
1921 DeclaredEffects {
1922 bare_namespaces,
1923 methods,
1924 }
1925}
1926
1927pub fn entry_basename(ctx: &CodegenContext) -> String {
1935 ctx.items
1936 .iter()
1937 .find_map(|item| match item {
1938 TopLevel::Module(m) => Some(m.name.clone()),
1939 _ => None,
1940 })
1941 .unwrap_or_else(|| {
1942 let mut chars = ctx.project_name.chars();
1943 match chars.next() {
1944 None => String::new(),
1945 Some(c) => c.to_uppercase().chain(chars).collect(),
1946 }
1947 })
1948}
1949
1950pub(crate) fn module_prefix_to_rust_path(prefix: &str) -> String {
1960 format!(
1961 "crate::aver_generated::{}",
1962 module_prefix_to_rust_segments(prefix).join("::")
1963 )
1964}
1965
1966fn module_segment_to_rust(segment: &str) -> String {
1967 let chars = segment.chars().collect::<Vec<_>>();
1968 let mut out = String::new();
1969
1970 for (idx, ch) in chars.iter().enumerate() {
1971 if ch.is_ascii_alphanumeric() {
1972 if ch.is_ascii_uppercase() {
1973 let prev_is_lower_or_digit = idx > 0
1974 && (chars[idx - 1].is_ascii_lowercase() || chars[idx - 1].is_ascii_digit());
1975 let next_is_lower = chars
1976 .get(idx + 1)
1977 .is_some_and(|next| next.is_ascii_lowercase());
1978 if idx > 0 && (prev_is_lower_or_digit || next_is_lower) && !out.ends_with('_') {
1979 out.push('_');
1980 }
1981 out.push(ch.to_ascii_lowercase());
1982 } else {
1983 out.push(ch.to_ascii_lowercase());
1984 }
1985 } else if !out.ends_with('_') {
1986 out.push('_');
1987 }
1988 }
1989
1990 let trimmed = out.trim_matches('_');
1991 let mut normalized = if trimmed.is_empty() {
1992 "module".to_string()
1993 } else {
1994 trimmed.to_string()
1995 };
1996
1997 if matches!(
1998 normalized.as_str(),
1999 "as" | "break"
2000 | "const"
2001 | "continue"
2002 | "crate"
2003 | "else"
2004 | "enum"
2005 | "extern"
2006 | "false"
2007 | "fn"
2008 | "for"
2009 | "if"
2010 | "impl"
2011 | "in"
2012 | "let"
2013 | "loop"
2014 | "match"
2015 | "mod"
2016 | "move"
2017 | "mut"
2018 | "pub"
2019 | "ref"
2020 | "return"
2021 | "self"
2022 | "Self"
2023 | "static"
2024 | "struct"
2025 | "super"
2026 | "trait"
2027 | "true"
2028 | "type"
2029 | "unsafe"
2030 | "use"
2031 | "where"
2032 | "while"
2033 ) {
2034 normalized.push_str("_mod");
2035 }
2036
2037 normalized
2038}
2039
2040pub(crate) fn split_type_params(s: &str, delim: char) -> Vec<String> {
2045 let mut parts = Vec::new();
2046 let mut depth = 0usize;
2047 let mut current = String::new();
2048 for ch in s.chars() {
2049 match ch {
2050 '<' | '(' => {
2051 depth += 1;
2052 current.push(ch);
2053 }
2054 '>' | ')' => {
2055 depth = depth.saturating_sub(1);
2056 current.push(ch);
2057 }
2058 _ if ch == delim && depth == 0 => {
2059 parts.push(current.trim().to_string());
2060 current.clear();
2061 }
2062 _ => current.push(ch),
2063 }
2064 }
2065 let rest = current.trim().to_string();
2066 if !rest.is_empty() {
2067 parts.push(rest);
2068 }
2069 parts
2070}
2071
2072pub(crate) fn escape_string_literal_ext(s: &str, unicode_escapes: bool) -> String {
2079 let mut out = String::with_capacity(s.len());
2080 for ch in s.chars() {
2081 match ch {
2082 '\\' => out.push_str("\\\\"),
2083 '"' => out.push_str("\\\""),
2084 '\n' => out.push_str("\\n"),
2085 '\r' => out.push_str("\\r"),
2086 '\t' => out.push_str("\\t"),
2087 '\0' => out.push_str("\\0"),
2088 c if c.is_control() => {
2089 if unicode_escapes {
2090 out.push_str(&format!("\\U{{{:06x}}}", c as u32));
2092 } else {
2093 out.push_str(&format!("\\x{:02x}", c as u32));
2094 }
2095 }
2096 c => out.push(c),
2097 }
2098 }
2099 out
2100}
2101
2102pub(crate) fn escape_string_literal(s: &str) -> String {
2104 escape_string_literal_ext(s, false)
2105}
2106
2107pub(crate) fn escape_string_literal_unicode(s: &str) -> String {
2109 escape_string_literal_ext(s, true)
2110}
2111
2112pub(crate) fn parse_type_annotation(ann: &str) -> Type {
2116 crate::types::parse_type_str(ann)
2117}
2118
2119pub(crate) fn is_set_type(ty: &Type) -> bool {
2125 matches!(ty, Type::Map(_, v) if matches!(v.as_ref(), Type::Unit))
2126}
2127
2128pub(crate) fn is_set_annotation(ann: &str) -> bool {
2130 is_set_type(&parse_type_annotation(ann))
2131}
2132
2133pub(crate) fn is_unit_expr_resolved(expr: &crate::ir::hir::ResolvedExpr) -> bool {
2136 matches!(
2137 expr,
2138 crate::ir::hir::ResolvedExpr::Literal(crate::ast::Literal::Unit)
2139 )
2140}
2141
2142pub(crate) fn escape_reserved_word(name: &str, reserved: &[&str], suffix: &str) -> String {
2147 if reserved.contains(&name) {
2148 format!("{}{}", name, suffix)
2149 } else {
2150 name.to_string()
2151 }
2152}
2153
2154pub(crate) fn escape_reserved_word_prefix(name: &str, reserved: &[&str], prefix: &str) -> String {
2157 if reserved.contains(&name) {
2158 format!("{}{}", prefix, name)
2159 } else {
2160 name.to_string()
2161 }
2162}
2163
2164pub(crate) fn to_lower_first(s: &str) -> String {
2168 let mut chars = s.chars();
2169 match chars.next() {
2170 None => String::new(),
2171 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
2172 }
2173}
2174
2175pub(crate) fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
2178 crate::ir::expr_to_dotted_name(expr)
2179}
2180
2181#[derive(Debug, Clone)]
2195pub(crate) enum OracleInjectionMode<'a> {
2196 LemmaBinding,
2197 LemmaBindingProjected,
2207 #[allow(dead_code)]
2208 SampleValue,
2209 SampleCaseBinding(&'a [(String, crate::ast::Spanned<Expr>)]),
2210}
2211
2212pub(crate) fn rewrite_effectful_calls_in_law<'fd, F>(
2221 expr: &crate::ast::Spanned<Expr>,
2222 law: &crate::ast::VerifyLaw,
2223 find_fn_def: F,
2224 mode: OracleInjectionMode,
2225) -> crate::ast::Spanned<Expr>
2226where
2227 F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2228{
2229 use crate::ast::{Spanned, VerifyGivenDomain};
2230
2231 let injection_by_effect: std::collections::HashMap<String, Spanned<Expr>> = law
2232 .givens
2233 .iter()
2234 .filter_map(|g| {
2235 let arg_expr = match &mode {
2236 OracleInjectionMode::LemmaBinding => {
2237 Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2238 }
2239 OracleInjectionMode::LemmaBindingProjected => {
2240 Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2248 }
2249 OracleInjectionMode::SampleValue => match &g.domain {
2250 VerifyGivenDomain::Explicit(vals) => vals.first().cloned()?,
2251 _ => return None,
2252 },
2253 OracleInjectionMode::SampleCaseBinding(case_bindings) => case_bindings
2254 .iter()
2255 .find(|(name, _)| name == &g.name)
2256 .map(|(_, v)| v.clone())?,
2257 };
2258 Some((g.type_name.clone(), arg_expr))
2259 })
2260 .collect();
2261 let rewritten = rewrite_effectful_call(expr, &injection_by_effect, find_fn_def);
2262
2263 if matches!(mode, OracleInjectionMode::LemmaBindingProjected) {
2272 let oracle_names: std::collections::HashSet<String> = law
2282 .givens
2283 .iter()
2284 .filter(|g| crate::types::checker::oracle_subtypes::has_bounded_subtype(&g.type_name))
2285 .map(|g| g.name.clone())
2286 .collect();
2287 if !oracle_names.is_empty() {
2288 return project_oracle_direct_calls(&rewritten, &oracle_names);
2289 }
2290 }
2291 rewritten
2292}
2293
2294fn project_oracle_direct_calls(
2307 expr: &crate::ast::Spanned<Expr>,
2308 oracle_names: &std::collections::HashSet<String>,
2309) -> crate::ast::Spanned<Expr> {
2310 use crate::ast::Spanned;
2311 let line = expr.line;
2312 let project_ident = |name: &str, line: usize| -> Spanned<Expr> {
2313 Spanned::new(
2314 Expr::Attr(
2315 Box::new(Spanned::new(Expr::Ident(name.to_string()), line)),
2316 "val".to_string(),
2317 ),
2318 line,
2319 )
2320 };
2321 let new_node = match &expr.node {
2322 Expr::Ident(name) if oracle_names.contains(name) => {
2326 return project_ident(name, line);
2327 }
2328 Expr::FnCall(callee, args) => {
2329 let new_args: Vec<Spanned<Expr>> = args
2330 .iter()
2331 .map(|a| project_oracle_direct_calls(a, oracle_names))
2332 .collect();
2333 let new_callee = if let Expr::Ident(name) = &callee.node
2335 && oracle_names.contains(name)
2336 {
2337 project_ident(name, callee.line)
2338 } else {
2339 project_oracle_direct_calls(callee, oracle_names)
2340 };
2341 Expr::FnCall(Box::new(new_callee), new_args)
2342 }
2343 Expr::Constructor(name, Some(arg)) => Expr::Constructor(
2344 name.clone(),
2345 Some(Box::new(project_oracle_direct_calls(arg, oracle_names))),
2346 ),
2347 Expr::Attr(obj, field) => Expr::Attr(
2348 Box::new(project_oracle_direct_calls(obj, oracle_names)),
2349 field.clone(),
2350 ),
2351 Expr::BinOp(op, l, r) => Expr::BinOp(
2352 *op,
2353 Box::new(project_oracle_direct_calls(l, oracle_names)),
2354 Box::new(project_oracle_direct_calls(r, oracle_names)),
2355 ),
2356 other => other.clone(),
2357 };
2358 Spanned::new(new_node, line)
2359}
2360
2361fn rewrite_effectful_call<'fd, F>(
2362 expr: &crate::ast::Spanned<Expr>,
2363 injection_by_effect: &std::collections::HashMap<String, crate::ast::Spanned<Expr>>,
2364 find_fn_def: F,
2365) -> crate::ast::Spanned<Expr>
2366where
2367 F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2368{
2369 use crate::ast::Spanned;
2370 use crate::types::checker::effect_classification::{EffectDimension, classify};
2371
2372 match &expr.node {
2373 Expr::FnCall(callee, args) => {
2374 let rewritten_args: Vec<Spanned<Expr>> = args
2375 .iter()
2376 .map(|a| rewrite_effectful_call(a, injection_by_effect, find_fn_def))
2377 .collect();
2378 let rewritten_callee = Box::new(rewrite_effectful_call(
2379 callee,
2380 injection_by_effect,
2381 find_fn_def,
2382 ));
2383
2384 let callee_name = match &callee.node {
2385 Expr::Ident(name) => Some(name.clone()),
2386 Expr::Resolved { name, .. } => Some(name.clone()),
2387 _ => None,
2388 };
2389
2390 if let Some(name) = callee_name
2391 && let Some(fd) = find_fn_def(&name)
2392 && !fd.effects.is_empty()
2393 && fd
2394 .effects
2395 .iter()
2396 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
2397 {
2398 let mut injected: Vec<Spanned<Expr>> = Vec::new();
2399 let needs_path = fd.effects.iter().any(|e| {
2400 matches!(
2401 classify(&e.node).map(|c| c.dimension),
2402 Some(EffectDimension::Generative | EffectDimension::GenerativeOutput)
2403 )
2404 });
2405 if needs_path {
2406 injected.push(Spanned::new(
2407 Expr::Attr(
2411 Box::new(Spanned::new(
2412 Expr::Ident("BranchPath".to_string()),
2413 expr.line,
2414 )),
2415 "Root".to_string(),
2416 ),
2417 expr.line,
2418 ));
2419 }
2420 let mut seen = std::collections::HashSet::new();
2421 for e in &fd.effects {
2422 if !seen.insert(e.node.clone()) {
2423 continue;
2424 }
2425 let Some(c) = classify(&e.node) else { continue };
2426 if matches!(c.dimension, EffectDimension::Output) {
2427 continue;
2428 }
2429 if let Some(inj) = injection_by_effect.get(&e.node) {
2430 injected.push(inj.clone());
2431 }
2432 }
2433 injected.extend(rewritten_args);
2434 return Spanned::new(Expr::FnCall(rewritten_callee, injected), expr.line);
2435 }
2436
2437 Spanned::new(Expr::FnCall(rewritten_callee, rewritten_args), expr.line)
2438 }
2439 Expr::BinOp(op, l, r) => Spanned::new(
2440 Expr::BinOp(
2441 *op,
2442 Box::new(rewrite_effectful_call(l, injection_by_effect, find_fn_def)),
2443 Box::new(rewrite_effectful_call(r, injection_by_effect, find_fn_def)),
2444 ),
2445 expr.line,
2446 ),
2447 Expr::Tuple(items) => Spanned::new(
2448 Expr::Tuple(
2449 items
2450 .iter()
2451 .map(|i| rewrite_effectful_call(i, injection_by_effect, find_fn_def))
2452 .collect(),
2453 ),
2454 expr.line,
2455 ),
2456 _ => expr.clone(),
2457 }
2458}
2459
2460pub(crate) fn verify_reachable_fn_names(items: &[TopLevel]) -> HashSet<String> {
2470 let mut reachable: HashSet<String> = HashSet::new();
2471 for item in items {
2472 if let TopLevel::Verify(vb) = item {
2473 collect_verify_block_refs(vb, &mut reachable);
2474 }
2475 }
2476 loop {
2478 let mut changed = false;
2479 for item in items {
2480 if let TopLevel::FnDef(fd) = item
2481 && reachable.contains(&fd.name)
2482 {
2483 let mut called = HashSet::new();
2484 collect_called_idents_in_body(&fd.body, &mut called);
2485 for name in called {
2486 if reachable.insert(name) {
2487 changed = true;
2488 }
2489 }
2490 }
2491 }
2492 if !changed {
2493 break;
2494 }
2495 }
2496 reachable
2497}
2498
2499fn collect_verify_block_refs(vb: &VerifyBlock, out: &mut HashSet<String>) {
2500 out.insert(vb.fn_name.clone());
2501 for (lhs, rhs) in &vb.cases {
2502 collect_called_idents(lhs, out);
2503 collect_called_idents(rhs, out);
2504 }
2505 if let VerifyKind::Law(law) = &vb.kind {
2506 collect_called_idents(&law.lhs, out);
2507 collect_called_idents(&law.rhs, out);
2508 if let Some(when) = &law.when {
2509 collect_called_idents(when, out);
2510 }
2511 for given in &law.givens {
2512 if let VerifyGivenDomain::Explicit(values) = &given.domain {
2513 for v in values {
2514 collect_called_idents(v, out);
2515 }
2516 }
2517 }
2518 }
2519 for given in &vb.cases_givens {
2520 if let VerifyGivenDomain::Explicit(values) = &given.domain {
2521 for v in values {
2522 collect_called_idents(v, out);
2523 }
2524 }
2525 }
2526}
2527
2528fn collect_called_idents_in_body(body: &FnBody, out: &mut HashSet<String>) {
2529 for stmt in body.stmts() {
2530 match stmt {
2531 Stmt::Binding(_, _, e) | Stmt::Expr(e) => collect_called_idents(e, out),
2532 }
2533 }
2534}
2535
2536fn collect_called_idents(expr: &Spanned<Expr>, out: &mut HashSet<String>) {
2537 match &expr.node {
2538 Expr::FnCall(callee, args) => {
2539 if let Expr::Ident(name) | Expr::Resolved { name, .. } = &callee.node {
2540 out.insert(name.clone());
2541 } else {
2542 collect_called_idents(callee, out);
2543 }
2544 for a in args {
2545 collect_called_idents(a, out);
2546 }
2547 }
2548 Expr::TailCall(boxed) => {
2549 let TailCallData { target, args, .. } = boxed.as_ref();
2550 out.insert(target.clone());
2551 for a in args {
2552 collect_called_idents(a, out);
2553 }
2554 }
2555 Expr::Ident(name) | Expr::Resolved { name, .. } => {
2556 out.insert(name.clone());
2557 }
2558 Expr::BinOp(_, l, r) => {
2559 collect_called_idents(l, out);
2560 collect_called_idents(r, out);
2561 }
2562 Expr::Neg(inner) => collect_called_idents(inner, out),
2563 Expr::Match { subject, arms, .. } => {
2564 collect_called_idents(subject, out);
2565 for arm in arms {
2566 collect_called_idents(&arm.body, out);
2567 }
2568 }
2569 Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
2570 collect_called_idents(inner, out);
2571 }
2572 Expr::Constructor(_, Some(inner)) => {
2573 collect_called_idents(inner, out);
2574 }
2575 Expr::InterpolatedStr(parts) => {
2576 for part in parts {
2577 if let StrPart::Parsed(inner) = part {
2578 collect_called_idents(inner, out);
2579 }
2580 }
2581 }
2582 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
2583 for i in items {
2584 collect_called_idents(i, out);
2585 }
2586 }
2587 Expr::MapLiteral(entries) => {
2588 for (k, v) in entries {
2589 collect_called_idents(k, out);
2590 collect_called_idents(v, out);
2591 }
2592 }
2593 Expr::RecordCreate { fields, .. } => {
2594 for (_, v) in fields {
2595 collect_called_idents(v, out);
2596 }
2597 }
2598 Expr::RecordUpdate { base, updates, .. } => {
2599 collect_called_idents(base, out);
2600 for (_, v) in updates {
2601 collect_called_idents(v, out);
2602 }
2603 }
2604 Expr::Literal(_) | Expr::Constructor(_, None) => {}
2605 }
2606}
2607
2608pub(crate) struct PerScopeSections {
2612 pub by_scope: std::collections::HashMap<String, Vec<String>>,
2613}
2614
2615impl PerScopeSections {
2616 pub(crate) fn take(&mut self, scope: &str) -> Vec<String> {
2617 self.by_scope.remove(scope).unwrap_or_default()
2618 }
2619}
2620
2621pub(crate) fn route_pure_components_per_scope<F, G>(
2630 ctx: &CodegenContext,
2631 is_pure: F,
2632 mut emit: G,
2633) -> PerScopeSections
2634where
2635 F: Fn(&FnDef) -> bool,
2636 G: FnMut(&[&FnDef], &str) -> Vec<String>,
2637{
2638 let mut by_scope: std::collections::HashMap<String, Vec<String>> =
2639 std::collections::HashMap::new();
2640
2641 let mut process =
2642 |fns: Vec<&FnDef>,
2643 scope: String,
2644 by_scope: &mut std::collections::HashMap<String, Vec<String>>| {
2645 let comps = crate::call_graph::ordered_fn_components(&fns, &ctx.module_prefixes);
2646 let bucket = by_scope.entry(scope.clone()).or_default();
2647 for comp in comps {
2648 bucket.extend(emit(&comp, scope.as_str()));
2649 }
2650 };
2651
2652 for module in &ctx.modules {
2653 let pure: Vec<&FnDef> = module.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2654 process(pure, module.prefix.clone(), &mut by_scope);
2655 }
2656 let entry_pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2657 process(entry_pure, String::new(), &mut by_scope);
2658
2659 PerScopeSections { by_scope }
2660}
2661
2662#[cfg(test)]
2663mod tests {
2664 use super::*;
2665 use crate::ast::{Literal, VerifyGiven, VerifyGivenDomain, VerifyLaw};
2666
2667 fn sb(node: Expr) -> Spanned<Expr> {
2668 Spanned::new(node, 1)
2669 }
2670
2671 fn bsb(node: Expr) -> Box<Spanned<Expr>> {
2672 Box::new(sb(node))
2673 }
2674
2675 fn law_with(lhs: Spanned<Expr>, rhs: Spanned<Expr>) -> VerifyLaw {
2676 VerifyLaw {
2677 name: "test".to_string(),
2678 givens: vec![VerifyGiven {
2679 name: "xs".to_string(),
2680 type_name: "List<String>".to_string(),
2681 domain: VerifyGivenDomain::Explicit(vec![sb(Expr::List(vec![]))]),
2682 }],
2683 when: None,
2684 lhs,
2685 rhs,
2686 sample_guards: Vec::new(),
2687 }
2688 }
2689
2690 #[test]
2691 fn law_calls_unclassified_fn_detects_dotted_callee() {
2692 let lhs = sb(Expr::FnCall(
2703 bsb(Expr::Attr(
2704 bsb(Expr::Ident("Dep".to_string())),
2705 "toSorted".to_string(),
2706 )),
2707 vec![sb(Expr::Ident("xs".to_string()))],
2708 ));
2709 let rhs = sb(Expr::Ident("xs".to_string()));
2710 let law = law_with(lhs, rhs);
2711
2712 let mut canonical = HashSet::new();
2715 canonical.insert("Dep.toSorted".to_string());
2716 assert!(law_calls_unclassified_fn(&law, &canonical));
2717
2718 let mut bare_only = HashSet::new();
2721 bare_only.insert("toSorted".to_string());
2722 assert!(
2723 law_calls_unclassified_fn(&law, &bare_only),
2724 "bare unclassified name must catch a dotted callsite via suffix match"
2725 );
2726
2727 let mut unrelated = HashSet::new();
2730 unrelated.insert("somethingElse".to_string());
2731 assert!(!law_calls_unclassified_fn(&law, &unrelated));
2732 }
2733
2734 #[test]
2735 fn law_lhs_has_trace_projection_skips_user_record_field() {
2736 let user_field_lhs = sb(Expr::Attr(
2744 bsb(Expr::Ident("log".to_string())),
2745 "trace".to_string(),
2746 ));
2747 assert!(
2748 !law_lhs_has_trace_projection(&user_field_lhs),
2749 "bare user-record `.trace` field must not trigger the gate"
2750 );
2751
2752 let runtime_trace_lhs = sb(Expr::FnCall(
2754 bsb(Expr::Attr(
2755 bsb(Expr::Attr(
2756 bsb(Expr::FnCall(bsb(Expr::Ident("fn".to_string())), vec![])),
2757 "trace".to_string(),
2758 )),
2759 "event".to_string(),
2760 )),
2761 vec![sb(Expr::Literal(Literal::Int(0)))],
2762 ));
2763 assert!(
2764 law_lhs_has_trace_projection(&runtime_trace_lhs),
2765 "Oracle `.trace.event(0)` projection must trigger the gate"
2766 );
2767 }
2768
2769 #[test]
2770 fn resolve_refined_type_disambiguates_cross_module_same_bare_name() {
2771 use crate::ast::{TypeDef, TypeVariant};
2780 use crate::codegen::ModuleInfo;
2781 use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
2782 use std::collections::HashMap;
2783 let _ = TypeVariant {
2784 name: String::new(),
2785 fields: Vec::new(),
2786 }; let make_module = |prefix: &str| ModuleInfo {
2789 prefix: prefix.to_string(),
2790 depends: Vec::new(),
2791 type_defs: vec![TypeDef::Product {
2792 name: "Natural".to_string(),
2793 fields: vec![("value".to_string(), "Int".to_string())],
2794 line: 1,
2795 }],
2796 fn_defs: Vec::new(),
2797 analysis: None,
2798 };
2799 let modules = vec![make_module("A"), make_module("B")];
2800
2801 let make_decl = |predicate_param: &str, witness: i64| RefinedTypeDecl {
2802 name: "Natural".to_string(),
2803 carrier_type: "Int".to_string(),
2804 carrier_field: "value".to_string(),
2805 predicate_param: predicate_param.to_string(),
2806 invariant: Predicate {
2807 free_vars: vec![(
2808 predicate_param.to_string(),
2809 QuantifierType::Plain("Int".to_string()),
2810 )],
2811 expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
2812 Literal::Bool(true),
2813 )),
2814 },
2815 witness: Some(witness.to_string()),
2816 };
2817 let symbols = crate::ir::SymbolTable::build(&[], &modules);
2818 let a_id = symbols
2819 .type_id_of(&crate::ir::TypeKey::in_module("A", "Natural"))
2820 .expect("A.Natural TypeId");
2821 let b_id = symbols
2822 .type_id_of(&crate::ir::TypeKey::in_module("B", "Natural"))
2823 .expect("B.Natural TypeId");
2824
2825 let mut refined_types: HashMap<crate::ir::TypeId, RefinedTypeDecl> = HashMap::new();
2826 refined_types.insert(a_id, make_decl("a", 0));
2827 refined_types.insert(b_id, make_decl("b", 10));
2828
2829 let a = resolve_refined_type_in(&refined_types, &symbols, &modules, "A.Natural")
2831 .expect("A.Natural canonical lookup");
2832 assert_eq!(a.predicate_param, "a");
2833 assert_eq!(a.witness.as_deref(), Some("0"));
2834 let b = resolve_refined_type_in(&refined_types, &symbols, &modules, "B.Natural")
2835 .expect("B.Natural canonical lookup");
2836 assert_eq!(b.predicate_param, "b");
2837 assert_eq!(b.witness.as_deref(), Some("10"));
2838
2839 let bare = resolve_refined_type_in(&refined_types, &symbols, &modules, "Natural")
2843 .expect("bare Natural resolves via module walk");
2844 assert!(
2845 bare.predicate_param == "a" || bare.predicate_param == "b",
2846 "bare Natural must resolve to one of the canonical decls"
2847 );
2848
2849 assert!(resolve_refined_type_in(&refined_types, &symbols, &modules, "Unrelated").is_none());
2852 }
2853
2854 #[test]
2855 fn find_refined_type_scoped_prefers_current_module_over_entry_collision() {
2856 use crate::ast::{TopLevel, TypeDef};
2864 use crate::codegen::{CodegenContext, ModuleInfo};
2865 use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
2866 use std::collections::{HashMap, HashSet};
2867
2868 let entry_natural = TypeDef::Product {
2869 name: "Natural".to_string(),
2870 fields: vec![("value".to_string(), "Int".to_string())],
2871 line: 1,
2872 };
2873 let module = ModuleInfo {
2874 prefix: "Mod".to_string(),
2875 depends: Vec::new(),
2876 type_defs: vec![TypeDef::Product {
2877 name: "Natural".to_string(),
2878 fields: vec![("value".to_string(), "Int".to_string())],
2879 line: 1,
2880 }],
2881 fn_defs: Vec::new(),
2882 analysis: None,
2883 };
2884
2885 let make_decl = |param: &str, witness: &str| RefinedTypeDecl {
2886 name: "Natural".to_string(),
2887 carrier_type: "Int".to_string(),
2888 carrier_field: "value".to_string(),
2889 predicate_param: param.to_string(),
2890 invariant: Predicate {
2891 free_vars: vec![(param.to_string(), QuantifierType::Plain("Int".to_string()))],
2892 expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
2893 Literal::Bool(true),
2894 )),
2895 },
2896 witness: Some(witness.to_string()),
2897 };
2898
2899 let items = vec![TopLevel::TypeDef(entry_natural)];
2900 let modules = vec![module];
2901 let symbol_table = crate::ir::SymbolTable::build(&items, &modules);
2902 let entry_id = symbol_table
2903 .type_id_of(&crate::ir::TypeKey::entry("Natural"))
2904 .expect("entry Natural id");
2905 let mod_id = symbol_table
2906 .type_id_of(&crate::ir::TypeKey::in_module("Mod", "Natural"))
2907 .expect("Mod.Natural id");
2908
2909 let mut ctx = CodegenContext {
2910 items,
2911 type_defs: Vec::new(),
2912 fn_defs: Vec::new(),
2913 project_name: "scope-test".to_string(),
2914 modules,
2915 module_prefixes: HashSet::new(),
2916 #[cfg(feature = "runtime")]
2917 policy: None,
2918 emit_replay_runtime: false,
2919 runtime_policy_from_env: false,
2920 guest_entry: None,
2921 emit_self_host_support: false,
2922 extra_fn_defs: Vec::new(),
2923 mutual_tco_members: HashSet::new(),
2924 recursive_fns: HashSet::new(),
2925 buffer_build_sinks: HashMap::new(),
2926 buffer_fusion_sites: Vec::new(),
2927 synthesized_buffered_fns: Vec::new(),
2928 proof_ir: crate::ir::ProofIR::default(),
2929 symbol_table,
2930 resolved_fn_defs: Vec::new(),
2931 resolved_module_fn_defs: Vec::new(),
2932 current_module_scope: std::cell::RefCell::new(None),
2933 resolved_program: crate::codegen::program_view::ResolvedProgramView::default(),
2934 program_shape: None,
2935 mir_program: None,
2936 };
2937 ctx.proof_ir
2938 .refined_types
2939 .insert(entry_id, make_decl("entry_n", "0"));
2940 ctx.proof_ir
2941 .refined_types
2942 .insert(mod_id, make_decl("mod_n", "10"));
2943
2944 let from_module = find_refined_type_scoped(&ctx, "Natural", Some("Mod"))
2945 .expect("Mod-scoped Natural lookup");
2946 assert_eq!(
2947 from_module.predicate_param, "mod_n",
2948 "scope=Some(\"Mod\") + bare `Natural` must resolve to Mod.Natural, \
2949 not entry's bare-keyed slot"
2950 );
2951
2952 let from_entry =
2954 find_refined_type_scoped(&ctx, "Natural", None).expect("entry Natural lookup");
2955 assert_eq!(from_entry.predicate_param, "entry_n");
2956 }
2957}