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