Skip to main content

aver/codegen/dafny/
mod.rs

1/// Aver → Dafny transpiler.
2///
3/// Single-module sources emit one `.dfy` file. Multi-module sources emit
4/// one file per dependent module wrapped in `module M { ... }`, plus a
5/// shared `common.dfy` with built-in records/helpers, plus the entry
6/// file holding the trust header, top-level items, and verify lemmas.
7mod expr;
8mod fuel;
9mod toplevel;
10
11use crate::ast::{FnDef, TopLevel, VerifyKind};
12use crate::codegen::{CodegenContext, ProjectOutput};
13
14/// Check if a function body uses the `?` (ErrorProp) operator.
15/// Such functions require early-return semantics that Dafny pure functions cannot express.
16fn 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
50/// Transpile an Aver program into a Dafny project.
51pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
52    transpile_unified(ctx)
53}
54
55/// Translate an Aver module prefix into a Dafny module identifier.
56/// Two transformations:
57/// - Dotted Aver prefixes (`Models.User`) flatten to underscore form
58///   so Dafny doesn't treat them as nested-module cycles when sibling
59///   submodules import each other.
60/// - Every emitted module name is prefixed with `Aver_` so that
61///   user-source `module Foo` (the Aver namespace of fns operating on
62///   a record) cannot collide with a `record Foo` declared in some
63///   other Aver module — Dafny resolves type and module names in the
64///   same namespace, so `Aver_Foo` (module) ≠ `Foo` (datatype).
65pub(crate) fn dafny_module_name(prefix: &str) -> String {
66    format!("Aver_{}", prefix.replace('.', "_"))
67}
68
69/// Multi-file Dafny output: one file per dependent module wrapped in
70/// `module M { ... }`, a shared `common.dfy` carrying built-in records
71/// and helpers under `module AverCommon`, and an entry `<project>.dfy`
72/// with the trust header, top-level items, and verify lemmas.
73fn 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    // SCC-route pure fns through the shared per-scope router (each scope
155    // independently — same reasoning as Lean). For DAG inputs each
156    // component is a singleton emitted via `emit_pure_or_axiom`; the
157    // `_or_axiom` half also handles the fuel-emitted/axiom-fallback
158    // skip-and-stub cases, so multi-fn SCCs that aren't fuel-handled
159    // emit each fn as an axiom and the SCC topology is otherwise
160    // ignored at this layer.
161    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    // ---- Per-module files (collected into the shared module tree) ----
176    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        // Submodules (`Models.User` → `Models/User.dfy`) live inside
192        // subdirectories, so `include` paths need `../` prefixes to reach
193        // the project root where `common.dfy` and sibling-module files
194        // live. Depth = number of segments minus one.
195        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    // ---- Entry sections ----
252    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    // Pure fns from entry came out of the shared per-scope router. The
259    // closure above already filtered `main` (it has `effects.is_empty()`
260    // == false because it lives under `! [...]` in practice; if a `main`
261    // ever lands as a pure fn the per-scope router will pick it up like
262    // any other and the verify lemmas below will simply not reference it).
263    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    // Lifted effectful fns (entry only — modules don't host effectful fns
269    // in the v1 emitter).
270    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    // Verify lemmas
307    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    // Open every dependent module + AverCommon so unqualified type names
351    // (`Point`, `Tile`) and helpers stay in scope at the top level.
352    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        // Fold subtype block into the union body BEFORE computing
371        // `needed_helpers` — the Oracle subtype block introduces
372        // `BranchPath` references (e.g. `predicate IsTimeUnixMsNonneg(
373        // f: (BranchPath, int) -> int)`) for files that declare
374        // classified effects but never spell `BranchPath` in user
375        // code. Without this, common.dfy misses the `datatype
376        // BranchPath` block and Main.dfy fails verification with
377        // `Type or type parameter is not declared in this scope:
378        // BranchPath`.
379        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    // ---- common.dfy ----
387    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
471/// Universal `ToString<T>` opaque — small (1 line), used by interpolation
472/// machinery in many shapes, kept always-on to avoid token-detection edge
473/// cases for things like `ToString(x)` showing up in nested type args.
474const DAFNY_PRELUDE_CORE_HELPERS: &str = r#"
475function ToString<T>(v: T): string
476"#;
477
478/// `BranchPath` constructors. Emitted only when the body uses Oracle
479/// lifting (any `BranchPath` reference); pure-math files don't need
480/// them. Note `BranchPath_child` calls `IntToString`, so when this
481/// helper is included the StringHelpers piece must come along too —
482/// that's enforced via `BUILTIN_HELPERS::depends_on` for `BranchPath`
483/// pulling in `NumericParse` (whose tokens cover `IntToString`).
484const 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
545/// `StringHelpers` covers the opaque/ish string utilities. Note Dafny
546/// has no AverDigits namespace; the numeric `IntToString`/`FromString`/
547/// `FloatToString`/`FromString` opaques live under the `NumericParse`
548/// helper key alongside Lean's parsing namespace, since the body-token
549/// detection is shared.
550const 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    /// Concatenate every emitted `.dfy` source. The unified emitter
635    /// splits the program into entry / per-module / `common.dfy`
636    /// regardless of how many user modules a source has, so legacy
637    /// substring assertions need to look across all generated files.
638    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        // Pure fn — body has no BranchPath, so neither the datatype
649        // declaration nor the constructor helpers are emitted.
650        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        // Effectful fn with a verify block — Oracle lifting reaches the
660        // proof body and introduces `BranchPath` references, pulling in
661        // both the datatype declaration and the constructor helpers.
662        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        // Plan Example 3 analog: pickOne() ! [Random.int] Random.int(1, 6).
678        // Verify block makes pickOne reachable — without it the proof
679        // backend skips the fn (nothing to prove about it).
680        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        // Signature carries the lifted params.
692        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        // Body calls oracle with threaded path + counter 0.
703        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        // Sanity: pure fn continues to come out of the regular path — no
713        // spurious path / oracle params prepended.
714        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        // Env.set is ambient stateful — not in the v1 proof subset (process
725        // env is global and read-after-write depends on the whole ambient
726        // map, not a per-call oracle). The fn must not appear in the emitted
727        // Dafny output.
728        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        // Plain `!` lifts to a tuple in the emitted Dafny — the parallel
747        // claim is captured by the meta-level schedule-invariance
748        // invariant. Verifies that each branch threads BranchPath.child
749        // and resets its counter to 0. Verify block makes `pair`
750        // reachable for the proof backend.
751        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        // Verify the expression-emission bridge: Aver-source BranchPath
777        // constructor calls map onto the Dafny underscore-named helpers.
778        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}