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 std::collections::{HashMap, HashSet};
75
76    // ProofIR is populated by the ContractLower pipeline stage. Mutual
77    // SCC members are exactly the fns whose contract is `Fuel { Lex }`
78    // — that's the unifying shape MutualIntCountdown /
79    // MutualStringPosAdvance / MutualSizeOfRanked all lower to.
80    //
81    // Round-6: `fn_contracts` is keyed by canonical `Module.fn` after
82    // round-5. The earlier `mutual_planned.contains(&fd.name)` filter
83    // (bare) misses every module-owned mutual-recursive SCC. Resolve
84    // per-`&FnDef` via pointer-eq scope so module-owned mutual fuel
85    // groups land in `mutual_fns_all` correctly.
86    let is_mutual_lex = |fd: &FnDef| -> bool {
87        crate::codegen::common::find_fn_contract_for_fn(ctx, fd).is_some_and(|c| {
88            matches!(
89                c.recursion,
90                Some(crate::ir::RecursionContract::Fuel {
91                    fuel_metric: crate::ir::FuelMetric::Lex { .. },
92                })
93            )
94        })
95    };
96
97    let mutual_fns_all: Vec<&FnDef> = ctx
98        .items
99        .iter()
100        .filter_map(|it| {
101            if let TopLevel::FnDef(fd) = it {
102                Some(fd)
103            } else {
104                None
105            }
106        })
107        .chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
108        .filter(|fd| is_mutual_lex(fd))
109        .collect();
110    let mutual_components =
111        crate::call_graph::ordered_fn_components(&mutual_fns_all, &ctx.module_prefixes);
112
113    let mut fuel_per_scope: HashMap<String, Vec<String>> = HashMap::new();
114    // Phase E finalization: SCC-membership sets key by opaque
115    // `FnId` resolved through the symbol table. Same shape as
116    // `ProofIR.fn_contracts` post-#142 — bare-name lookups are gone
117    // by construction, so two same-bare-name fns across modules can
118    // never collide in these sets even after laws-in-modules lands.
119    let mut fuel_emitted: HashSet<crate::ir::FnId> = HashSet::new();
120    let mut native_emitted: HashSet<crate::ir::FnId> = HashSet::new();
121    let mut axiom_fn_ids: HashSet<crate::ir::FnId> = HashSet::new();
122
123    let insert_fn_ids = |set: &mut HashSet<crate::ir::FnId>, fns: &[&FnDef]| {
124        for fd in fns {
125            if let Some(id) = crate::codegen::common::fn_id_for_decl(ctx, fd) {
126                set.insert(id);
127            }
128        }
129    };
130
131    for component in &mutual_components {
132        let scc_fns: Vec<&FnDef> = component.iter().map(|fd| &**fd).collect();
133        let scope = scc_fns
134            .first()
135            .and_then(|fd| crate::codegen::common::fn_owning_scope_for(ctx, fd))
136            .map(|s| s.to_string())
137            .unwrap_or_default();
138        // Mutual SCC emit resolves fn bodies through `emit_fn_body` →
139        // `emit_expr_legacy`, which falls back to
140        // `ctx.active_module_scope()` when no explicit scope is passed.
141        // Wrap the fuel/native dispatch in `with_module_scope` so a
142        // module-owned mutual group doesn't resolve as if it were
143        // entry-scope (same shape as the pure-fn path in
144        // `route_pure_components_per_scope` below).
145        let scope_opt = if scope.is_empty() {
146            None
147        } else {
148            Some(scope.as_str())
149        };
150        ctx.with_module_scope(scope_opt, || {
151            // Try native `decreases` tuple first — when every member has a
152            // sizeOf-measurable parameter and a classifier rank, the SCC
153            // emits as plain mutual functions and proofs over concrete
154            // values no longer hit the fuel-encoding's symbolic-unfolding
155            // ceiling (BigInt's 10⁹ pairs close as real samples instead of
156            // needing the literal-magnitude cutoff). Falls back to fuel
157            // when the SCC has a non-sizeOf member.
158            if let Some(code) = fuel::emit_mutual_native_decreases_group(&scc_fns, ctx) {
159                fuel_per_scope.entry(scope.clone()).or_default().push(code);
160                insert_fn_ids(&mut native_emitted, &scc_fns);
161            } else {
162                match fuel::emit_mutual_fuel_group(&scc_fns, ctx) {
163                    Some(code) => {
164                        fuel_per_scope.entry(scope.clone()).or_default().push(code);
165                        insert_fn_ids(&mut fuel_emitted, &scc_fns);
166                    }
167                    None => {
168                        insert_fn_ids(&mut axiom_fn_ids, &scc_fns);
169                    }
170                }
171            }
172        });
173    }
174
175    let needs_axiom_for_error_prop = |fd: &FnDef| -> bool {
176        body_uses_error_prop(&fd.body)
177            && crate::types::checker::effect_lifting::lower_pure_question_bang_fn(fd)
178                .ok()
179                .flatten()
180                .is_none()
181    };
182    let id_in = |set: &HashSet<crate::ir::FnId>, fd: &FnDef| -> bool {
183        crate::codegen::common::fn_id_for_decl(ctx, fd).is_some_and(|id| set.contains(&id))
184    };
185    let emit_pure_or_axiom = |fd: &FnDef| -> String {
186        if needs_axiom_for_error_prop(fd) {
187            toplevel::emit_fn_def_axiom(fd, ctx)
188        } else if id_in(&fuel_emitted, fd) || id_in(&native_emitted, fd) {
189            String::new()
190        } else if id_in(&axiom_fn_ids, fd) {
191            toplevel::emit_fn_def_axiom(fd, ctx)
192        } else {
193            toplevel::emit_fn_def(fd, ctx)
194        }
195    };
196
197    // SCC-route pure fns through the shared per-scope router (each scope
198    // independently — same reasoning as Lean). For DAG inputs each
199    // component is a singleton emitted via `emit_pure_or_axiom`; the
200    // `_or_axiom` half also handles the fuel-emitted/axiom-fallback
201    // skip-and-stub cases, so multi-fn SCCs that aren't fuel-handled
202    // emit each fn as an axiom and the SCC topology is otherwise
203    // ignored at this layer.
204    let mut pure_per_scope = crate::codegen::common::route_pure_components_per_scope(
205        ctx,
206        |fd| fd.effects.is_empty() && fd.name != "main",
207        |comp, scope| {
208            let scope_opt = if scope.is_empty() { None } else { Some(scope) };
209            ctx.with_module_scope(scope_opt, || {
210                comp.iter()
211                    .map(|fd| emit_pure_or_axiom(fd))
212                    .filter(|s| !s.is_empty())
213                    .collect()
214            })
215        },
216    );
217
218    let mut module_files: Vec<(String, String)> = Vec::new();
219    let mut union_body = String::new();
220
221    // ---- Per-module files (collected into the shared module tree) ----
222    for module in &ctx.modules {
223        let mut sections: Vec<String> = Vec::new();
224        ctx.with_module_scope(Some(module.prefix.as_str()), || {
225            for td in &module.type_defs {
226                if let Some(code) =
227                    toplevel::emit_type_def_in_scope(td, ctx, Some(module.prefix.as_str()))
228                {
229                    sections.push(code);
230                }
231            }
232        });
233        sections.extend(pure_per_scope.take(&module.prefix));
234        if let Some(fuel) = fuel_per_scope.get(&module.prefix) {
235            sections.extend(fuel.clone());
236        }
237        let body = sections.join("\n");
238        union_body.push_str(&body);
239        union_body.push('\n');
240
241        // Submodules (`Models.User` → `Models/User.dfy`) live inside
242        // subdirectories, so `include` paths need `../` prefixes to reach
243        // the project root where `common.dfy` and sibling-module files
244        // live. Depth = number of segments minus one.
245        let depth = module.prefix.chars().filter(|c| *c == '.').count();
246        let up = "../".repeat(depth);
247        let depends_includes: String = module
248            .depends
249            .iter()
250            .map(|d| {
251                format!(
252                    "include \"{}{}.dfy\"",
253                    up,
254                    crate::codegen::common::module_prefix_to_filename(d)
255                )
256            })
257            .collect::<Vec<_>>()
258            .join("\n");
259        let depends_imports: String = module
260            .depends
261            .iter()
262            .map(|d| format!("  import opened {}", dafny_module_name(d)))
263            .collect::<Vec<_>>()
264            .join("\n");
265
266        let mut header = format!(
267            "// Aver-generated module: {}\ninclude \"{}common.dfy\"\n",
268            module.prefix, up
269        );
270        if !depends_includes.is_empty() {
271            header.push_str(&depends_includes);
272            header.push('\n');
273        }
274
275        let mut module_inner = String::from("  import opened AverCommon\n");
276        if !depends_imports.is_empty() {
277            module_inner.push_str(&depends_imports);
278            module_inner.push('\n');
279        }
280        module_inner.push('\n');
281        for line in body.lines() {
282            if line.is_empty() {
283                module_inner.push('\n');
284            } else {
285                module_inner.push_str("  ");
286                module_inner.push_str(line);
287                module_inner.push('\n');
288            }
289        }
290
291        let content = format!(
292            "{}\nmodule {} {{\n{}}}\n",
293            header,
294            dafny_module_name(&module.prefix),
295            module_inner
296        );
297        let path = module.prefix.replace('.', "/");
298        module_files.push((format!("{}.dfy", path), content));
299    }
300
301    // ---- Entry sections ----
302    let mut entry_sections: Vec<String> = Vec::new();
303    for td in &ctx.type_defs {
304        if let Some(code) = toplevel::emit_type_def(td, ctx) {
305            entry_sections.push(code);
306        }
307    }
308    // Pure fns from entry came out of the shared per-scope router. The
309    // closure above already filtered `main` (it has `effects.is_empty()`
310    // == false because it lives under `! [...]` in practice; if a `main`
311    // ever lands as a pure fn the per-scope router will pick it up like
312    // any other and the verify lemmas below will simply not reference it).
313    entry_sections.extend(pure_per_scope.take(""));
314    if let Some(fuel) = fuel_per_scope.get("") {
315        entry_sections.extend(fuel.clone());
316    }
317
318    // Lifted effectful fns (entry only — modules don't host effectful fns
319    // in the v1 emitter).
320    let reachable = crate::codegen::common::verify_reachable_fn_names(&ctx.items);
321    let mut helpers: HashMap<String, Vec<String>> = HashMap::new();
322    for item in &ctx.items {
323        if let TopLevel::FnDef(fd) = item
324            && !fd.effects.is_empty()
325            && fd.name != "main"
326            && !body_uses_error_prop(&fd.body)
327            && reachable.contains(&fd.name)
328            && fd
329                .effects
330                .iter()
331                .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
332        {
333            helpers.insert(
334                fd.name.clone(),
335                fd.effects.iter().map(|e| e.node.clone()).collect(),
336            );
337        }
338    }
339    for item in &ctx.items {
340        if let TopLevel::FnDef(fd) = item
341            && !fd.effects.is_empty()
342            && fd.name != "main"
343            && !body_uses_error_prop(&fd.body)
344            && reachable.contains(&fd.name)
345            && fd
346                .effects
347                .iter()
348                .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
349            && let Ok(Some(lifted)) =
350                crate::types::checker::effect_lifting::lift_fn_def_with_helpers(fd, &helpers)
351        {
352            entry_sections.push(toplevel::emit_fn_def(&lifted, ctx));
353        }
354    }
355
356    // Verify lemmas
357    let mut law_counter: HashMap<String, usize> = HashMap::new();
358    for item in &ctx.items {
359        if let TopLevel::Verify(vb) = item
360            && let VerifyKind::Law(law) = &vb.kind
361        {
362            let count = law_counter.entry(vb.fn_name.clone()).or_insert(0);
363            *count += 1;
364            let suffix = if *count > 1 {
365                format!("_{}", count)
366            } else {
367                String::new()
368            };
369            let direct_opaque: HashSet<crate::ir::FnId> =
370                axiom_fn_ids.union(&fuel_emitted).copied().collect();
371            let opaque_fns = toplevel::transitive_opaque_closure(ctx, &direct_opaque);
372            // Native mutual-rec members + their transitive callers
373            // also need bounded-∀ universal (true ∀ over int doesn't
374            // close even with native decreases) — but stays separate
375            // from opaque so per-sample bodies on the native path
376            // skip the fuel-magnitude cutoff.
377            let native_transitive = toplevel::transitive_opaque_closure(ctx, &native_emitted);
378            if !vb.cases.is_empty()
379                && let Some(code) = toplevel::emit_law_samples(
380                    vb,
381                    law,
382                    ctx,
383                    &suffix,
384                    &opaque_fns,
385                    &fuel_emitted,
386                    &native_transitive,
387                )
388            {
389                entry_sections.push(code);
390            }
391            entry_sections.push(toplevel::emit_verify_law(
392                vb,
393                law,
394                ctx,
395                &opaque_fns,
396                &native_transitive,
397                &suffix,
398            ));
399        }
400    }
401
402    let entry_body = entry_sections.join("\n");
403    union_body.push_str(&entry_body);
404    union_body.push('\n');
405
406    let entry_includes: String = ctx
407        .modules
408        .iter()
409        .map(|m| {
410            format!(
411                "include \"{}.dfy\"",
412                crate::codegen::common::module_prefix_to_filename(&m.prefix)
413            )
414        })
415        .collect::<Vec<_>>()
416        .join("\n");
417    let entry_name = crate::codegen::common::entry_basename(ctx);
418    let mut entry_parts: Vec<String> = vec![format!(
419        "// Aver-generated entry: {}\ninclude \"common.dfy\"\n{}",
420        entry_name, entry_includes
421    )];
422    // Open every dependent module + AverCommon so unqualified type names
423    // (`Point`, `Tile`) and helpers stay in scope at the top level.
424    let mut opens = vec!["import opened AverCommon".to_string()];
425    for m in &ctx.modules {
426        opens.push(format!("import opened {}", dafny_module_name(&m.prefix)));
427    }
428    entry_parts.push(opens.join("\n"));
429    let declared = crate::codegen::common::collect_declared_effects(ctx);
430    let has_ip = union_body.contains("BranchPath");
431    let has_classified =
432        crate::types::checker::effect_classification::classifications_for_proof_subset()
433            .iter()
434            .any(|c| declared.includes(c.method));
435    if has_ip || has_classified {
436        entry_parts.push(
437            crate::types::checker::proof_trust_header::generate_commented("// ", &declared, has_ip),
438        );
439    }
440    let subtype_block = crate::types::checker::oracle_subtypes::dafny_subtype_predicates(&declared);
441    if !subtype_block.is_empty() {
442        // Fold subtype block into the union body BEFORE computing
443        // `needed_helpers` — the Oracle subtype block introduces
444        // `BranchPath` references (e.g. `predicate IsTimeUnixMsNonneg(
445        // f: (BranchPath, int) -> int)`) for files that declare
446        // classified effects but never spell `BranchPath` in user
447        // code. Without this, common.dfy misses the `datatype
448        // BranchPath` block and Main.dfy fails verification with
449        // `Type or type parameter is not declared in this scope:
450        // BranchPath`.
451        union_body.push_str(&subtype_block);
452        union_body.push('\n');
453        entry_parts.push(subtype_block);
454    }
455    entry_parts.push(entry_body);
456    let entry_content = entry_parts.join("\n\n");
457
458    // ---- common.dfy ----
459    let common_content = build_common_dafny(&union_body);
460
461    let mut files = module_files;
462    files.push((format!("{}.dfy", entry_name), entry_content));
463    files.push(("common.dfy".to_string(), common_content));
464    ProjectOutput { files }
465}
466
467fn build_common_dafny(union_body: &str) -> String {
468    let mut sections: Vec<String> = vec![
469        "// Aver-generated shared library: built-in records and helpers".to_string(),
470        "module AverCommon {".to_string(),
471        DAFNY_PRELUDE_HEAD.to_string(),
472    ];
473    for record in crate::codegen::builtin_records::needed_records(union_body, false) {
474        sections.push(crate::codegen::builtin_records::render_dafny(record));
475    }
476    sections.push(DAFNY_PRELUDE_CORE_HELPERS.to_string());
477    for helper in crate::codegen::builtin_helpers::needed_helpers(union_body, false) {
478        match helper.key {
479            "BranchPath" => sections.push(DAFNY_HELPER_BRANCH_PATH.to_string()),
480            "AverList" => sections.push(DAFNY_HELPER_AVER_LIST.to_string()),
481            "StringHelpers" => sections.push(DAFNY_HELPER_STRING_HELPERS.to_string()),
482            "NumericParse" => sections.push(DAFNY_HELPER_NUMERIC_PARSE.to_string()),
483            "CharByte" => sections.push(DAFNY_HELPER_CHAR_BYTE.to_string()),
484            "AverMap" => sections.push(DAFNY_HELPER_AVER_MAP.to_string()),
485            "AverMeasure" | "ProofFuel" => {}
486            "FloatInstances" | "ExceptInstances" | "StringHadd" => {}
487            "ResultDatatype" => sections.push(DAFNY_HELPER_RESULT_DATATYPE.to_string()),
488            "OptionDatatype" => sections.push(DAFNY_HELPER_OPTION_DATATYPE.to_string()),
489            "OptionToResult" => sections.push(DAFNY_HELPER_OPTION_TO_RESULT.to_string()),
490            "BranchPathDatatype" => sections.push(DAFNY_HELPER_BRANCH_PATH_DATATYPE.to_string()),
491            other => panic!(
492                "Dafny backend has no implementation for builtin helper key '{}'. \
493                 Add a match arm in build_common_dafny or remove the key from BUILTIN_HELPERS.",
494                other
495            ),
496        }
497    }
498    sections.push("}".to_string());
499    sections.join("\n")
500}
501
502const DAFNY_PRELUDE_HEAD: &str = r#"// --- Prelude: standard types and helpers ---
503"#;
504
505const DAFNY_HELPER_RESULT_DATATYPE: &str = r#"
506datatype Result<T, E> = Ok(value: T) | Err(error: E)
507
508function ResultWithDefault<T, E>(r: Result<T, E>, d: T): T {
509  match r
510  case Ok(v) => v
511  case Err(_) => d
512}
513"#;
514
515const DAFNY_HELPER_OPTION_DATATYPE: &str = r#"
516datatype Option<T> = None | Some(value: T)
517
518function OptionWithDefault<T>(o: Option<T>, d: T): T {
519  match o
520  case Some(v) => v
521  case None => d
522}
523"#;
524
525const DAFNY_HELPER_OPTION_TO_RESULT: &str = r#"
526function OptionToResult<T, E>(o: Option<T>, err: E): Result<T, E> {
527  match o
528  case Some(v) => Result.Ok(v)
529  case None => Result.Err(err)
530}
531"#;
532
533const DAFNY_HELPER_BRANCH_PATH_DATATYPE: &str = r#"
534// Oracle v1: BranchPath is the proof-side representation of a position
535// in the structural tree of `!`/`?!` groups. Dewey-decimal under the hood
536// ("", "0", "2.0", …); constructors mirror the Aver-source BranchPath
537// opaque builtin (`.root`, `.child`, `.parse`) so the lifted bodies can
538// reference them directly without case-splitting at the call site.
539
540datatype BranchPath = BranchPath(dewey: string)
541"#;
542
543/// Universal `ToString<T>` opaque — small (1 line), used by interpolation
544/// machinery in many shapes, kept always-on to avoid token-detection edge
545/// cases for things like `ToString(x)` showing up in nested type args.
546const DAFNY_PRELUDE_CORE_HELPERS: &str = r#"
547function ToString<T>(v: T): string
548"#;
549
550/// `BranchPath` constructors. Emitted only when the body uses Oracle
551/// lifting (any `BranchPath` reference); pure-math files don't need
552/// them. Note `BranchPath_child` calls `IntToString`, so when this
553/// helper is included the StringHelpers piece must come along too —
554/// that's enforced via `BUILTIN_HELPERS::depends_on` for `BranchPath`
555/// pulling in `NumericParse` (whose tokens cover `IntToString`).
556const DAFNY_HELPER_BRANCH_PATH: &str = r#"
557const BranchPath_Root: BranchPath := BranchPath("")
558
559function BranchPath_child(p: BranchPath, idx: int): BranchPath
560  requires idx >= 0
561{
562  if |p.dewey| == 0 then BranchPath(IntToString(idx))
563  else BranchPath(p.dewey + "." + IntToString(idx))
564}
565
566function BranchPath_parse(s: string): BranchPath {
567  BranchPath(s)
568}
569"#;
570
571const DAFNY_HELPER_AVER_LIST: &str = r#"
572function ListReverse<T>(xs: seq<T>): seq<T>
573  decreases |xs|
574{
575  if |xs| == 0 then []
576  else ListReverse(xs[1..]) + [xs[0]]
577}
578
579function ListHead<T>(xs: seq<T>): Option<T> {
580  if |xs| == 0 then None
581  else Some(xs[0])
582}
583
584function ListTail<T>(xs: seq<T>): seq<T> {
585  if |xs| == 0 then []
586  else xs[1..]
587}
588
589function ListTake<T>(xs: seq<T>, n: int): seq<T> {
590  if n <= 0 then []
591  else if n >= |xs| then xs
592  else xs[..n]
593}
594
595function ListDrop<T>(xs: seq<T>, n: int): seq<T> {
596  if n <= 0 then xs
597  else if n >= |xs| then []
598  else xs[n..]
599}
600"#;
601
602const DAFNY_HELPER_AVER_MAP: &str = r#"
603function MapGet<K, V>(m: map<K, V>, k: K): Option<V> {
604  if k in m then Some(m[k])
605  else None
606}
607
608function MapEntries<K, V>(m: map<K, V>): seq<(K, V)>
609function MapFromList<K, V>(entries: seq<(K, V)>): map<K, V>
610  decreases |entries|
611{
612  if |entries| == 0 then map[]
613  else MapFromList(entries[..|entries|-1])[entries[|entries|-1].0 := entries[|entries|-1].1]
614}
615"#;
616
617/// `StringHelpers` covers the opaque/ish string utilities. Note Dafny
618/// has no AverDigits namespace; the numeric `IntToString`/`FromString`/
619/// `FloatToString`/`FromString` opaques live under the `NumericParse`
620/// helper key alongside Lean's parsing namespace, since the body-token
621/// detection is shared.
622const DAFNY_HELPER_STRING_HELPERS: &str = r#"
623function StringCharAt(s: string, i: int): Option<string> {
624  if 0 <= i < |s| then Option.Some([s[i]]) else Option.None
625}
626
627function StringChars(s: string): seq<string> {
628  seq(|s|, (i: int) requires 0 <= i < |s| => [s[i]])
629}
630
631function StringSlice(s: string, from_: int, to_: int): string
632{
633  var lo := if from_ < 0 then 0 else if from_ > |s| then |s| else from_;
634  var hi := if to_ < 0 then 0 else if to_ > |s| then |s| else to_;
635  if lo >= hi then "" else s[lo..hi]
636}
637
638function StringJoin(sep: string, parts: seq<string>): string
639  decreases |parts|
640{
641  if |parts| == 0 then ""
642  else if |parts| == 1 then parts[0]
643  else parts[0] + sep + StringJoin(sep, parts[1..])
644}
645
646function StringSplit(s: string, sep: string): seq<string>
647function StringContains(s: string, sub: string): bool
648function StringStartsWith(s: string, prefix: string): bool
649function StringEndsWith(s: string, suffix: string): bool
650function StringTrim(s: string): string
651function StringReplace(s: string, from_: string, to_: string): string
652function StringRepeat(s: string, n: int): string
653function StringIndexOf(s: string, sub: string): int
654function StringToUpper(s: string): string
655function StringToLower(s: string): string
656function StringFromBool(b: bool): string
657function StringByteLength(s: string): int
658
659function ListReverseStr(xs: seq<string>): seq<string>
660"#;
661
662const DAFNY_HELPER_NUMERIC_PARSE: &str = r#"
663function IntToString(n: int): string
664function IntFromString(s: string): Result<int, string>
665function FloatToString(r: real): string
666function FloatFromString(s: string): Result<real, string>
667function FloatPi(): real
668function FloatSqrt(r: real): real
669function FloatPow(base: real, exp: real): real
670function FloatToInt(r: real): int
671function FloatSin(r: real): real
672function FloatCos(r: real): real
673function FloatAtan2(y: real, x: real): real
674
675function FloatDiv(a: real, b: real): real
676{
677  if b == 0.0 then 0.0 else a / b
678}
679"#;
680
681const DAFNY_HELPER_CHAR_BYTE: &str = r#"
682function CharToCode(c: string): int
683function CharFromCode(n: int): Option<string>
684function ByteToHex(b: int): Result<string, string>
685function ByteFromHex(s: string): Result<int, string>
686"#;
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use crate::codegen::build_context;
692    use crate::source::parse_source;
693
694    fn ctx_from_source(src: &str, project_name: &str) -> CodegenContext {
695        let mut items = parse_source(src).expect("parse");
696        // Proof-mode minimal pipeline — same shape as `lean::tests::
697        // ctx_from_source`; see that for why every rewriting stage is
698        // off (resolve / escape / interp_lower / buffer_build / last_use
699        // would alter source-level recursion shapes the classifier
700        // matches against).
701        let pipeline_result = crate::ir::pipeline::run(
702            &mut items,
703            crate::ir::PipelineConfig {
704                run_tco: true,
705                typecheck: Some(crate::ir::TypecheckMode::Full { base_dir: None }),
706                run_interp_lower: false,
707                run_buffer_build: false,
708                run_resolve: false,
709                run_last_use: false,
710                run_analyze: true,
711                run_escape: false,
712                run_refinement_lower: true,
713                run_contract_lower: true,
714                run_law_lower: true,
715                // BuildSymbols is needed for fn_contracts lookup
716                // (keyed by opaque FnId resolved through the symbol
717                // table since the FnKey → FnId migration).
718                run_build_symbols: true,
719                dep_modules: &[],
720                alloc_policy: None,
721                call_ctx: None,
722                on_after_pass: None,
723            },
724        );
725        let tc = pipeline_result.typecheck.expect("typecheck requested");
726        assert!(
727            tc.errors.is_empty(),
728            "source should typecheck: {:?}",
729            tc.errors
730        );
731        let proof_ir = pipeline_result.proof_ir;
732        let mut ctx = build_context(
733            items,
734            &tc,
735            pipeline_result.analysis.as_ref(),
736            project_name.to_string(),
737            vec![],
738            pipeline_result.symbol_table,
739            pipeline_result.resolved_items,
740        );
741        if let Some(ir) = proof_ir {
742            ctx.proof_ir = ir;
743        }
744        ctx
745    }
746
747    /// Concatenate every emitted `.dfy` source. The unified emitter
748    /// splits the program into entry / per-module / `common.dfy`
749    /// regardless of how many user modules a source has, so legacy
750    /// substring assertions need to look across all generated files.
751    fn dafny_output(out: &ProjectOutput) -> String {
752        out.files
753            .iter()
754            .filter_map(|(name, content)| name.ends_with(".dfy").then_some(content.as_str()))
755            .collect::<Vec<&str>>()
756            .join("\n")
757    }
758
759    #[test]
760    fn prelude_emits_branch_path_only_when_used() {
761        // Pure fn — body has no BranchPath, so neither the datatype
762        // declaration nor the constructor helpers are emitted.
763        let src = "module M\n    intent = \"t\"\n\nfn pure(x: Int) -> Int\n    x\n";
764        let ctx = ctx_from_source(src, "m");
765        let out = transpile(&ctx);
766        let dfy = dafny_output(&out);
767        assert!(!dfy.contains("datatype BranchPath"));
768        assert!(!dfy.contains("const BranchPath_Root"));
769        assert!(!dfy.contains("function BranchPath_child"));
770        assert!(!dfy.contains("function BranchPath_parse"));
771
772        // Effectful fn with a verify block — Oracle lifting reaches the
773        // proof body and introduces `BranchPath` references, pulling in
774        // both the datatype declaration and the constructor helpers.
775        let src_eff = "module M\n    intent = \"t\"\n\n\
776                       fn rollMax(path: BranchPath, n: Int, lo: Int, hi: Int) -> Int\n    hi\n\n\
777                       fn roll() -> Int\n    ! [Random.int]\n    Random.int(1, 6)\n\n\
778                       verify roll law alwaysSix\n    given rnd: Random.int = [rollMax]\n    roll() => 6\n";
779        let ctx_eff = ctx_from_source(src_eff, "m");
780        let out_eff = transpile(&ctx_eff);
781        let dfy_eff = dafny_output(&out_eff);
782        assert!(dfy_eff.contains("datatype BranchPath"));
783        assert!(dfy_eff.contains("const BranchPath_Root"));
784        assert!(dfy_eff.contains("function BranchPath_child"));
785        assert!(dfy_eff.contains("function BranchPath_parse"));
786    }
787
788    #[test]
789    fn effectful_generative_fn_emits_lifted_form() {
790        // Plan Example 3 analog: pickOne() ! [Random.int] Random.int(1, 6).
791        // Verify block makes pickOne reachable — without it the proof
792        // backend skips the fn (nothing to prove about it).
793        let src = "module M\n\
794             \x20   intent = \"t\"\n\
795             \n\
796             fn pickOne() -> Int\n\
797             \x20   ! [Random.int]\n\
798             \x20   Random.int(1, 6)\n\
799             verify pickOne\n\
800             \x20   pickOne() => 1\n";
801        let ctx = ctx_from_source(src, "m");
802        let out = transpile(&ctx);
803        let dfy = dafny_output(&out);
804        // Signature carries the lifted params.
805        assert!(
806            dfy.contains("function pickOne(path: BranchPath"),
807            "missing path param:\n{}",
808            dfy
809        );
810        assert!(
811            dfy.contains("rnd_Random_int"),
812            "missing oracle param:\n{}",
813            dfy
814        );
815        // Body calls oracle with threaded path + counter 0.
816        assert!(
817            dfy.contains("rnd_Random_int(path, 0, 1, 6)"),
818            "missing oracle call:\n{}",
819            dfy
820        );
821    }
822
823    #[test]
824    fn pure_functions_still_emit_as_before() {
825        // Sanity: pure fn continues to come out of the regular path — no
826        // spurious path / oracle params prepended.
827        let src = "module M\n    intent = \"t\"\n\nfn double(x: Int) -> Int\n    x + x\n";
828        let ctx = ctx_from_source(src, "m");
829        let out = transpile(&ctx);
830        let dfy = dafny_output(&out);
831        assert!(dfy.contains("function double(x: int): int"));
832        assert!(!dfy.contains("function double(path: BranchPath"));
833    }
834
835    #[test]
836    fn effectful_fn_with_unclassified_effect_is_still_skipped() {
837        // Env.set is ambient stateful — not in the v1 proof subset (process
838        // env is global and read-after-write depends on the whole ambient
839        // map, not a per-call oracle). The fn must not appear in the emitted
840        // Dafny output.
841        let src = "module M\n\
842             \x20   intent = \"t\"\n\
843             \n\
844             fn configure(key: String, value: String) -> Unit\n\
845             \x20   ! [Env.set]\n\
846             \x20   Env.set(key, value)\n";
847        let ctx = ctx_from_source(src, "m");
848        let out = transpile(&ctx);
849        let dfy = dafny_output(&out);
850        assert!(
851            !dfy.contains("function configure"),
852            "stateful effectful fn should be skipped; got:\n{}",
853            dfy
854        );
855    }
856
857    #[test]
858    fn bang_product_emits_lifted_tuple_with_child_paths() {
859        // Plain `!` lifts to a tuple in the emitted Dafny — the parallel
860        // claim is captured by the meta-level schedule-invariance
861        // invariant. Verifies that each branch threads BranchPath.child
862        // and resets its counter to 0. Verify block makes `pair`
863        // reachable for the proof backend.
864        let src = "module M\n\
865             \x20   intent = \"t\"\n\
866             \n\
867             fn pair() -> Tuple<Int, Int>\n\
868             \x20   ! [Random.int]\n\
869             \x20   (Random.int(1, 6), Random.int(1, 6))!\n\
870             verify pair\n\
871             \x20   pair() => (1, 1)\n";
872        let ctx = ctx_from_source(src, "m");
873        let out = transpile(&ctx);
874        let dfy = dafny_output(&out);
875        assert!(
876            dfy.contains("BranchPath_child(path, 0)"),
877            "branch 0 path missing:\n{}",
878            dfy
879        );
880        assert!(
881            dfy.contains("BranchPath_child(path, 1)"),
882            "branch 1 path missing:\n{}",
883            dfy
884        );
885    }
886
887    #[test]
888    fn branch_path_call_renders_with_underscore_names() {
889        // Verify the expression-emission bridge: Aver-source BranchPath
890        // constructor calls map onto the Dafny underscore-named helpers.
891        let src = "module M\n\
892             \x20   intent = \"t\"\n\
893             \n\
894             fn mkPath() -> BranchPath\n\
895             \x20   BranchPath.child(BranchPath.Root, 2)\n";
896        let ctx = ctx_from_source(src, "m");
897        let out = transpile(&ctx);
898        let dfy = dafny_output(&out);
899        assert!(
900            dfy.contains("BranchPath_child(BranchPath_Root, 2)"),
901            "expected underscore-form call; got:\n{}",
902            dfy
903        );
904    }
905
906    #[test]
907    fn int_countdown_guarded_emits_requires_clause() {
908        // The `match n { 0 -> 0; _ -> down(n - 1) }` shape is the Lean
909        // native-guarded target; on the Dafny side it lands on the
910        // single-fn `emit_fn_def` path whose `infer_decreases` already
911        // produces `requires n >= 0` + `decreases n`. This test pins the
912        // existing Dafny shape so the Lean refactor doesn't accidentally
913        // route this fn through a fuel/axiom path on the Dafny side.
914        let src = "module M\n\
915             \x20   intent = \"t\"\n\
916             \n\
917             fn down(n: Int) -> Int\n\
918             \x20   match n\n\
919             \x20       0 -> 0\n\
920             \x20       _ -> down(n - 1)\n";
921        let ctx = ctx_from_source(src, "m");
922        let out = transpile(&ctx);
923        let dfy = dafny_output(&out);
924        assert!(
925            dfy.contains("function down(n: int): int"),
926            "expected native Dafny function for countdown, got:\n{}",
927            dfy
928        );
929        assert!(
930            dfy.contains("requires n >= 0"),
931            "expected `requires n >= 0` clause, got:\n{}",
932            dfy
933        );
934        assert!(
935            dfy.contains("decreases n"),
936            "expected `decreases n` clause, got:\n{}",
937            dfy
938        );
939        assert!(
940            !dfy.contains("down__fuel"),
941            "should not emit fuel helper for native shape, got:\n{}",
942            dfy
943        );
944    }
945}