1mod expr;
8mod fuel;
9mod toplevel;
10
11use crate::ast::{FnDef, TopLevel, VerifyKind};
12use crate::codegen::{CodegenContext, ProjectOutput};
13
14fn body_uses_error_prop(body: &std::sync::Arc<crate::ast::FnBody>) -> bool {
17 match body.as_ref() {
18 crate::ast::FnBody::Block(stmts) => stmts.iter().any(|s| match s {
19 crate::ast::Stmt::Binding(_, _, expr) => expr_uses_error_prop(expr),
20 crate::ast::Stmt::Expr(expr) => expr_uses_error_prop(expr),
21 }),
22 }
23}
24
25fn expr_uses_error_prop(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
26 use crate::ast::Expr;
27 match &expr.node {
28 Expr::ErrorProp(_) => true,
29 Expr::FnCall(f, args) => expr_uses_error_prop(f) || args.iter().any(expr_uses_error_prop),
30 Expr::BinOp(_, l, r) => expr_uses_error_prop(l) || expr_uses_error_prop(r),
31 Expr::Match { subject, arms, .. } => {
32 expr_uses_error_prop(subject) || arms.iter().any(|a| expr_uses_error_prop(&a.body))
33 }
34 Expr::Constructor(_, Some(arg)) => expr_uses_error_prop(arg),
35 Expr::List(elems) | Expr::Tuple(elems) => elems.iter().any(expr_uses_error_prop),
36 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_uses_error_prop(e)),
37 Expr::RecordUpdate { base, updates, .. } => {
38 expr_uses_error_prop(base) || updates.iter().any(|(_, e)| expr_uses_error_prop(e))
39 }
40 Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
41 crate::ast::StrPart::Parsed(e) => expr_uses_error_prop(e),
42 _ => false,
43 }),
44 Expr::Attr(obj, _) => expr_uses_error_prop(obj),
45 Expr::TailCall(inner) => inner.args.iter().any(expr_uses_error_prop),
46 _ => false,
47 }
48}
49
50pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
52 transpile_unified(ctx)
53}
54
55pub(crate) fn dafny_module_name(prefix: &str) -> String {
66 format!("Aver_{}", prefix.replace('.', "_"))
67}
68
69fn transpile_unified(ctx: &CodegenContext) -> ProjectOutput {
74 use std::collections::{HashMap, HashSet};
75
76 let is_mutual_lex = |fd: &FnDef| -> bool {
87 crate::codegen::common::find_fn_contract_for_fn(ctx, fd).is_some_and(|c| {
88 matches!(
89 c.recursion,
90 Some(crate::ir::RecursionContract::Fuel {
91 fuel_metric: crate::ir::FuelMetric::Lex { .. },
92 })
93 )
94 })
95 };
96
97 let mutual_fns_all: Vec<&FnDef> = ctx
98 .items
99 .iter()
100 .filter_map(|it| {
101 if let TopLevel::FnDef(fd) = it {
102 Some(fd)
103 } else {
104 None
105 }
106 })
107 .chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
108 .filter(|fd| is_mutual_lex(fd))
109 .collect();
110 let mutual_components =
111 crate::call_graph::ordered_fn_components(&mutual_fns_all, &ctx.module_prefixes);
112
113 let mut fuel_per_scope: HashMap<String, Vec<String>> = HashMap::new();
114 let mut fuel_emitted: HashSet<crate::ir::FnId> = HashSet::new();
120 let mut native_emitted: HashSet<crate::ir::FnId> = HashSet::new();
121 let mut axiom_fn_ids: HashSet<crate::ir::FnId> = HashSet::new();
122
123 let insert_fn_ids = |set: &mut HashSet<crate::ir::FnId>, fns: &[&FnDef]| {
124 for fd in fns {
125 if let Some(id) = crate::codegen::common::fn_id_for_decl(ctx, fd) {
126 set.insert(id);
127 }
128 }
129 };
130
131 for component in &mutual_components {
132 let scc_fns: Vec<&FnDef> = component.iter().map(|fd| &**fd).collect();
133 let scope = scc_fns
134 .first()
135 .and_then(|fd| crate::codegen::common::fn_owning_scope_for(ctx, fd))
136 .map(|s| s.to_string())
137 .unwrap_or_default();
138 let scope_opt = if scope.is_empty() {
146 None
147 } else {
148 Some(scope.as_str())
149 };
150 ctx.with_module_scope(scope_opt, || {
151 if let Some(code) = fuel::emit_mutual_native_decreases_group(&scc_fns, ctx) {
159 fuel_per_scope.entry(scope.clone()).or_default().push(code);
160 insert_fn_ids(&mut native_emitted, &scc_fns);
161 } else {
162 match fuel::emit_mutual_fuel_group(&scc_fns, ctx) {
163 Some(code) => {
164 fuel_per_scope.entry(scope.clone()).or_default().push(code);
165 insert_fn_ids(&mut fuel_emitted, &scc_fns);
166 }
167 None => {
168 insert_fn_ids(&mut axiom_fn_ids, &scc_fns);
169 }
170 }
171 }
172 });
173 }
174
175 let needs_axiom_for_error_prop = |fd: &FnDef| -> bool {
176 body_uses_error_prop(&fd.body)
177 && crate::types::checker::effect_lifting::lower_pure_question_bang_fn(fd)
178 .ok()
179 .flatten()
180 .is_none()
181 };
182 let id_in = |set: &HashSet<crate::ir::FnId>, fd: &FnDef| -> bool {
183 crate::codegen::common::fn_id_for_decl(ctx, fd).is_some_and(|id| set.contains(&id))
184 };
185 let emit_pure_or_axiom = |fd: &FnDef| -> String {
186 if needs_axiom_for_error_prop(fd) {
187 toplevel::emit_fn_def_axiom(fd, ctx)
188 } else if id_in(&fuel_emitted, fd) || id_in(&native_emitted, fd) {
189 String::new()
190 } else if id_in(&axiom_fn_ids, fd) {
191 toplevel::emit_fn_def_axiom(fd, ctx)
192 } else {
193 toplevel::emit_fn_def(fd, ctx)
194 }
195 };
196
197 let mut pure_per_scope = crate::codegen::common::route_pure_components_per_scope(
205 ctx,
206 |fd| fd.effects.is_empty() && fd.name != "main",
207 |comp, scope| {
208 let scope_opt = if scope.is_empty() { None } else { Some(scope) };
209 ctx.with_module_scope(scope_opt, || {
210 comp.iter()
211 .map(|fd| emit_pure_or_axiom(fd))
212 .filter(|s| !s.is_empty())
213 .collect()
214 })
215 },
216 );
217
218 let mut module_files: Vec<(String, String)> = Vec::new();
219 let mut union_body = String::new();
220
221 for module in &ctx.modules {
223 let mut sections: Vec<String> = Vec::new();
224 ctx.with_module_scope(Some(module.prefix.as_str()), || {
225 for td in &module.type_defs {
226 if let Some(code) =
227 toplevel::emit_type_def_in_scope(td, ctx, Some(module.prefix.as_str()))
228 {
229 sections.push(code);
230 }
231 }
232 });
233 sections.extend(pure_per_scope.take(&module.prefix));
234 if let Some(fuel) = fuel_per_scope.get(&module.prefix) {
235 sections.extend(fuel.clone());
236 }
237 let body = sections.join("\n");
238 union_body.push_str(&body);
239 union_body.push('\n');
240
241 let depth = module.prefix.chars().filter(|c| *c == '.').count();
246 let up = "../".repeat(depth);
247 let depends_includes: String = module
248 .depends
249 .iter()
250 .map(|d| {
251 format!(
252 "include \"{}{}.dfy\"",
253 up,
254 crate::codegen::common::module_prefix_to_filename(d)
255 )
256 })
257 .collect::<Vec<_>>()
258 .join("\n");
259 let depends_imports: String = module
260 .depends
261 .iter()
262 .map(|d| format!(" import opened {}", dafny_module_name(d)))
263 .collect::<Vec<_>>()
264 .join("\n");
265
266 let mut header = format!(
267 "// Aver-generated module: {}\ninclude \"{}common.dfy\"\n",
268 module.prefix, up
269 );
270 if !depends_includes.is_empty() {
271 header.push_str(&depends_includes);
272 header.push('\n');
273 }
274
275 let mut module_inner = String::from(" import opened AverCommon\n");
276 if !depends_imports.is_empty() {
277 module_inner.push_str(&depends_imports);
278 module_inner.push('\n');
279 }
280 module_inner.push('\n');
281 for line in body.lines() {
282 if line.is_empty() {
283 module_inner.push('\n');
284 } else {
285 module_inner.push_str(" ");
286 module_inner.push_str(line);
287 module_inner.push('\n');
288 }
289 }
290
291 let content = format!(
292 "{}\nmodule {} {{\n{}}}\n",
293 header,
294 dafny_module_name(&module.prefix),
295 module_inner
296 );
297 let path = module.prefix.replace('.', "/");
298 module_files.push((format!("{}.dfy", path), content));
299 }
300
301 let mut entry_sections: Vec<String> = Vec::new();
303 for td in &ctx.type_defs {
304 if let Some(code) = toplevel::emit_type_def(td, ctx) {
305 entry_sections.push(code);
306 }
307 }
308 entry_sections.extend(pure_per_scope.take(""));
314 if let Some(fuel) = fuel_per_scope.get("") {
315 entry_sections.extend(fuel.clone());
316 }
317
318 let reachable = crate::codegen::common::verify_reachable_fn_names(&ctx.items);
321 let mut helpers: HashMap<String, Vec<String>> = HashMap::new();
322 for item in &ctx.items {
323 if let TopLevel::FnDef(fd) = item
324 && !fd.effects.is_empty()
325 && fd.name != "main"
326 && !body_uses_error_prop(&fd.body)
327 && reachable.contains(&fd.name)
328 && fd
329 .effects
330 .iter()
331 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
332 {
333 helpers.insert(
334 fd.name.clone(),
335 fd.effects.iter().map(|e| e.node.clone()).collect(),
336 );
337 }
338 }
339 for item in &ctx.items {
340 if let TopLevel::FnDef(fd) = item
341 && !fd.effects.is_empty()
342 && fd.name != "main"
343 && !body_uses_error_prop(&fd.body)
344 && reachable.contains(&fd.name)
345 && fd
346 .effects
347 .iter()
348 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
349 && let Ok(Some(lifted)) =
350 crate::types::checker::effect_lifting::lift_fn_def_with_helpers(fd, &helpers)
351 {
352 entry_sections.push(toplevel::emit_fn_def(&lifted, ctx));
353 }
354 }
355
356 let mut law_counter: HashMap<String, usize> = HashMap::new();
358 for item in &ctx.items {
359 if let TopLevel::Verify(vb) = item
360 && let VerifyKind::Law(law) = &vb.kind
361 {
362 let count = law_counter.entry(vb.fn_name.clone()).or_insert(0);
363 *count += 1;
364 let suffix = if *count > 1 {
365 format!("_{}", count)
366 } else {
367 String::new()
368 };
369 let direct_opaque: HashSet<crate::ir::FnId> =
370 axiom_fn_ids.union(&fuel_emitted).copied().collect();
371 let opaque_fns = toplevel::transitive_opaque_closure(ctx, &direct_opaque);
372 let native_transitive = toplevel::transitive_opaque_closure(ctx, &native_emitted);
378 if !vb.cases.is_empty()
379 && let Some(code) = toplevel::emit_law_samples(
380 vb,
381 law,
382 ctx,
383 &suffix,
384 &opaque_fns,
385 &fuel_emitted,
386 &native_transitive,
387 )
388 {
389 entry_sections.push(code);
390 }
391 entry_sections.push(toplevel::emit_verify_law(
392 vb,
393 law,
394 ctx,
395 &opaque_fns,
396 &native_transitive,
397 &suffix,
398 ));
399 }
400 }
401
402 let entry_body = entry_sections.join("\n");
403 union_body.push_str(&entry_body);
404 union_body.push('\n');
405
406 let entry_includes: String = ctx
407 .modules
408 .iter()
409 .map(|m| {
410 format!(
411 "include \"{}.dfy\"",
412 crate::codegen::common::module_prefix_to_filename(&m.prefix)
413 )
414 })
415 .collect::<Vec<_>>()
416 .join("\n");
417 let entry_name = crate::codegen::common::entry_basename(ctx);
418 let mut entry_parts: Vec<String> = vec![format!(
419 "// Aver-generated entry: {}\ninclude \"common.dfy\"\n{}",
420 entry_name, entry_includes
421 )];
422 let mut opens = vec!["import opened AverCommon".to_string()];
425 for m in &ctx.modules {
426 opens.push(format!("import opened {}", dafny_module_name(&m.prefix)));
427 }
428 entry_parts.push(opens.join("\n"));
429 let declared = crate::codegen::common::collect_declared_effects(ctx);
430 let has_ip = union_body.contains("BranchPath");
431 let has_classified =
432 crate::types::checker::effect_classification::classifications_for_proof_subset()
433 .iter()
434 .any(|c| declared.includes(c.method));
435 if has_ip || has_classified {
436 entry_parts.push(
437 crate::types::checker::proof_trust_header::generate_commented("// ", &declared, has_ip),
438 );
439 }
440 let subtype_block = crate::types::checker::oracle_subtypes::dafny_subtype_predicates(&declared);
441 if !subtype_block.is_empty() {
442 union_body.push_str(&subtype_block);
452 union_body.push('\n');
453 entry_parts.push(subtype_block);
454 }
455 entry_parts.push(entry_body);
456 let entry_content = entry_parts.join("\n\n");
457
458 let common_content = build_common_dafny(&union_body);
460
461 let mut files = module_files;
462 files.push((format!("{}.dfy", entry_name), entry_content));
463 files.push(("common.dfy".to_string(), common_content));
464 ProjectOutput { files }
465}
466
467fn build_common_dafny(union_body: &str) -> String {
468 let mut sections: Vec<String> = vec![
469 "// Aver-generated shared library: built-in records and helpers".to_string(),
470 "module AverCommon {".to_string(),
471 DAFNY_PRELUDE_HEAD.to_string(),
472 ];
473 for record in crate::codegen::builtin_records::needed_records(union_body, false) {
474 sections.push(crate::codegen::builtin_records::render_dafny(record));
475 }
476 sections.push(DAFNY_PRELUDE_CORE_HELPERS.to_string());
477 for helper in crate::codegen::builtin_helpers::needed_helpers(union_body, false) {
478 match helper.key {
479 "BranchPath" => sections.push(DAFNY_HELPER_BRANCH_PATH.to_string()),
480 "AverList" => sections.push(DAFNY_HELPER_AVER_LIST.to_string()),
481 "StringHelpers" => sections.push(DAFNY_HELPER_STRING_HELPERS.to_string()),
482 "NumericParse" => sections.push(DAFNY_HELPER_NUMERIC_PARSE.to_string()),
483 "CharByte" => sections.push(DAFNY_HELPER_CHAR_BYTE.to_string()),
484 "AverMap" => sections.push(DAFNY_HELPER_AVER_MAP.to_string()),
485 "AverMeasure" | "ProofFuel" => {}
486 "FloatInstances" | "ExceptInstances" | "StringHadd" => {}
487 "ResultDatatype" => sections.push(DAFNY_HELPER_RESULT_DATATYPE.to_string()),
488 "OptionDatatype" => sections.push(DAFNY_HELPER_OPTION_DATATYPE.to_string()),
489 "OptionToResult" => sections.push(DAFNY_HELPER_OPTION_TO_RESULT.to_string()),
490 "BranchPathDatatype" => sections.push(DAFNY_HELPER_BRANCH_PATH_DATATYPE.to_string()),
491 other => panic!(
492 "Dafny backend has no implementation for builtin helper key '{}'. \
493 Add a match arm in build_common_dafny or remove the key from BUILTIN_HELPERS.",
494 other
495 ),
496 }
497 }
498 sections.push("}".to_string());
499 sections.join("\n")
500}
501
502const DAFNY_PRELUDE_HEAD: &str = r#"// --- Prelude: standard types and helpers ---
503"#;
504
505const DAFNY_HELPER_RESULT_DATATYPE: &str = r#"
506datatype Result<T, E> = Ok(value: T) | Err(error: E)
507
508function ResultWithDefault<T, E>(r: Result<T, E>, d: T): T {
509 match r
510 case Ok(v) => v
511 case Err(_) => d
512}
513"#;
514
515const DAFNY_HELPER_OPTION_DATATYPE: &str = r#"
516datatype Option<T> = None | Some(value: T)
517
518function OptionWithDefault<T>(o: Option<T>, d: T): T {
519 match o
520 case Some(v) => v
521 case None => d
522}
523"#;
524
525const DAFNY_HELPER_OPTION_TO_RESULT: &str = r#"
526function OptionToResult<T, E>(o: Option<T>, err: E): Result<T, E> {
527 match o
528 case Some(v) => Result.Ok(v)
529 case None => Result.Err(err)
530}
531"#;
532
533const DAFNY_HELPER_BRANCH_PATH_DATATYPE: &str = r#"
534// Oracle v1: BranchPath is the proof-side representation of a position
535// in the structural tree of `!`/`?!` groups. Dewey-decimal under the hood
536// ("", "0", "2.0", …); constructors mirror the Aver-source BranchPath
537// opaque builtin (`.root`, `.child`, `.parse`) so the lifted bodies can
538// reference them directly without case-splitting at the call site.
539
540datatype BranchPath = BranchPath(dewey: string)
541"#;
542
543const DAFNY_PRELUDE_CORE_HELPERS: &str = r#"
547function ToString<T>(v: T): string
548"#;
549
550const DAFNY_HELPER_BRANCH_PATH: &str = r#"
557const BranchPath_Root: BranchPath := BranchPath("")
558
559function BranchPath_child(p: BranchPath, idx: int): BranchPath
560 requires idx >= 0
561{
562 if |p.dewey| == 0 then BranchPath(IntToString(idx))
563 else BranchPath(p.dewey + "." + IntToString(idx))
564}
565
566function BranchPath_parse(s: string): BranchPath {
567 BranchPath(s)
568}
569"#;
570
571const DAFNY_HELPER_AVER_LIST: &str = r#"
572function ListReverse<T>(xs: seq<T>): seq<T>
573 decreases |xs|
574{
575 if |xs| == 0 then []
576 else ListReverse(xs[1..]) + [xs[0]]
577}
578
579function ListHead<T>(xs: seq<T>): Option<T> {
580 if |xs| == 0 then None
581 else Some(xs[0])
582}
583
584function ListTail<T>(xs: seq<T>): seq<T> {
585 if |xs| == 0 then []
586 else xs[1..]
587}
588
589function ListTake<T>(xs: seq<T>, n: int): seq<T> {
590 if n <= 0 then []
591 else if n >= |xs| then xs
592 else xs[..n]
593}
594
595function ListDrop<T>(xs: seq<T>, n: int): seq<T> {
596 if n <= 0 then xs
597 else if n >= |xs| then []
598 else xs[n..]
599}
600"#;
601
602const DAFNY_HELPER_AVER_MAP: &str = r#"
603function MapGet<K, V>(m: map<K, V>, k: K): Option<V> {
604 if k in m then Some(m[k])
605 else None
606}
607
608function MapEntries<K, V>(m: map<K, V>): seq<(K, V)>
609function MapFromList<K, V>(entries: seq<(K, V)>): map<K, V>
610 decreases |entries|
611{
612 if |entries| == 0 then map[]
613 else MapFromList(entries[..|entries|-1])[entries[|entries|-1].0 := entries[|entries|-1].1]
614}
615"#;
616
617const DAFNY_HELPER_STRING_HELPERS: &str = r#"
623function StringCharAt(s: string, i: int): Option<string> {
624 if 0 <= i < |s| then Option.Some([s[i]]) else Option.None
625}
626
627function StringChars(s: string): seq<string> {
628 seq(|s|, (i: int) requires 0 <= i < |s| => [s[i]])
629}
630
631function StringSlice(s: string, from_: int, to_: int): string
632{
633 var lo := if from_ < 0 then 0 else if from_ > |s| then |s| else from_;
634 var hi := if to_ < 0 then 0 else if to_ > |s| then |s| else to_;
635 if lo >= hi then "" else s[lo..hi]
636}
637
638function StringJoin(sep: string, parts: seq<string>): string
639 decreases |parts|
640{
641 if |parts| == 0 then ""
642 else if |parts| == 1 then parts[0]
643 else parts[0] + sep + StringJoin(sep, parts[1..])
644}
645
646function StringSplit(s: string, sep: string): seq<string>
647function StringContains(s: string, sub: string): bool
648function StringStartsWith(s: string, prefix: string): bool
649function StringEndsWith(s: string, suffix: string): bool
650function StringTrim(s: string): string
651function StringReplace(s: string, from_: string, to_: string): string
652function StringRepeat(s: string, n: int): string
653function StringIndexOf(s: string, sub: string): int
654function StringToUpper(s: string): string
655function StringToLower(s: string): string
656function StringFromBool(b: bool): string
657function StringByteLength(s: string): int
658
659function ListReverseStr(xs: seq<string>): seq<string>
660"#;
661
662const DAFNY_HELPER_NUMERIC_PARSE: &str = r#"
663function IntToString(n: int): string
664function IntFromString(s: string): Result<int, string>
665function FloatToString(r: real): string
666function FloatFromString(s: string): Result<real, string>
667function FloatPi(): real
668function FloatSqrt(r: real): real
669function FloatPow(base: real, exp: real): real
670function FloatToInt(r: real): int
671function FloatSin(r: real): real
672function FloatCos(r: real): real
673function FloatAtan2(y: real, x: real): real
674
675function FloatDiv(a: real, b: real): real
676{
677 if b == 0.0 then 0.0 else a / b
678}
679"#;
680
681const DAFNY_HELPER_CHAR_BYTE: &str = r#"
682function CharToCode(c: string): int
683function CharFromCode(n: int): Option<string>
684function ByteToHex(b: int): Result<string, string>
685function ByteFromHex(s: string): Result<int, string>
686"#;
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use crate::codegen::build_context;
692 use crate::source::parse_source;
693
694 fn ctx_from_source(src: &str, project_name: &str) -> CodegenContext {
695 let mut items = parse_source(src).expect("parse");
696 let pipeline_result = crate::ir::pipeline::run(
702 &mut items,
703 crate::ir::PipelineConfig {
704 run_tco: true,
705 typecheck: Some(crate::ir::TypecheckMode::Full { base_dir: None }),
706 run_interp_lower: false,
707 run_buffer_build: false,
708 run_resolve: false,
709 run_last_use: false,
710 run_analyze: true,
711 run_escape: false,
712 run_refinement_lower: true,
713 run_contract_lower: true,
714 run_law_lower: true,
715 run_build_symbols: true,
719 dep_modules: &[],
720 alloc_policy: None,
721 call_ctx: None,
722 on_after_pass: None,
723 },
724 );
725 let tc = pipeline_result.typecheck.expect("typecheck requested");
726 assert!(
727 tc.errors.is_empty(),
728 "source should typecheck: {:?}",
729 tc.errors
730 );
731 let proof_ir = pipeline_result.proof_ir;
732 let mut ctx = build_context(
733 items,
734 &tc,
735 pipeline_result.analysis.as_ref(),
736 project_name.to_string(),
737 vec![],
738 pipeline_result.symbol_table,
739 pipeline_result.resolved_items,
740 );
741 if let Some(ir) = proof_ir {
742 ctx.proof_ir = ir;
743 }
744 ctx
745 }
746
747 fn dafny_output(out: &ProjectOutput) -> String {
752 out.files
753 .iter()
754 .filter_map(|(name, content)| name.ends_with(".dfy").then_some(content.as_str()))
755 .collect::<Vec<&str>>()
756 .join("\n")
757 }
758
759 #[test]
760 fn prelude_emits_branch_path_only_when_used() {
761 let src = "module M\n intent = \"t\"\n\nfn pure(x: Int) -> Int\n x\n";
764 let ctx = ctx_from_source(src, "m");
765 let out = transpile(&ctx);
766 let dfy = dafny_output(&out);
767 assert!(!dfy.contains("datatype BranchPath"));
768 assert!(!dfy.contains("const BranchPath_Root"));
769 assert!(!dfy.contains("function BranchPath_child"));
770 assert!(!dfy.contains("function BranchPath_parse"));
771
772 let src_eff = "module M\n intent = \"t\"\n\n\
776 fn rollMax(path: BranchPath, n: Int, lo: Int, hi: Int) -> Int\n hi\n\n\
777 fn roll() -> Int\n ! [Random.int]\n Random.int(1, 6)\n\n\
778 verify roll law alwaysSix\n given rnd: Random.int = [rollMax]\n roll() => 6\n";
779 let ctx_eff = ctx_from_source(src_eff, "m");
780 let out_eff = transpile(&ctx_eff);
781 let dfy_eff = dafny_output(&out_eff);
782 assert!(dfy_eff.contains("datatype BranchPath"));
783 assert!(dfy_eff.contains("const BranchPath_Root"));
784 assert!(dfy_eff.contains("function BranchPath_child"));
785 assert!(dfy_eff.contains("function BranchPath_parse"));
786 }
787
788 #[test]
789 fn effectful_generative_fn_emits_lifted_form() {
790 let src = "module M\n\
794 \x20 intent = \"t\"\n\
795 \n\
796 fn pickOne() -> Int\n\
797 \x20 ! [Random.int]\n\
798 \x20 Random.int(1, 6)\n\
799 verify pickOne\n\
800 \x20 pickOne() => 1\n";
801 let ctx = ctx_from_source(src, "m");
802 let out = transpile(&ctx);
803 let dfy = dafny_output(&out);
804 assert!(
806 dfy.contains("function pickOne(path: BranchPath"),
807 "missing path param:\n{}",
808 dfy
809 );
810 assert!(
811 dfy.contains("rnd_Random_int"),
812 "missing oracle param:\n{}",
813 dfy
814 );
815 assert!(
817 dfy.contains("rnd_Random_int(path, 0, 1, 6)"),
818 "missing oracle call:\n{}",
819 dfy
820 );
821 }
822
823 #[test]
824 fn pure_functions_still_emit_as_before() {
825 let src = "module M\n intent = \"t\"\n\nfn double(x: Int) -> Int\n x + x\n";
828 let ctx = ctx_from_source(src, "m");
829 let out = transpile(&ctx);
830 let dfy = dafny_output(&out);
831 assert!(dfy.contains("function double(x: int): int"));
832 assert!(!dfy.contains("function double(path: BranchPath"));
833 }
834
835 #[test]
836 fn effectful_fn_with_unclassified_effect_is_still_skipped() {
837 let src = "module M\n\
842 \x20 intent = \"t\"\n\
843 \n\
844 fn configure(key: String, value: String) -> Unit\n\
845 \x20 ! [Env.set]\n\
846 \x20 Env.set(key, value)\n";
847 let ctx = ctx_from_source(src, "m");
848 let out = transpile(&ctx);
849 let dfy = dafny_output(&out);
850 assert!(
851 !dfy.contains("function configure"),
852 "stateful effectful fn should be skipped; got:\n{}",
853 dfy
854 );
855 }
856
857 #[test]
858 fn bang_product_emits_lifted_tuple_with_child_paths() {
859 let src = "module M\n\
865 \x20 intent = \"t\"\n\
866 \n\
867 fn pair() -> Tuple<Int, Int>\n\
868 \x20 ! [Random.int]\n\
869 \x20 (Random.int(1, 6), Random.int(1, 6))!\n\
870 verify pair\n\
871 \x20 pair() => (1, 1)\n";
872 let ctx = ctx_from_source(src, "m");
873 let out = transpile(&ctx);
874 let dfy = dafny_output(&out);
875 assert!(
876 dfy.contains("BranchPath_child(path, 0)"),
877 "branch 0 path missing:\n{}",
878 dfy
879 );
880 assert!(
881 dfy.contains("BranchPath_child(path, 1)"),
882 "branch 1 path missing:\n{}",
883 dfy
884 );
885 }
886
887 #[test]
888 fn branch_path_call_renders_with_underscore_names() {
889 let src = "module M\n\
892 \x20 intent = \"t\"\n\
893 \n\
894 fn mkPath() -> BranchPath\n\
895 \x20 BranchPath.child(BranchPath.Root, 2)\n";
896 let ctx = ctx_from_source(src, "m");
897 let out = transpile(&ctx);
898 let dfy = dafny_output(&out);
899 assert!(
900 dfy.contains("BranchPath_child(BranchPath_Root, 2)"),
901 "expected underscore-form call; got:\n{}",
902 dfy
903 );
904 }
905
906 #[test]
907 fn int_countdown_guarded_emits_requires_clause() {
908 let src = "module M\n\
915 \x20 intent = \"t\"\n\
916 \n\
917 fn down(n: Int) -> Int\n\
918 \x20 match n\n\
919 \x20 0 -> 0\n\
920 \x20 _ -> down(n - 1)\n";
921 let ctx = ctx_from_source(src, "m");
922 let out = transpile(&ctx);
923 let dfy = dafny_output(&out);
924 assert!(
925 dfy.contains("function down(n: int): int"),
926 "expected native Dafny function for countdown, got:\n{}",
927 dfy
928 );
929 assert!(
930 dfy.contains("requires n >= 0"),
931 "expected `requires n >= 0` clause, got:\n{}",
932 dfy
933 );
934 assert!(
935 dfy.contains("decreases n"),
936 "expected `decreases n` clause, got:\n{}",
937 dfy
938 );
939 assert!(
940 !dfy.contains("down__fuel"),
941 "should not emit fuel helper for native shape, got:\n{}",
942 dfy
943 );
944 }
945}