Skip to main content

aver/codegen/dafny/
mod.rs

1/// Aver → Dafny transpiler.
2///
3/// Generates a single `.dfy` file with:
4/// - `datatype` declarations for sum types
5/// - `datatype` wrappers for product types (records)
6/// - `function` definitions for pure functions
7/// - `lemma` stubs for verify-law blocks
8/// - `assert` examples for verify-cases blocks
9mod expr;
10mod fuel;
11mod toplevel;
12
13use crate::ast::{FnDef, TopLevel, VerifyKind};
14use crate::codegen::{CodegenContext, ProjectOutput};
15
16/// Check if a function body uses the `?` (ErrorProp) operator.
17/// Such functions require early-return semantics that Dafny pure functions cannot express.
18fn body_uses_error_prop(body: &std::sync::Arc<crate::ast::FnBody>) -> bool {
19    match body.as_ref() {
20        crate::ast::FnBody::Block(stmts) => stmts.iter().any(|s| match s {
21            crate::ast::Stmt::Binding(_, _, expr) => expr_uses_error_prop(expr),
22            crate::ast::Stmt::Expr(expr) => expr_uses_error_prop(expr),
23        }),
24    }
25}
26
27fn expr_uses_error_prop(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
28    use crate::ast::Expr;
29    match &expr.node {
30        Expr::ErrorProp(_) => true,
31        Expr::FnCall(f, args) => expr_uses_error_prop(f) || args.iter().any(expr_uses_error_prop),
32        Expr::BinOp(_, l, r) => expr_uses_error_prop(l) || expr_uses_error_prop(r),
33        Expr::Match { subject, arms, .. } => {
34            expr_uses_error_prop(subject) || arms.iter().any(|a| expr_uses_error_prop(&a.body))
35        }
36        Expr::Constructor(_, Some(arg)) => expr_uses_error_prop(arg),
37        Expr::List(elems) | Expr::Tuple(elems) => elems.iter().any(expr_uses_error_prop),
38        Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_uses_error_prop(e)),
39        Expr::RecordUpdate { base, updates, .. } => {
40            expr_uses_error_prop(base) || updates.iter().any(|(_, e)| expr_uses_error_prop(e))
41        }
42        Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
43            crate::ast::StrPart::Parsed(e) => expr_uses_error_prop(e),
44            _ => false,
45        }),
46        Expr::Attr(obj, _) => expr_uses_error_prop(obj),
47        Expr::TailCall(inner) => inner.args.iter().any(expr_uses_error_prop),
48        _ => false,
49    }
50}
51
52/// Transpile an Aver program into a Dafny project.
53pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
54    // Oracle v1: emit the trust-assumption header block at the top of the
55    // file so any downstream reviewer sees exactly which claims the proof
56    // relies on. The generator lives in the checker layer and is shared
57    // with the Lean backend to guarantee byte-identical claims.
58    let mut sections: Vec<String> = vec![
59        "// Generated by the Aver → Dafny transpiler".to_string(),
60        "// Pure core logic plus Oracle-lifted classified effects\n".to_string(),
61        crate::types::checker::proof_trust_header::generate_commented("// ") + "\n",
62        DAFNY_PRELUDE.to_string(),
63    ];
64
65    // Recursion classifier — same detector the Lean backend uses. Fns
66    // with a supported plan emit through the usual `decreases`-guarded
67    // path; recursive fns outside the supported patterns (mutual without
68    // a termination measure the classifier recognises, non-structural
69    // nested recursion, etc.) fall back to a `function {:axiom}`
70    // declaration so the whole file still type-checks. This parallels
71    // Lean's `partial def` fallback for unsupported shapes.
72    let (recursion_plans, _recursion_issues) = crate::codegen::recursion::analyze_plans(ctx);
73
74    // Mutual-recursion SCCs get Lean's mutual fuel-guarded emission
75    // ported to Dafny: each fn in the SCC gets a `fn__fuel(fuel: nat,
76    // …)` helper with `decreases fuel` and a wrapper supplying a
77    // plan-specific fuel metric. Fns whose return type has no obvious
78    // total default (Named ADTs without an inferred zero-inhabitant)
79    // fall back to `function {:axiom}` — parallel to Lean's
80    // `partial def`.
81    use crate::codegen::recursion::RecursionPlan;
82    let mutual_planned: std::collections::HashSet<String> = recursion_plans
83        .iter()
84        .filter(|(_, plan)| {
85            matches!(
86                plan,
87                RecursionPlan::MutualIntCountdown
88                    | RecursionPlan::MutualStringPosAdvance { .. }
89                    | RecursionPlan::MutualSizeOfRanked { .. }
90            )
91        })
92        .map(|(name, _)| name.clone())
93        .collect();
94
95    let mutual_fns_all: Vec<&FnDef> = ctx
96        .items
97        .iter()
98        .filter_map(|it| {
99            if let TopLevel::FnDef(fd) = it {
100                Some(fd)
101            } else {
102                None
103            }
104        })
105        .chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
106        .filter(|fd| mutual_planned.contains(&fd.name))
107        .collect();
108    let mutual_components =
109        crate::call_graph::ordered_fn_components(&mutual_fns_all, &ctx.module_prefixes);
110
111    let mut mutual_fuel_sections: Vec<String> = Vec::new();
112    let mut fuel_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
113    let mut axiom_fn_names: std::collections::HashSet<String> = std::collections::HashSet::new();
114    for component in &mutual_components {
115        let scc_fns: Vec<&FnDef> = component.iter().map(|fd| &**fd).collect();
116        match fuel::emit_mutual_fuel_group(&scc_fns, ctx, &recursion_plans) {
117            Some(code) => {
118                mutual_fuel_sections.push(code);
119                for fd in &scc_fns {
120                    fuel_emitted.insert(fd.name.clone());
121                }
122            }
123            None => {
124                for fd in &scc_fns {
125                    axiom_fn_names.insert(fd.name.clone());
126                }
127            }
128        }
129    }
130
131    let emit_pure_fn = |fd: &FnDef| -> String {
132        if fuel_emitted.contains(&fd.name) {
133            String::new() // emitted through mutual fuel group below
134        } else if axiom_fn_names.contains(&fd.name) {
135            toplevel::emit_fn_def_axiom(fd)
136        } else {
137            toplevel::emit_fn_def(fd, ctx)
138        }
139    };
140
141    // Emit type definitions from dependent modules
142    for module in &ctx.modules {
143        for td in &module.type_defs {
144            if let Some(code) = toplevel::emit_type_def(td) {
145                sections.push(code);
146            }
147        }
148    }
149
150    // Emit type definitions from the main module
151    for td in &ctx.type_defs {
152        if let Some(code) = toplevel::emit_type_def(td) {
153            sections.push(code);
154        }
155    }
156
157    // Emit function definitions. Pure fns go through the normal
158    // `emit_fn_def` path (which already lowers `?` via
159    // `lower_pure_question_bang_fn` when possible). When the body uses
160    // `?` and the lowering doesn't apply, emit an axiom declaration so
161    // the callee is still in scope for other Dafny output — without
162    // this, a fuel helper in a mutual SCC can reference the fn by name
163    // and the whole file loses resolution.
164    let needs_axiom_for_error_prop = |fd: &FnDef| -> bool {
165        body_uses_error_prop(&fd.body)
166            && crate::types::checker::effect_lifting::lower_pure_question_bang_fn(fd)
167                .ok()
168                .flatten()
169                .is_none()
170    };
171    let emit_pure_or_axiom = |fd: &FnDef| -> String {
172        if needs_axiom_for_error_prop(fd) {
173            toplevel::emit_fn_def_axiom(fd)
174        } else {
175            emit_pure_fn(fd)
176        }
177    };
178    for module in &ctx.modules {
179        for fd in &module.fn_defs {
180            if fd.effects.is_empty() {
181                sections.push(emit_pure_or_axiom(fd));
182            }
183        }
184    }
185    for item in &ctx.items {
186        if let TopLevel::FnDef(fd) = item
187            && fd.effects.is_empty()
188            && fd.name != "main"
189        {
190            sections.push(emit_pure_or_axiom(fd));
191        }
192    }
193
194    // Mutual fuel-guarded helpers and wrappers — one block per SCC.
195    // Emitted after the non-mutual pure fns so the axiom fallback block
196    // (if any SCC couldn't be defaulted) sits near the other opaque
197    // definitions in the output.
198    for section in mutual_fuel_sections {
199        sections.push(section);
200    }
201
202    // Oracle v1: emit lifted form for effectful functions whose effects are
203    // all classified (snapshot / generative / output). The lifter prepends
204    // `path: BranchPath` plus one oracle / capability param per non-output
205    // effect, rewriting effect-method calls in the body. Unclassified or
206    // higher-order-callback effects are still skipped.
207    //
208    // Only fns reachable from some verify block are emitted — otherwise
209    // a non-terminating effectful fn (e.g. a REPL loop) would force
210    // Dafny to demand a decreases clause for a fn nobody asked to prove.
211    let reachable = crate::codegen::common::verify_reachable_fn_names(&ctx.items);
212    //
213    // Collect the helper registry first so call sites to effectful
214    // user fns in any lifted body get `(path, oracle...)` injected
215    // to match the callee's lifted arity.
216    let mut helpers: std::collections::HashMap<String, Vec<String>> =
217        std::collections::HashMap::new();
218    for item in &ctx.items {
219        if let TopLevel::FnDef(fd) = item
220            && !fd.effects.is_empty()
221            && fd.name != "main"
222            && !body_uses_error_prop(&fd.body)
223            && reachable.contains(&fd.name)
224            && fd
225                .effects
226                .iter()
227                .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
228        {
229            helpers.insert(
230                fd.name.clone(),
231                fd.effects.iter().map(|e| e.node.clone()).collect(),
232            );
233        }
234    }
235    for item in &ctx.items {
236        if let TopLevel::FnDef(fd) = item
237            && !fd.effects.is_empty()
238            && fd.name != "main"
239            && !body_uses_error_prop(&fd.body)
240            && reachable.contains(&fd.name)
241            && fd
242                .effects
243                .iter()
244                .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
245            && let Ok(Some(lifted)) =
246                crate::types::checker::effect_lifting::lift_fn_def_with_helpers(fd, &helpers)
247        {
248            sections.push(toplevel::emit_fn_def(&lifted, ctx));
249        }
250    }
251
252    // Emit verify law blocks: sample assertions + universal lemma.
253    let mut law_counter: std::collections::HashMap<String, usize> =
254        std::collections::HashMap::new();
255    for item in &ctx.items {
256        if let TopLevel::Verify(vb) = item
257            && let VerifyKind::Law(law) = &vb.kind
258        {
259            let count = law_counter.entry(vb.fn_name.clone()).or_insert(0);
260            *count += 1;
261            let suffix = if *count > 1 {
262                format!("_{}", count)
263            } else {
264                String::new()
265            };
266
267            // 1. Sample assertions from the domain expansion (concrete smoke tests)
268            if !vb.cases.is_empty()
269                && let Some(code) = toplevel::emit_law_samples(vb, law, ctx, &suffix)
270            {
271                sections.push(code);
272            }
273
274            // 2. Universal lemma (when clause becomes requires).
275            //    Passing in the set of fns Dafny can't unfold in a
276            //    lemma context — axiom-fallback fns have no body at
277            //    all, and fuel-guarded wrappers only expand to a
278            //    call with a concrete metric that Dafny can't
279            //    symbolically reason about. In both cases the lemma
280            //    body short-circuits to `assume <ensures>`, matching
281            //    Lean's `sorry` fallback for the same shape.
282            let opaque_fns: std::collections::HashSet<String> =
283                axiom_fn_names.union(&fuel_emitted).cloned().collect();
284            sections.push(toplevel::emit_verify_law(vb, law, ctx, &opaque_fns));
285        }
286    }
287
288    let content = sections.join("\n");
289    let file_name = format!("{}.dfy", ctx.project_name);
290
291    ProjectOutput {
292        files: vec![(file_name, content)],
293    }
294}
295
296const DAFNY_PRELUDE: &str = r#"// --- Prelude: standard types and helpers ---
297
298datatype Result<T, E> = Ok(value: T) | Err(error: E)
299
300datatype Option<T> = None | Some(value: T)
301
302// Oracle v1: BranchPath is the proof-side representation of a position
303// in the structural tree of `!`/`?!` groups. Dewey-decimal under the hood
304// ("", "0", "2.0", …); constructors mirror the Aver-source BranchPath
305// opaque builtin (`.root`, `.child`, `.parse`) so the lifted bodies can
306// reference them directly without case-splitting at the call site.
307
308datatype BranchPath = BranchPath(dewey: string)
309
310// Oracle v1: HttpResponse / HttpRequest / Header are built-in Aver
311// record types that surface in `Http.*` effect signatures. Lifted
312// effectful fns using those effects reference them in oracle / result
313// types, so the proof export must know the shape.
314datatype Header = Header(name: string, value: string)
315
316datatype HttpResponse = HttpResponse(status: int, body: string, headers: seq<Header>)
317
318datatype HttpRequest = HttpRequest(method_: string, path: string, body: string, headers: seq<Header>)
319
320const BranchPath_Root: BranchPath := BranchPath("")
321
322function BranchPath_child(p: BranchPath, idx: int): BranchPath
323  requires idx >= 0
324{
325  if |p.dewey| == 0 then BranchPath(IntToString(idx))
326  else BranchPath(p.dewey + "." + IntToString(idx))
327}
328
329function BranchPath_parse(s: string): BranchPath {
330  BranchPath(s)
331}
332
333function ResultWithDefault<T, E>(r: Result<T, E>, d: T): T {
334  match r
335  case Ok(v) => v
336  case Err(_) => d
337}
338
339function OptionWithDefault<T>(o: Option<T>, d: T): T {
340  match o
341  case Some(v) => v
342  case None => d
343}
344
345function OptionToResult<T, E>(o: Option<T>, err: E): Result<T, E> {
346  match o
347  case Some(v) => Result.Ok(v)
348  case None => Result.Err(err)
349}
350
351function ListReverse<T>(xs: seq<T>): seq<T>
352  decreases |xs|
353{
354  if |xs| == 0 then []
355  else ListReverse(xs[1..]) + [xs[0]]
356}
357
358function ListHead<T>(xs: seq<T>): Option<T> {
359  if |xs| == 0 then None
360  else Some(xs[0])
361}
362
363function ListTail<T>(xs: seq<T>): seq<T> {
364  if |xs| == 0 then []
365  else xs[1..]
366}
367
368function ListTake<T>(xs: seq<T>, n: int): seq<T> {
369  if n <= 0 then []
370  else if n >= |xs| then xs
371  else xs[..n]
372}
373
374function ListDrop<T>(xs: seq<T>, n: int): seq<T> {
375  if n <= 0 then xs
376  else if n >= |xs| then []
377  else xs[n..]
378}
379
380function MapGet<K, V>(m: map<K, V>, k: K): Option<V> {
381  if k in m then Some(m[k])
382  else None
383}
384
385// --- String/Char helpers (opaque stubs for verification) ---
386
387function IntToString(n: int): string
388function IntFromString(s: string): Result<int, string>
389function FloatToString(r: real): string
390function FloatFromString(s: string): Result<real, string>
391function StringCharAt(s: string, i: int): Option<string> {
392  if 0 <= i < |s| then Option.Some([s[i]]) else Option.None
393}
394function StringChars(s: string): seq<string> {
395  seq(|s|, (i: int) requires 0 <= i < |s| => [s[i]])
396}
397function StringJoin(sep: string, parts: seq<string>): string
398  decreases |parts|
399{
400  if |parts| == 0 then ""
401  else if |parts| == 1 then parts[0]
402  else parts[0] + sep + StringJoin(sep, parts[1..])
403}
404function CharToCode(c: string): int
405function CharFromCode(n: int): Option<string>
406function MapEntries<K, V>(m: map<K, V>): seq<(K, V)>
407function MapFromList<K, V>(entries: seq<(K, V)>): map<K, V>
408  decreases |entries|
409{
410  if |entries| == 0 then map[]
411  else MapFromList(entries[..|entries|-1])[entries[|entries|-1].0 := entries[|entries|-1].1]
412}
413function ByteToHex(b: int): Result<string, string>
414function ByteFromHex(s: string): Result<int, string>
415function ToString<T>(v: T): string
416"#;
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crate::codegen::build_context;
422    use crate::source::parse_source;
423    use crate::tco;
424    use crate::types::checker::run_type_check_full;
425    use std::collections::HashSet;
426
427    fn ctx_from_source(src: &str, project_name: &str) -> CodegenContext {
428        let mut items = parse_source(src).expect("parse");
429        tco::transform_program(&mut items);
430        let tc = run_type_check_full(&items, None);
431        assert!(
432            tc.errors.is_empty(),
433            "source should typecheck: {:?}",
434            tc.errors
435        );
436        build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
437    }
438
439    fn dafny_output(out: &ProjectOutput) -> &str {
440        out.files
441            .iter()
442            .find_map(|(name, content)| name.ends_with(".dfy").then_some(content.as_str()))
443            .expect("expected a .dfy file")
444    }
445
446    #[test]
447    fn prelude_carries_branch_path_datatype_and_helpers() {
448        let src = "module M\n    intent = \"t\"\n\nfn pure(x: Int) -> Int\n    x\n";
449        let ctx = ctx_from_source(src, "m");
450        let out = transpile(&ctx);
451        let dfy = dafny_output(&out);
452        assert!(dfy.contains("datatype BranchPath"));
453        assert!(dfy.contains("const BranchPath_Root"));
454        assert!(dfy.contains("function BranchPath_child"));
455        assert!(dfy.contains("function BranchPath_parse"));
456    }
457
458    #[test]
459    fn effectful_generative_fn_emits_lifted_form() {
460        // Plan Example 3 analog: pickOne() ! [Random.int] Random.int(1, 6).
461        // Verify block makes pickOne reachable — without it the proof
462        // backend skips the fn (nothing to prove about it).
463        let src = "module M\n\
464             \x20   intent = \"t\"\n\
465             \n\
466             fn pickOne() -> Int\n\
467             \x20   ! [Random.int]\n\
468             \x20   Random.int(1, 6)\n\
469             verify pickOne\n\
470             \x20   pickOne() => 1\n";
471        let ctx = ctx_from_source(src, "m");
472        let out = transpile(&ctx);
473        let dfy = dafny_output(&out);
474        // Signature carries the lifted params.
475        assert!(
476            dfy.contains("function pickOne(path: BranchPath"),
477            "missing path param:\n{}",
478            dfy
479        );
480        assert!(
481            dfy.contains("rnd_Random_int"),
482            "missing oracle param:\n{}",
483            dfy
484        );
485        // Body calls oracle with threaded path + counter 0.
486        assert!(
487            dfy.contains("rnd_Random_int(path, 0, 1, 6)"),
488            "missing oracle call:\n{}",
489            dfy
490        );
491    }
492
493    #[test]
494    fn pure_functions_still_emit_as_before() {
495        // Sanity: pure fn continues to come out of the regular path — no
496        // spurious path / oracle params prepended.
497        let src = "module M\n    intent = \"t\"\n\nfn double(x: Int) -> Int\n    x + x\n";
498        let ctx = ctx_from_source(src, "m");
499        let out = transpile(&ctx);
500        let dfy = dafny_output(&out);
501        assert!(dfy.contains("function double(x: int): int"));
502        assert!(!dfy.contains("function double(path: BranchPath"));
503    }
504
505    #[test]
506    fn effectful_fn_with_unclassified_effect_is_still_skipped() {
507        // Env.set is ambient stateful — not in the v1 proof subset (process
508        // env is global and read-after-write depends on the whole ambient
509        // map, not a per-call oracle). The fn must not appear in the emitted
510        // Dafny output.
511        let src = "module M\n\
512             \x20   intent = \"t\"\n\
513             \n\
514             fn configure(key: String, value: String) -> Unit\n\
515             \x20   ! [Env.set]\n\
516             \x20   Env.set(key, value)\n";
517        let ctx = ctx_from_source(src, "m");
518        let out = transpile(&ctx);
519        let dfy = dafny_output(&out);
520        assert!(
521            !dfy.contains("function configure"),
522            "stateful effectful fn should be skipped; got:\n{}",
523            dfy
524        );
525    }
526
527    #[test]
528    fn bang_product_emits_lifted_tuple_with_child_paths() {
529        // Plain `!` lifts to a tuple in the emitted Dafny — the parallel
530        // claim is captured by the meta-level schedule-invariance
531        // invariant. Verifies that each branch threads BranchPath.child
532        // and resets its counter to 0. Verify block makes `pair`
533        // reachable for the proof backend.
534        let src = "module M\n\
535             \x20   intent = \"t\"\n\
536             \n\
537             fn pair() -> (Int, Int)\n\
538             \x20   ! [Random.int]\n\
539             \x20   (Random.int(1, 6), Random.int(1, 6))!\n\
540             verify pair\n\
541             \x20   pair() => (1, 1)\n";
542        let ctx = ctx_from_source(src, "m");
543        let out = transpile(&ctx);
544        let dfy = dafny_output(&out);
545        assert!(
546            dfy.contains("BranchPath_child(path, 0)"),
547            "branch 0 path missing:\n{}",
548            dfy
549        );
550        assert!(
551            dfy.contains("BranchPath_child(path, 1)"),
552            "branch 1 path missing:\n{}",
553            dfy
554        );
555    }
556
557    #[test]
558    fn branch_path_call_renders_with_underscore_names() {
559        // Verify the expression-emission bridge: Aver-source BranchPath
560        // constructor calls map onto the Dafny underscore-named helpers.
561        let src = "module M\n\
562             \x20   intent = \"t\"\n\
563             \n\
564             fn mkPath() -> BranchPath\n\
565             \x20   BranchPath.child(BranchPath.Root, 2)\n";
566        let ctx = ctx_from_source(src, "m");
567        let out = transpile(&ctx);
568        let dfy = dafny_output(&out);
569        assert!(
570            dfy.contains("BranchPath_child(BranchPath_Root, 2)"),
571            "expected underscore-form call; got:\n{}",
572            dfy
573        );
574    }
575}