1mod expr;
10mod fuel;
11mod toplevel;
12
13use crate::ast::{FnDef, TopLevel, VerifyKind};
14use crate::codegen::{CodegenContext, ProjectOutput};
15
16fn 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
52pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
54 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 let (recursion_plans, _recursion_issues) = crate::codegen::recursion::analyze_plans(ctx);
73
74 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() } 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 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 for td in &ctx.type_defs {
152 if let Some(code) = toplevel::emit_type_def(td) {
153 sections.push(code);
154 }
155 }
156
157 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 for section in mutual_fuel_sections {
199 sections.push(section);
200 }
201
202 let reachable = crate::codegen::common::verify_reachable_fn_names(&ctx.items);
212 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 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 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 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 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 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 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 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 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 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 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}