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 lemmas;
10mod toplevel;
11
12use crate::ast::{FnDef, TopLevel, VerifyKind};
13use crate::codegen::{CodegenContext, ProjectOutput};
14
15/// Check if a function body uses the `?` (ErrorProp) operator.
16/// Such functions require early-return semantics that Dafny pure functions cannot express.
17fn 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
51/// Transpile an Aver program into a Dafny project.
52pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
53    transpile_unified(ctx)
54}
55
56/// Translate an Aver module prefix into a Dafny module identifier.
57/// Two transformations:
58/// - Dotted Aver prefixes (`Models.User`) flatten to underscore form
59///   so Dafny doesn't treat them as nested-module cycles when sibling
60///   submodules import each other.
61/// - Every emitted module name is prefixed with `Aver_` so that
62///   user-source `module Foo` (the Aver namespace of fns operating on
63///   a record) cannot collide with a `record Foo` declared in some
64///   other Aver module — Dafny resolves type and module names in the
65///   same namespace, so `Aver_Foo` (module) ≠ `Foo` (datatype).
66pub(crate) fn dafny_module_name(prefix: &str) -> String {
67    format!("Aver_{}", prefix.replace('.', "_"))
68}
69
70/// Multi-file Dafny output: one file per dependent module wrapped in
71/// `module M { ... }`, a shared `common.dfy` carrying built-in records
72/// and helpers under `module AverCommon`, and an entry `<project>.dfy`
73/// with the trust header, top-level items, and verify lemmas.
74fn transpile_unified(ctx: &CodegenContext) -> ProjectOutput {
75    use std::collections::{HashMap, HashSet};
76
77    // ProofIR is populated by the ContractLower pipeline stage. Mutual
78    // SCC members are exactly the fns whose contract is `Fuel { Lex }`
79    // — that's the unifying shape MutualIntCountdown /
80    // MutualStringPosAdvance / MutualSizeOfRanked all lower to.
81    //
82    // Round-6: `fn_contracts` is keyed by canonical `Module.fn` after
83    // round-5. The earlier `mutual_planned.contains(&fd.name)` filter
84    // (bare) misses every module-owned mutual-recursive SCC. Resolve
85    // per-`&FnDef` via pointer-eq scope so module-owned mutual fuel
86    // groups land in `mutual_fns_all` correctly.
87    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    // Phase E finalization: SCC-membership sets key by opaque
116    // `FnId` resolved through the symbol table. Same shape as
117    // `ProofIR.fn_contracts` post-#142 — bare-name lookups are gone
118    // by construction, so two same-bare-name fns across modules can
119    // never collide in these sets even after laws-in-modules lands.
120    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        // Mutual SCC emit resolves fn bodies through `emit_fn_body` →
140        // `emit_expr_legacy`, which falls back to
141        // `ctx.active_module_scope()` when no explicit scope is passed.
142        // Wrap the fuel/native dispatch in `with_module_scope` so a
143        // module-owned mutual group doesn't resolve as if it were
144        // entry-scope (same shape as the pure-fn path in
145        // `route_pure_components_per_scope` below).
146        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            // Try native `decreases` tuple first — when every member has a
153            // sizeOf-measurable parameter and a classifier rank, the SCC
154            // emits as plain mutual functions and proofs over concrete
155            // values no longer hit the fuel-encoding's symbolic-unfolding
156            // ceiling (BigInt's 10⁹ pairs close as real samples instead of
157            // needing the literal-magnitude cutoff). Falls back to fuel
158            // when the SCC has a non-sizeOf member.
159            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    // Self-recursive singletons whose termination no recognized
177    // `decreases` pattern justifies (doubling/halving recursion on a
178    // rational num/den pair — tests/fixtures/expo_outside_subset.av):
179    // guessing a measure emits a function whose own `decreases` fails
180    // verification AND a synthesized `requires p >= 0` that breaks
181    // every total caller. Route them to the opaque `{:axiom}` form —
182    // callers stay wellformed, and laws over them are reported as
183    // omitted/unproven instead of erroring. Kept in a SEPARATE set
184    // from `axiom_fn_ids` so the law/sample machinery for the
185    // pre-existing axiom population (mutual-SCC fallback, `?`-lowering
186    // failures) is untouched; this set only (a) switches the fn's own
187    // emission to `{:axiom}` and (b) suppresses sample asserts that
188    // could never be proved against an opaque body.
189    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    // SCC-route pure fns through the shared per-scope router (each scope
243    // independently — same reasoning as Lean). For DAG inputs each
244    // component is a singleton emitted via `emit_pure_or_axiom`; the
245    // `_or_axiom` half also handles the fuel-emitted/axiom-fallback
246    // skip-and-stub cases, so multi-fn SCCs that aren't fuel-handled
247    // emit each fn as an axiom and the SCC topology is otherwise
248    // ignored at this layer.
249    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    // ---- Per-module files (collected into the shared module tree) ----
267    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        // Submodules (`Models.User` → `Models/User.dfy`) live inside
287        // subdirectories, so `include` paths need `../` prefixes to reach
288        // the project root where `common.dfy` and sibling-module files
289        // live. Depth = number of segments minus one.
290        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    // ---- Entry sections ----
347    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    // Pure fns from entry came out of the shared per-scope router. The
354    // closure above already filtered `main` (it has `effects.is_empty()`
355    // == false because it lives under `! [...]` in practice; if a `main`
356    // ever lands as a pure fn the per-scope router will pick it up like
357    // any other and the verify lemmas below will simply not reference it).
358    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    // Lifted effectful fns (entry only — modules don't host effectful fns
364    // in the v1 emitter).
365    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    // Verify lemmas
402    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            // Native mutual-rec members + their transitive callers
418            // also need bounded-∀ universal (true ∀ over int doesn't
419            // close even with native decreases) — but stays separate
420            // from opaque so per-sample bodies on the native path
421            // skip the fuel-magnitude cutoff.
422            let native_transitive = toplevel::transitive_opaque_closure(ctx, &native_emitted);
423            // Truly opaque `{:axiom}` fns from the termination-decline
424            // path (and their transitive callers): nothing about their
425            // value is provable, so sample asserts/lemmas over them
426            // could only fail. Suppressed with a marker comment below.
427            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    // Open every dependent module + AverCommon so unqualified type names
476    // (`Point`, `Tile`) and helpers stay in scope at the top level.
477    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        // Fold subtype block into the union body BEFORE computing
496        // `needed_helpers` — the Oracle subtype block introduces
497        // `BranchPath` references (e.g. `predicate IsTimeUnixMsNonneg(
498        // f: (BranchPath, int) -> int)`) for files that declare
499        // classified effects but never spell `BranchPath` in user
500        // code. Without this, common.dfy misses the `datatype
501        // BranchPath` block and Main.dfy fails verification with
502        // `Type or type parameter is not declared in this scope:
503        // BranchPath`.
504        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    // ---- common.dfy ----
512    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
596/// Universal `ToString<T>` opaque — small (1 line), used by interpolation
597/// machinery in many shapes, kept always-on to avoid token-detection edge
598/// cases for things like `ToString(x)` showing up in nested type args.
599const DAFNY_PRELUDE_CORE_HELPERS: &str = r#"
600function ToString<T>(v: T): string
601"#;
602
603/// `BranchPath` constructors. Emitted only when the body uses Oracle
604/// lifting (any `BranchPath` reference); pure-math files don't need
605/// them. Note `BranchPath_child` calls `IntToString`, so when this
606/// helper is included the StringHelpers piece must come along too —
607/// that's enforced via `BUILTIN_HELPERS::depends_on` for `BranchPath`
608/// pulling in `NumericParse` (whose tokens cover `IntToString`).
609const 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
670/// `StringHelpers` covers the opaque/ish string utilities. Note Dafny
671/// has no AverDigits namespace; the numeric `IntToString`/`FromString`/
672/// `FloatToString`/`FromString` opaques live under the `NumericParse`
673/// helper key alongside Lean's parsing namespace, since the body-token
674/// detection is shared.
675const 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        // Proof-mode minimal pipeline — same shape as `lean::tests::
750        // ctx_from_source`; see that for why every rewriting stage is
751        // off (resolve / escape / interp_lower / buffer_build / last_use
752        // would alter source-level recursion shapes the classifier
753        // matches against).
754        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                // BuildSymbols is needed for fn_contracts lookup
770                // (keyed by opaque FnId resolved through the symbol
771                // table since the FnKey → FnId migration).
772                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    /// Concatenate every emitted `.dfy` source. The unified emitter
802    /// splits the program into entry / per-module / `common.dfy`
803    /// regardless of how many user modules a source has, so legacy
804    /// substring assertions need to look across all generated files.
805    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        // Pure fn — body has no BranchPath, so neither the datatype
816        // declaration nor the constructor helpers are emitted.
817        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        // Effectful fn with a verify block — Oracle lifting reaches the
827        // proof body and introduces `BranchPath` references, pulling in
828        // both the datatype declaration and the constructor helpers.
829        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        // Plan Example 3 analog: pickOne() ! [Random.int] Random.int(1, 6).
845        // Verify block makes pickOne reachable — without it the proof
846        // backend skips the fn (nothing to prove about it).
847        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        // Signature carries the lifted params.
859        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        // Body calls oracle with threaded path + counter 0.
870        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        // Sanity: pure fn continues to come out of the regular path — no
880        // spurious path / oracle params prepended.
881        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        // Env.set is ambient stateful — not in the v1 proof subset (process
892        // env is global and read-after-write depends on the whole ambient
893        // map, not a per-call oracle). The fn must not appear in the emitted
894        // Dafny output.
895        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        // Plain `!` lifts to a tuple in the emitted Dafny — the parallel
914        // claim is captured by the meta-level schedule-invariance
915        // invariant. Verifies that each branch threads BranchPath.child
916        // and resets its counter to 0. Verify block makes `pair`
917        // reachable for the proof backend.
918        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        // Verify the expression-emission bridge: Aver-source BranchPath
944        // constructor calls map onto the Dafny underscore-named helpers.
945        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        // The `match n { 0 -> 0; _ -> down(n - 1) }` shape is the Lean
963        // native-guarded target; on the Dafny side it lands on the
964        // single-fn `emit_fn_def` path whose `infer_decreases` already
965        // produces `requires n >= 0` + `decreases n`. This test pins the
966        // existing Dafny shape so the Lean refactor doesn't accidentally
967        // route this fn through a fuel/axiom path on the Dafny side.
968        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}