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 crate::codegen::recursion::RecursionPlan;
75 use std::collections::{HashMap, HashSet};
76
77 let (recursion_plans, _recursion_issues) = crate::codegen::recursion::analyze_plans(ctx);
78 let mutual_planned: HashSet<String> = recursion_plans
79 .iter()
80 .filter(|(_, plan)| {
81 matches!(
82 plan,
83 RecursionPlan::MutualIntCountdown
84 | RecursionPlan::MutualStringPosAdvance { .. }
85 | RecursionPlan::MutualSizeOfRanked { .. }
86 )
87 })
88 .map(|(name, _)| name.clone())
89 .collect();
90
91 let fn_scope = crate::codegen::common::fn_owning_scope(ctx);
92
93 let mutual_fns_all: Vec<&FnDef> = ctx
94 .items
95 .iter()
96 .filter_map(|it| {
97 if let TopLevel::FnDef(fd) = it {
98 Some(fd)
99 } else {
100 None
101 }
102 })
103 .chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
104 .filter(|fd| mutual_planned.contains(&fd.name))
105 .collect();
106 let mutual_components =
107 crate::call_graph::ordered_fn_components(&mutual_fns_all, &ctx.module_prefixes);
108
109 let mut fuel_per_scope: HashMap<String, Vec<String>> = HashMap::new();
110 let mut fuel_emitted: HashSet<String> = HashSet::new();
111 let mut axiom_fn_names: HashSet<String> = HashSet::new();
112
113 for component in &mutual_components {
114 let scc_fns: Vec<&FnDef> = component.iter().map(|fd| &**fd).collect();
115 let scope = scc_fns
116 .first()
117 .and_then(|fd| fn_scope.get(&fd.name))
118 .cloned()
119 .unwrap_or_default();
120 match fuel::emit_mutual_fuel_group(&scc_fns, ctx, &recursion_plans) {
121 Some(code) => {
122 fuel_per_scope.entry(scope).or_default().push(code);
123 for fd in &scc_fns {
124 fuel_emitted.insert(fd.name.clone());
125 }
126 }
127 None => {
128 for fd in &scc_fns {
129 axiom_fn_names.insert(fd.name.clone());
130 }
131 }
132 }
133 }
134
135 let needs_axiom_for_error_prop = |fd: &FnDef| -> bool {
136 body_uses_error_prop(&fd.body)
137 && crate::types::checker::effect_lifting::lower_pure_question_bang_fn(fd)
138 .ok()
139 .flatten()
140 .is_none()
141 };
142 let emit_pure_or_axiom = |fd: &FnDef| -> String {
143 if needs_axiom_for_error_prop(fd) {
144 toplevel::emit_fn_def_axiom(fd)
145 } else if fuel_emitted.contains(&fd.name) {
146 String::new()
147 } else if axiom_fn_names.contains(&fd.name) {
148 toplevel::emit_fn_def_axiom(fd)
149 } else {
150 toplevel::emit_fn_def(fd, ctx)
151 }
152 };
153
154 let mut pure_per_scope = crate::codegen::common::route_pure_components_per_scope(
162 ctx,
163 |fd| fd.effects.is_empty() && fd.name != "main",
164 |comp| {
165 comp.iter()
166 .map(|fd| emit_pure_or_axiom(fd))
167 .filter(|s| !s.is_empty())
168 .collect()
169 },
170 );
171
172 let mut module_files: Vec<(String, String)> = Vec::new();
173 let mut union_body = String::new();
174
175 for module in &ctx.modules {
177 let mut sections: Vec<String> = Vec::new();
178 for td in &module.type_defs {
179 if let Some(code) = toplevel::emit_type_def(td) {
180 sections.push(code);
181 }
182 }
183 sections.extend(pure_per_scope.take(&module.prefix));
184 if let Some(fuel) = fuel_per_scope.get(&module.prefix) {
185 sections.extend(fuel.clone());
186 }
187 let body = sections.join("\n");
188 union_body.push_str(&body);
189 union_body.push('\n');
190
191 let depth = module.prefix.chars().filter(|c| *c == '.').count();
196 let up = "../".repeat(depth);
197 let depends_includes: String = module
198 .depends
199 .iter()
200 .map(|d| {
201 format!(
202 "include \"{}{}.dfy\"",
203 up,
204 crate::codegen::common::module_prefix_to_filename(d)
205 )
206 })
207 .collect::<Vec<_>>()
208 .join("\n");
209 let depends_imports: String = module
210 .depends
211 .iter()
212 .map(|d| format!(" import opened {}", dafny_module_name(d)))
213 .collect::<Vec<_>>()
214 .join("\n");
215
216 let mut header = format!(
217 "// Aver-generated module: {}\ninclude \"{}common.dfy\"\n",
218 module.prefix, up
219 );
220 if !depends_includes.is_empty() {
221 header.push_str(&depends_includes);
222 header.push('\n');
223 }
224
225 let mut module_inner = String::from(" import opened AverCommon\n");
226 if !depends_imports.is_empty() {
227 module_inner.push_str(&depends_imports);
228 module_inner.push('\n');
229 }
230 module_inner.push('\n');
231 for line in body.lines() {
232 if line.is_empty() {
233 module_inner.push('\n');
234 } else {
235 module_inner.push_str(" ");
236 module_inner.push_str(line);
237 module_inner.push('\n');
238 }
239 }
240
241 let content = format!(
242 "{}\nmodule {} {{\n{}}}\n",
243 header,
244 dafny_module_name(&module.prefix),
245 module_inner
246 );
247 let path = module.prefix.replace('.', "/");
248 module_files.push((format!("{}.dfy", path), content));
249 }
250
251 let mut entry_sections: Vec<String> = Vec::new();
253 for td in &ctx.type_defs {
254 if let Some(code) = toplevel::emit_type_def(td) {
255 entry_sections.push(code);
256 }
257 }
258 entry_sections.extend(pure_per_scope.take(""));
264 if let Some(fuel) = fuel_per_scope.get("") {
265 entry_sections.extend(fuel.clone());
266 }
267
268 let reachable = crate::codegen::common::verify_reachable_fn_names(&ctx.items);
271 let mut helpers: HashMap<String, Vec<String>> = HashMap::new();
272 for item in &ctx.items {
273 if let TopLevel::FnDef(fd) = item
274 && !fd.effects.is_empty()
275 && fd.name != "main"
276 && !body_uses_error_prop(&fd.body)
277 && reachable.contains(&fd.name)
278 && fd
279 .effects
280 .iter()
281 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
282 {
283 helpers.insert(
284 fd.name.clone(),
285 fd.effects.iter().map(|e| e.node.clone()).collect(),
286 );
287 }
288 }
289 for item in &ctx.items {
290 if let TopLevel::FnDef(fd) = item
291 && !fd.effects.is_empty()
292 && fd.name != "main"
293 && !body_uses_error_prop(&fd.body)
294 && reachable.contains(&fd.name)
295 && fd
296 .effects
297 .iter()
298 .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
299 && let Ok(Some(lifted)) =
300 crate::types::checker::effect_lifting::lift_fn_def_with_helpers(fd, &helpers)
301 {
302 entry_sections.push(toplevel::emit_fn_def(&lifted, ctx));
303 }
304 }
305
306 let mut law_counter: HashMap<String, usize> = HashMap::new();
308 for item in &ctx.items {
309 if let TopLevel::Verify(vb) = item
310 && let VerifyKind::Law(law) = &vb.kind
311 {
312 let count = law_counter.entry(vb.fn_name.clone()).or_insert(0);
313 *count += 1;
314 let suffix = if *count > 1 {
315 format!("_{}", count)
316 } else {
317 String::new()
318 };
319 if !vb.cases.is_empty()
320 && let Some(code) = toplevel::emit_law_samples(vb, law, ctx, &suffix)
321 {
322 entry_sections.push(code);
323 }
324 let opaque_fns: HashSet<String> =
325 axiom_fn_names.union(&fuel_emitted).cloned().collect();
326 entry_sections.push(toplevel::emit_verify_law(vb, law, ctx, &opaque_fns));
327 }
328 }
329
330 let entry_body = entry_sections.join("\n");
331 union_body.push_str(&entry_body);
332 union_body.push('\n');
333
334 let entry_includes: String = ctx
335 .modules
336 .iter()
337 .map(|m| {
338 format!(
339 "include \"{}.dfy\"",
340 crate::codegen::common::module_prefix_to_filename(&m.prefix)
341 )
342 })
343 .collect::<Vec<_>>()
344 .join("\n");
345 let entry_name = crate::codegen::common::entry_basename(ctx);
346 let mut entry_parts: Vec<String> = vec![format!(
347 "// Aver-generated entry: {}\ninclude \"common.dfy\"\n{}",
348 entry_name, entry_includes
349 )];
350 let mut opens = vec!["import opened AverCommon".to_string()];
353 for m in &ctx.modules {
354 opens.push(format!("import opened {}", dafny_module_name(&m.prefix)));
355 }
356 entry_parts.push(opens.join("\n"));
357 let declared = crate::codegen::common::collect_declared_effects(ctx);
358 let has_ip = union_body.contains("BranchPath");
359 let has_classified =
360 crate::types::checker::effect_classification::classifications_for_proof_subset()
361 .iter()
362 .any(|c| declared.includes(c.method));
363 if has_ip || has_classified {
364 entry_parts.push(
365 crate::types::checker::proof_trust_header::generate_commented("// ", &declared, has_ip),
366 );
367 }
368 let subtype_block = crate::types::checker::oracle_subtypes::dafny_subtype_predicates(&declared);
369 if !subtype_block.is_empty() {
370 union_body.push_str(&subtype_block);
380 union_body.push('\n');
381 entry_parts.push(subtype_block);
382 }
383 entry_parts.push(entry_body);
384 let entry_content = entry_parts.join("\n\n");
385
386 let common_content = build_common_dafny(&union_body);
388
389 let mut files = module_files;
390 files.push((format!("{}.dfy", entry_name), entry_content));
391 files.push(("common.dfy".to_string(), common_content));
392 ProjectOutput { files }
393}
394
395fn build_common_dafny(union_body: &str) -> String {
396 let mut sections: Vec<String> = vec![
397 "// Aver-generated shared library: built-in records and helpers".to_string(),
398 "module AverCommon {".to_string(),
399 DAFNY_PRELUDE_HEAD.to_string(),
400 ];
401 for record in crate::codegen::builtin_records::needed_records(union_body, false) {
402 sections.push(crate::codegen::builtin_records::render_dafny(record));
403 }
404 sections.push(DAFNY_PRELUDE_CORE_HELPERS.to_string());
405 for helper in crate::codegen::builtin_helpers::needed_helpers(union_body, false) {
406 match helper.key {
407 "BranchPath" => sections.push(DAFNY_HELPER_BRANCH_PATH.to_string()),
408 "AverList" => sections.push(DAFNY_HELPER_AVER_LIST.to_string()),
409 "StringHelpers" => sections.push(DAFNY_HELPER_STRING_HELPERS.to_string()),
410 "NumericParse" => sections.push(DAFNY_HELPER_NUMERIC_PARSE.to_string()),
411 "CharByte" => sections.push(DAFNY_HELPER_CHAR_BYTE.to_string()),
412 "AverMap" => sections.push(DAFNY_HELPER_AVER_MAP.to_string()),
413 "AverMeasure" | "ProofFuel" => {}
414 "FloatInstances" | "ExceptInstances" | "StringHadd" => {}
415 "ResultDatatype" => sections.push(DAFNY_HELPER_RESULT_DATATYPE.to_string()),
416 "OptionDatatype" => sections.push(DAFNY_HELPER_OPTION_DATATYPE.to_string()),
417 "OptionToResult" => sections.push(DAFNY_HELPER_OPTION_TO_RESULT.to_string()),
418 "BranchPathDatatype" => sections.push(DAFNY_HELPER_BRANCH_PATH_DATATYPE.to_string()),
419 other => panic!(
420 "Dafny backend has no implementation for builtin helper key '{}'. \
421 Add a match arm in build_common_dafny or remove the key from BUILTIN_HELPERS.",
422 other
423 ),
424 }
425 }
426 sections.push("}".to_string());
427 sections.join("\n")
428}
429
430const DAFNY_PRELUDE_HEAD: &str = r#"// --- Prelude: standard types and helpers ---
431"#;
432
433const DAFNY_HELPER_RESULT_DATATYPE: &str = r#"
434datatype Result<T, E> = Ok(value: T) | Err(error: E)
435
436function ResultWithDefault<T, E>(r: Result<T, E>, d: T): T {
437 match r
438 case Ok(v) => v
439 case Err(_) => d
440}
441"#;
442
443const DAFNY_HELPER_OPTION_DATATYPE: &str = r#"
444datatype Option<T> = None | Some(value: T)
445
446function OptionWithDefault<T>(o: Option<T>, d: T): T {
447 match o
448 case Some(v) => v
449 case None => d
450}
451"#;
452
453const DAFNY_HELPER_OPTION_TO_RESULT: &str = r#"
454function OptionToResult<T, E>(o: Option<T>, err: E): Result<T, E> {
455 match o
456 case Some(v) => Result.Ok(v)
457 case None => Result.Err(err)
458}
459"#;
460
461const DAFNY_HELPER_BRANCH_PATH_DATATYPE: &str = r#"
462// Oracle v1: BranchPath is the proof-side representation of a position
463// in the structural tree of `!`/`?!` groups. Dewey-decimal under the hood
464// ("", "0", "2.0", …); constructors mirror the Aver-source BranchPath
465// opaque builtin (`.root`, `.child`, `.parse`) so the lifted bodies can
466// reference them directly without case-splitting at the call site.
467
468datatype BranchPath = BranchPath(dewey: string)
469"#;
470
471const DAFNY_PRELUDE_CORE_HELPERS: &str = r#"
475function ToString<T>(v: T): string
476"#;
477
478const DAFNY_HELPER_BRANCH_PATH: &str = r#"
485const BranchPath_Root: BranchPath := BranchPath("")
486
487function BranchPath_child(p: BranchPath, idx: int): BranchPath
488 requires idx >= 0
489{
490 if |p.dewey| == 0 then BranchPath(IntToString(idx))
491 else BranchPath(p.dewey + "." + IntToString(idx))
492}
493
494function BranchPath_parse(s: string): BranchPath {
495 BranchPath(s)
496}
497"#;
498
499const DAFNY_HELPER_AVER_LIST: &str = r#"
500function ListReverse<T>(xs: seq<T>): seq<T>
501 decreases |xs|
502{
503 if |xs| == 0 then []
504 else ListReverse(xs[1..]) + [xs[0]]
505}
506
507function ListHead<T>(xs: seq<T>): Option<T> {
508 if |xs| == 0 then None
509 else Some(xs[0])
510}
511
512function ListTail<T>(xs: seq<T>): seq<T> {
513 if |xs| == 0 then []
514 else xs[1..]
515}
516
517function ListTake<T>(xs: seq<T>, n: int): seq<T> {
518 if n <= 0 then []
519 else if n >= |xs| then xs
520 else xs[..n]
521}
522
523function ListDrop<T>(xs: seq<T>, n: int): seq<T> {
524 if n <= 0 then xs
525 else if n >= |xs| then []
526 else xs[n..]
527}
528"#;
529
530const DAFNY_HELPER_AVER_MAP: &str = r#"
531function MapGet<K, V>(m: map<K, V>, k: K): Option<V> {
532 if k in m then Some(m[k])
533 else None
534}
535
536function MapEntries<K, V>(m: map<K, V>): seq<(K, V)>
537function MapFromList<K, V>(entries: seq<(K, V)>): map<K, V>
538 decreases |entries|
539{
540 if |entries| == 0 then map[]
541 else MapFromList(entries[..|entries|-1])[entries[|entries|-1].0 := entries[|entries|-1].1]
542}
543"#;
544
545const DAFNY_HELPER_STRING_HELPERS: &str = r#"
551function StringCharAt(s: string, i: int): Option<string> {
552 if 0 <= i < |s| then Option.Some([s[i]]) else Option.None
553}
554
555function StringChars(s: string): seq<string> {
556 seq(|s|, (i: int) requires 0 <= i < |s| => [s[i]])
557}
558
559function StringJoin(sep: string, parts: seq<string>): string
560 decreases |parts|
561{
562 if |parts| == 0 then ""
563 else if |parts| == 1 then parts[0]
564 else parts[0] + sep + StringJoin(sep, parts[1..])
565}
566
567function StringSplit(s: string, sep: string): seq<string>
568function StringContains(s: string, sub: string): bool
569function StringStartsWith(s: string, prefix: string): bool
570function StringEndsWith(s: string, suffix: string): bool
571function StringTrim(s: string): string
572function StringReplace(s: string, from_: string, to_: string): string
573function StringRepeat(s: string, n: int): string
574function StringIndexOf(s: string, sub: string): int
575function StringToUpper(s: string): string
576function StringToLower(s: string): string
577function StringFromBool(b: bool): string
578function StringByteLength(s: string): int
579
580function ListReverseStr(xs: seq<string>): seq<string>
581"#;
582
583const DAFNY_HELPER_NUMERIC_PARSE: &str = r#"
584function IntToString(n: int): string
585function IntFromString(s: string): Result<int, string>
586function FloatToString(r: real): string
587function FloatFromString(s: string): Result<real, string>
588function FloatPi(): real
589function FloatSqrt(r: real): real
590function FloatPow(base: real, exp: real): real
591function FloatToInt(r: real): int
592function FloatSin(r: real): real
593function FloatCos(r: real): real
594function FloatAtan2(y: real, x: real): real
595"#;
596
597const DAFNY_HELPER_CHAR_BYTE: &str = r#"
598function CharToCode(c: string): int
599function CharFromCode(n: int): Option<string>
600function ByteToHex(b: int): Result<string, string>
601function ByteFromHex(s: string): Result<int, string>
602"#;
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::codegen::build_context;
608 use crate::source::parse_source;
609
610 use std::collections::HashSet;
611
612 fn ctx_from_source(src: &str, project_name: &str) -> CodegenContext {
613 let mut items = parse_source(src).expect("parse");
614 crate::ir::pipeline::tco(&mut items);
615 let tc = crate::ir::pipeline::typecheck(
616 &items,
617 &crate::ir::TypecheckMode::Full { base_dir: None },
618 );
619 assert!(
620 tc.errors.is_empty(),
621 "source should typecheck: {:?}",
622 tc.errors
623 );
624 build_context(
625 items,
626 &tc,
627 None,
628 HashSet::new(),
629 project_name.to_string(),
630 vec![],
631 )
632 }
633
634 fn dafny_output(out: &ProjectOutput) -> String {
639 out.files
640 .iter()
641 .filter_map(|(name, content)| name.ends_with(".dfy").then_some(content.as_str()))
642 .collect::<Vec<&str>>()
643 .join("\n")
644 }
645
646 #[test]
647 fn prelude_emits_branch_path_only_when_used() {
648 let src = "module M\n intent = \"t\"\n\nfn pure(x: Int) -> Int\n x\n";
651 let ctx = ctx_from_source(src, "m");
652 let out = transpile(&ctx);
653 let dfy = dafny_output(&out);
654 assert!(!dfy.contains("datatype BranchPath"));
655 assert!(!dfy.contains("const BranchPath_Root"));
656 assert!(!dfy.contains("function BranchPath_child"));
657 assert!(!dfy.contains("function BranchPath_parse"));
658
659 let src_eff = "module M\n intent = \"t\"\n\n\
663 fn rollMax(path: BranchPath, n: Int, lo: Int, hi: Int) -> Int\n hi\n\n\
664 fn roll() -> Int\n ! [Random.int]\n Random.int(1, 6)\n\n\
665 verify roll law alwaysSix\n given rnd: Random.int = [rollMax]\n roll() => 6\n";
666 let ctx_eff = ctx_from_source(src_eff, "m");
667 let out_eff = transpile(&ctx_eff);
668 let dfy_eff = dafny_output(&out_eff);
669 assert!(dfy_eff.contains("datatype BranchPath"));
670 assert!(dfy_eff.contains("const BranchPath_Root"));
671 assert!(dfy_eff.contains("function BranchPath_child"));
672 assert!(dfy_eff.contains("function BranchPath_parse"));
673 }
674
675 #[test]
676 fn effectful_generative_fn_emits_lifted_form() {
677 let src = "module M\n\
681 \x20 intent = \"t\"\n\
682 \n\
683 fn pickOne() -> Int\n\
684 \x20 ! [Random.int]\n\
685 \x20 Random.int(1, 6)\n\
686 verify pickOne\n\
687 \x20 pickOne() => 1\n";
688 let ctx = ctx_from_source(src, "m");
689 let out = transpile(&ctx);
690 let dfy = dafny_output(&out);
691 assert!(
693 dfy.contains("function pickOne(path: BranchPath"),
694 "missing path param:\n{}",
695 dfy
696 );
697 assert!(
698 dfy.contains("rnd_Random_int"),
699 "missing oracle param:\n{}",
700 dfy
701 );
702 assert!(
704 dfy.contains("rnd_Random_int(path, 0, 1, 6)"),
705 "missing oracle call:\n{}",
706 dfy
707 );
708 }
709
710 #[test]
711 fn pure_functions_still_emit_as_before() {
712 let src = "module M\n intent = \"t\"\n\nfn double(x: Int) -> Int\n x + x\n";
715 let ctx = ctx_from_source(src, "m");
716 let out = transpile(&ctx);
717 let dfy = dafny_output(&out);
718 assert!(dfy.contains("function double(x: int): int"));
719 assert!(!dfy.contains("function double(path: BranchPath"));
720 }
721
722 #[test]
723 fn effectful_fn_with_unclassified_effect_is_still_skipped() {
724 let src = "module M\n\
729 \x20 intent = \"t\"\n\
730 \n\
731 fn configure(key: String, value: String) -> Unit\n\
732 \x20 ! [Env.set]\n\
733 \x20 Env.set(key, value)\n";
734 let ctx = ctx_from_source(src, "m");
735 let out = transpile(&ctx);
736 let dfy = dafny_output(&out);
737 assert!(
738 !dfy.contains("function configure"),
739 "stateful effectful fn should be skipped; got:\n{}",
740 dfy
741 );
742 }
743
744 #[test]
745 fn bang_product_emits_lifted_tuple_with_child_paths() {
746 let src = "module M\n\
752 \x20 intent = \"t\"\n\
753 \n\
754 fn pair() -> Tuple<Int, Int>\n\
755 \x20 ! [Random.int]\n\
756 \x20 (Random.int(1, 6), Random.int(1, 6))!\n\
757 verify pair\n\
758 \x20 pair() => (1, 1)\n";
759 let ctx = ctx_from_source(src, "m");
760 let out = transpile(&ctx);
761 let dfy = dafny_output(&out);
762 assert!(
763 dfy.contains("BranchPath_child(path, 0)"),
764 "branch 0 path missing:\n{}",
765 dfy
766 );
767 assert!(
768 dfy.contains("BranchPath_child(path, 1)"),
769 "branch 1 path missing:\n{}",
770 dfy
771 );
772 }
773
774 #[test]
775 fn branch_path_call_renders_with_underscore_names() {
776 let src = "module M\n\
779 \x20 intent = \"t\"\n\
780 \n\
781 fn mkPath() -> BranchPath\n\
782 \x20 BranchPath.child(BranchPath.Root, 2)\n";
783 let ctx = ctx_from_source(src, "m");
784 let out = transpile(&ctx);
785 let dfy = dafny_output(&out);
786 assert!(
787 dfy.contains("BranchPath_child(BranchPath_Root, 2)"),
788 "expected underscore-form call; got:\n{}",
789 dfy
790 );
791 }
792}