Skip to main content

aver/codegen/
common.rs

1use std::collections::HashSet;
2
3use crate::ast::{
4    Expr, FnBody, FnDef, Spanned, Stmt, StrPart, TailCallData, TopLevel, TypeDef, TypeVariant,
5    VerifyBlock, VerifyGivenDomain, VerifyKind,
6};
7use crate::codegen::CodegenContext;
8use crate::types::Type;
9
10// Backend-neutral predicates on AST items — all three codegen backends
11// (Lean, Dafny, Rust) want the same view of "is this pure?",
12// "self-referencing type?", and "what's the name of this type def?".
13
14/// A function is pure if it declares no effects and isn't `main`.
15pub fn is_pure_fn(fd: &FnDef) -> bool {
16    fd.effects.is_empty() && fd.name != "main"
17}
18
19/// True when the type definition mentions its own name somewhere in a
20/// field or variant payload (recursive ADT).
21pub fn is_recursive_type_def(td: &TypeDef) -> bool {
22    match td {
23        TypeDef::Sum { name, variants, .. } => is_recursive_sum(name, variants),
24        TypeDef::Product { name, fields, .. } => is_recursive_product(name, fields),
25    }
26}
27
28/// The declared name of a type definition.
29pub fn type_def_name(td: &TypeDef) -> &str {
30    match td {
31        TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
32    }
33}
34
35/// Granular variant of [`is_recursive_type_def`] taking a sum's
36/// `(name, variants)` split — some backends already have the parts
37/// separated and don't want to rebuild a `TypeDef` just to query.
38pub fn is_recursive_sum(name: &str, variants: &[TypeVariant]) -> bool {
39    variants
40        .iter()
41        .any(|v| v.fields.iter().any(|f| type_ref_contains(f, name)))
42}
43
44/// Granular variant of [`is_recursive_type_def`] for products.
45pub fn is_recursive_product(name: &str, fields: &[(String, String)]) -> bool {
46    fields.iter().any(|(_, ty)| type_ref_contains(ty, name))
47}
48
49fn type_ref_contains(annotation: &str, type_name: &str) -> bool {
50    // Direct match or any generic position: List<Foo>, Option<Foo>,
51    // Map<K, Foo>, (Foo, Bar), etc.
52    annotation == type_name
53        || annotation.contains(&format!("<{}", type_name))
54        || annotation.contains(&format!("{}>", type_name))
55        || annotation.contains(&format!(", {}", type_name))
56        || annotation.contains(&format!("{},", type_name))
57}
58
59/// Check if a name is a user-defined type (sum or product), including modules.
60pub(crate) fn is_user_type(name: &str, ctx: &CodegenContext) -> bool {
61    let check_td = |td: &TypeDef| match td {
62        TypeDef::Sum { name: n, .. } => n == name,
63        TypeDef::Product { name: n, .. } => n == name,
64    };
65    ctx.type_defs.iter().any(check_td)
66        || ctx.modules.iter().any(|m| m.type_defs.iter().any(check_td))
67}
68
69/// Resolve a module-qualified dotted name to `(module_prefix, local_suffix)`.
70/// Example: `Models.User.nameById` -> `("Models.User", "nameById")`.
71pub(crate) fn resolve_module_call<'a>(
72    dotted_name: &'a str,
73    ctx: &'a CodegenContext,
74) -> Option<(&'a str, &'a str)> {
75    let mut best: Option<&str> = None;
76    for prefix in &ctx.module_prefixes {
77        let dotted_prefix = format!("{}.", prefix);
78        if dotted_name.starts_with(&dotted_prefix) && best.is_none_or(|b| prefix.len() > b.len()) {
79            best = Some(prefix.as_str());
80        }
81    }
82    best.map(|prefix| (prefix, &dotted_name[prefix.len() + 1..]))
83}
84
85pub(crate) fn module_prefix_to_rust_segments(prefix: &str) -> Vec<String> {
86    prefix.split('.').map(module_segment_to_rust).collect()
87}
88
89/// Translate an Aver module prefix (`Models.User`, `Combat`) into a relative
90/// filesystem path stem with `/` separators. Lean's path-as-module convention
91/// and Dafny's `include "..."` paths both use this — same shape, no
92/// backend-specific escaping.
93pub(crate) fn module_prefix_to_filename(prefix: &str) -> String {
94    prefix.replace('.', "/")
95}
96
97/// Effects declared in fn signatures, preserving the distinction
98/// between namespace-level and method-level declarations.
99///
100/// - `bare_namespaces`: e.g. `! [Console]` ⇒ permits every classified
101///   `Console.*` method.
102/// - `methods`: e.g. `! [Console.print]` ⇒ permits only that one
103///   specific method (not the whole namespace).
104///
105/// Aver source allows both forms — we keep them separate so a single
106/// `! [Random.int]` does not pull every `Random.*` method into the
107/// trust header (or any other consumer that maps method-by-method).
108pub(crate) struct DeclaredEffects {
109    pub bare_namespaces: HashSet<String>,
110    pub methods: HashSet<String>,
111}
112
113impl DeclaredEffects {
114    /// True if `c_method` (e.g. `"Random.int"`) is declared either as
115    /// an explicit method or via its bare namespace (`"Random"`).
116    pub fn includes(&self, c_method: &str) -> bool {
117        if self.methods.contains(c_method) {
118            return true;
119        }
120        if let Some((ns, _)) = c_method.split_once('.') {
121            return self.bare_namespaces.contains(ns);
122        }
123        false
124    }
125}
126
127/// Collect declared effects across `ctx` (entry + dependent modules).
128/// Single source of truth for the proof-side trust header and the
129/// runtime-dependency detector in the Rust backend.
130pub(crate) fn collect_declared_effects(ctx: &CodegenContext) -> DeclaredEffects {
131    let mut bare_namespaces: HashSet<String> = HashSet::new();
132    let mut methods: HashSet<String> = HashSet::new();
133    let mut record = |effect: &str| {
134        if effect.contains('.') {
135            methods.insert(effect.to_string());
136        } else {
137            bare_namespaces.insert(effect.to_string());
138        }
139    };
140    for item in &ctx.items {
141        if let TopLevel::FnDef(fd) = item {
142            for eff in &fd.effects {
143                record(&eff.node);
144            }
145        }
146    }
147    for module in &ctx.modules {
148        for fd in &module.fn_defs {
149            for eff in &fd.effects {
150                record(&eff.node);
151            }
152        }
153    }
154    DeclaredEffects {
155        bare_namespaces,
156        methods,
157    }
158}
159
160/// Basename for the entry file emitted by Lean / Dafny. Prefer the
161/// source-declared module name (`module Foo` → `Foo`) so the entry
162/// file's name matches what the user wrote; fall back to a capitalised
163/// project name when no `module` declaration is present. Lake's
164/// path-as-module-name convention forces this for Lean — Dafny doesn't
165/// strictly need it but the same basename keeps the two backends
166/// aligned (no more `playground.dfy` vs `OracleTrace.lean`).
167pub fn entry_basename(ctx: &CodegenContext) -> String {
168    ctx.items
169        .iter()
170        .find_map(|item| match item {
171            TopLevel::Module(m) => Some(m.name.clone()),
172            _ => None,
173        })
174        .unwrap_or_else(|| {
175            let mut chars = ctx.project_name.chars();
176            match chars.next() {
177                None => String::new(),
178                Some(c) => c.to_uppercase().chain(chars).collect(),
179            }
180        })
181}
182
183/// Map every fn name in the program to its owning scope: the dependent
184/// module's prefix, or `""` for the entry. Used by the multi-file Lean
185/// and Dafny paths to route SCC components and fuel groups to the right
186/// per-scope file.
187pub(crate) fn fn_owning_scope(ctx: &CodegenContext) -> std::collections::HashMap<String, String> {
188    let mut scope = std::collections::HashMap::new();
189    for m in &ctx.modules {
190        for fd in &m.fn_defs {
191            scope.insert(fd.name.clone(), m.prefix.clone());
192        }
193    }
194    for fd in &ctx.fn_defs {
195        scope.insert(fd.name.clone(), String::new());
196    }
197    scope
198}
199
200pub(crate) fn module_prefix_to_rust_path(prefix: &str) -> String {
201    format!(
202        "crate::aver_generated::{}",
203        module_prefix_to_rust_segments(prefix).join("::")
204    )
205}
206
207fn module_segment_to_rust(segment: &str) -> String {
208    let chars = segment.chars().collect::<Vec<_>>();
209    let mut out = String::new();
210
211    for (idx, ch) in chars.iter().enumerate() {
212        if ch.is_ascii_alphanumeric() {
213            if ch.is_ascii_uppercase() {
214                let prev_is_lower_or_digit = idx > 0
215                    && (chars[idx - 1].is_ascii_lowercase() || chars[idx - 1].is_ascii_digit());
216                let next_is_lower = chars
217                    .get(idx + 1)
218                    .is_some_and(|next| next.is_ascii_lowercase());
219                if idx > 0 && (prev_is_lower_or_digit || next_is_lower) && !out.ends_with('_') {
220                    out.push('_');
221                }
222                out.push(ch.to_ascii_lowercase());
223            } else {
224                out.push(ch.to_ascii_lowercase());
225            }
226        } else if !out.ends_with('_') {
227            out.push('_');
228        }
229    }
230
231    let trimmed = out.trim_matches('_');
232    let mut normalized = if trimmed.is_empty() {
233        "module".to_string()
234    } else {
235        trimmed.to_string()
236    };
237
238    if matches!(
239        normalized.as_str(),
240        "as" | "break"
241            | "const"
242            | "continue"
243            | "crate"
244            | "else"
245            | "enum"
246            | "extern"
247            | "false"
248            | "fn"
249            | "for"
250            | "if"
251            | "impl"
252            | "in"
253            | "let"
254            | "loop"
255            | "match"
256            | "mod"
257            | "move"
258            | "mut"
259            | "pub"
260            | "ref"
261            | "return"
262            | "self"
263            | "Self"
264            | "static"
265            | "struct"
266            | "super"
267            | "trait"
268            | "true"
269            | "type"
270            | "unsafe"
271            | "use"
272            | "where"
273            | "while"
274    ) {
275        normalized.push_str("_mod");
276    }
277
278    normalized
279}
280
281/// Split a type annotation string at top-level delimiters (not inside `<>` or `()`).
282///
283/// Used by multiple backends to parse Aver type annotation strings like
284/// `"Map<String, List<Int>>"` or `"(String, Int)"`.
285pub(crate) fn split_type_params(s: &str, delim: char) -> Vec<String> {
286    let mut parts = Vec::new();
287    let mut depth = 0usize;
288    let mut current = String::new();
289    for ch in s.chars() {
290        match ch {
291            '<' | '(' => {
292                depth += 1;
293                current.push(ch);
294            }
295            '>' | ')' => {
296                depth = depth.saturating_sub(1);
297                current.push(ch);
298            }
299            _ if ch == delim && depth == 0 => {
300                parts.push(current.trim().to_string());
301                current.clear();
302            }
303            _ => current.push(ch),
304        }
305    }
306    let rest = current.trim().to_string();
307    if !rest.is_empty() {
308        parts.push(rest);
309    }
310    parts
311}
312
313/// Escape a string literal for target languages that use C-style escapes.
314/// Handles `\\`, `\"`, `\n`, `\r`, `\t`, `\0`,
315/// and generic control characters as `\xHH` (Lean/Rust) or `\uHHHH` (Dafny).
316///
317/// Use `unicode_escapes = true` for Dafny (which needs `\uHHHH`),
318/// `false` for Lean/Rust (which accept `\xHH`).
319pub(crate) fn escape_string_literal_ext(s: &str, unicode_escapes: bool) -> String {
320    let mut out = String::with_capacity(s.len());
321    for ch in s.chars() {
322        match ch {
323            '\\' => out.push_str("\\\\"),
324            '"' => out.push_str("\\\""),
325            '\n' => out.push_str("\\n"),
326            '\r' => out.push_str("\\r"),
327            '\t' => out.push_str("\\t"),
328            '\0' => out.push_str("\\0"),
329            c if c.is_control() => {
330                if unicode_escapes {
331                    // Dafny 4+ with Unicode chars enabled: \U{HHHHHH}
332                    out.push_str(&format!("\\U{{{:06x}}}", c as u32));
333                } else {
334                    out.push_str(&format!("\\x{:02x}", c as u32));
335                }
336            }
337            c => out.push(c),
338        }
339    }
340    out
341}
342
343/// Convenience: escape with `\xHH` for control chars (Lean, Rust).
344pub(crate) fn escape_string_literal(s: &str) -> String {
345    escape_string_literal_ext(s, false)
346}
347
348/// Convenience: escape with `\u{HHHH}` for control chars (Dafny).
349pub(crate) fn escape_string_literal_unicode(s: &str) -> String {
350    escape_string_literal_ext(s, true)
351}
352
353/// Parse an Aver type annotation string into the internal `Type` enum.
354///
355/// Thin wrapper around `types::parse_type_str` for use in codegen modules.
356pub(crate) fn parse_type_annotation(ann: &str) -> Type {
357    crate::types::parse_type_str(ann)
358}
359
360/// Check if a `Type` represents a set pattern: `Map<T, Unit>`.
361///
362/// Aver has no dedicated `Set` type — the idiomatic way to express a set
363/// is `Map<T, Unit>`. Codegen backends can lower this to the target
364/// language's native set type (Dafny `set<T>`, Lean `Finset T`, etc.).
365pub(crate) fn is_set_type(ty: &Type) -> bool {
366    matches!(ty, Type::Map(_, v) if matches!(v.as_ref(), Type::Unit))
367}
368
369/// Check if a type annotation string represents a set (`Map<T, Unit>`).
370pub(crate) fn is_set_annotation(ann: &str) -> bool {
371    is_set_type(&parse_type_annotation(ann))
372}
373
374/// Check if an expression is a compile-time Unit literal.
375pub(crate) fn is_unit_expr(expr: &crate::ast::Expr) -> bool {
376    matches!(expr, crate::ast::Expr::Literal(crate::ast::Literal::Unit))
377}
378
379/// Check if a spanned expression is a compile-time Unit literal.
380pub(crate) fn is_unit_expr_spanned(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
381    is_unit_expr(&expr.node)
382}
383
384/// Escape an Aver identifier if it collides with a target language reserved word.
385///
386/// `affix` is appended as a suffix (e.g. `"_"` for Dafny, `"'"` for Lean).
387/// For prefix escaping (e.g. Rust `r#`), use [`escape_reserved_word_prefix`].
388pub(crate) fn escape_reserved_word(name: &str, reserved: &[&str], suffix: &str) -> String {
389    if reserved.contains(&name) {
390        format!("{}{}", name, suffix)
391    } else {
392        name.to_string()
393    }
394}
395
396/// Like [`escape_reserved_word`] but prepends a prefix instead of appending a suffix.
397/// Used for Rust's `r#keyword` raw identifier syntax.
398pub(crate) fn escape_reserved_word_prefix(name: &str, reserved: &[&str], prefix: &str) -> String {
399    if reserved.contains(&name) {
400        format!("{}{}", prefix, name)
401    } else {
402        name.to_string()
403    }
404}
405
406/// Convert first character of a string to lowercase.
407///
408/// Used when converting PascalCase type/variant names to camelCase identifiers.
409pub(crate) fn to_lower_first(s: &str) -> String {
410    let mut chars = s.chars();
411    match chars.next() {
412        None => String::new(),
413        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
414    }
415}
416
417/// Convert an attribute chain into dotted name.
418/// Example: `Console.print` -> `Some("Console.print")`.
419pub(crate) fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
420    crate::ir::expr_to_dotted_name(expr)
421}
422
423/// Oracle v1: how to materialise the oracle argument for an effectful
424/// fn call in a law body.
425///
426/// - `LemmaBinding` — use the lemma-local identifier (`rnd`), matching
427///   the `given` name. Correct for the universal lemma body.
428/// - `SampleValue` — use the first Explicit domain value (the stub
429///   fn's identifier, e.g. `stubConst`). Correct for the concrete
430///   sample assertions where there's no lemma binding in scope and a
431///   single domain value.
432/// - `SampleCaseBinding(case_bindings)` — use the per-case binding
433///   value (by `given.name`). Correct for sample theorems when the
434///   domain has multiple values and each case substitutes a
435///   different one (`given stub: Http.get = [httpDown, httpOk]`).
436#[derive(Debug, Clone)]
437pub(crate) enum OracleInjectionMode<'a> {
438    LemmaBinding,
439    /// Like `LemmaBinding` but project through the subtype carrier
440    /// for classified `Generative` / `GenerativeOutput` effect-givens
441    /// — `g.name` becomes `g.name.val` in the rewritten expression.
442    /// Used by the Lean backend where lifted theorems quantify over
443    /// the constrained subtype (`RandomIntInBounds`) instead of the
444    /// plain function type, so call sites need to peel the carrier.
445    /// Dafny stays on `LemmaBinding` (no first-class subtype types
446    /// over functions); the bound is enforced via `requires` on the
447    /// emitted lemma instead.
448    LemmaBindingProjected,
449    #[allow(dead_code)]
450    SampleValue,
451    SampleCaseBinding(&'a [(String, crate::ast::Spanned<Expr>)]),
452}
453
454/// Oracle v1: rewrite any call to an effectful fn in a law body so
455/// it targets the lifted signature — prepend `BranchPath.root()` (for
456/// generative / gen+output effects) plus one argument per classified
457/// non-output effect in the callee's signature.
458///
459/// Backend-agnostic — operates on AST + `CodegenContext`. Both the
460/// Dafny and Lean backends call this before emitting the law body so
461/// the law statement matches the lifted fn shape emitted alongside.
462pub(crate) fn rewrite_effectful_calls_in_law(
463    expr: &crate::ast::Spanned<Expr>,
464    law: &crate::ast::VerifyLaw,
465    ctx: &CodegenContext,
466    mode: OracleInjectionMode,
467) -> crate::ast::Spanned<Expr> {
468    use crate::ast::{Spanned, VerifyGivenDomain};
469
470    let injection_by_effect: std::collections::HashMap<String, Spanned<Expr>> = law
471        .givens
472        .iter()
473        .filter_map(|g| {
474            let arg_expr = match &mode {
475                OracleInjectionMode::LemmaBinding => Spanned {
476                    node: Expr::Ident(g.name.clone()),
477                    line: expr.line,
478                },
479                OracleInjectionMode::LemmaBindingProjected => {
480                    // Inject the bare oracle name; the post-rewrite pass
481                    // `project_oracle_direct_calls` walks the whole
482                    // expression once and lifts every reference to a
483                    // subtype-carried oracle (callee, arg, comparison
484                    // LHS, ...) through `.val`. Doing the projection
485                    // here as well would compound — `Attr(Attr(rng,
486                    // val), val)` for refs the injection wraps.
487                    Spanned {
488                        node: Expr::Ident(g.name.clone()),
489                        line: expr.line,
490                    }
491                }
492                OracleInjectionMode::SampleValue => match &g.domain {
493                    VerifyGivenDomain::Explicit(vals) => vals.first().cloned()?,
494                    _ => return None,
495                },
496                OracleInjectionMode::SampleCaseBinding(case_bindings) => case_bindings
497                    .iter()
498                    .find(|(name, _)| name == &g.name)
499                    .map(|(_, v)| v.clone())?,
500            };
501            Some((g.type_name.clone(), arg_expr))
502        })
503        .collect();
504    let rewritten = rewrite_effectful_call(expr, &injection_by_effect, ctx);
505
506    // For `LemmaBindingProjected`, oracle bindings live as subtypes
507    // (`RandomIntInBounds` etc.); direct calls `rng(path, n, min, max)`
508    // in the law body need to peel `.val` off the carrier. Walk the
509    // rewritten expression once more and rewrite direct
510    // `FnCall(Ident(<oracle_name>), args)` shapes into
511    // `FnCall(Attr(Ident(<oracle_name>), "val"), args)` for every
512    // classified Generative-shape given. Other modes leave the body
513    // alone.
514    if matches!(mode, OracleInjectionMode::LemmaBindingProjected) {
515        let oracle_names: std::collections::HashSet<String> = law
516            .givens
517            .iter()
518            .filter(|g| {
519                matches!(
520                    crate::types::checker::effect_classification::classify(&g.type_name)
521                        .map(|c| c.dimension),
522                    Some(crate::types::checker::effect_classification::EffectDimension::Generative)
523                        | Some(
524                            crate::types::checker::effect_classification::EffectDimension::GenerativeOutput
525                        )
526                )
527            })
528            .map(|g| g.name.clone())
529            .collect();
530        if !oracle_names.is_empty() {
531            return project_oracle_direct_calls(&rewritten, &oracle_names);
532        }
533    }
534    rewritten
535}
536
537/// Rewrite every reference to a subtype-carried oracle so the surrounding
538/// expression type-checks against the carrier:
539///
540/// * Bare ident `rng` → `rng.val` (when `rng` is passed as an argument
541///   to a helper, or compared with `=` in a domain-premise / `when`
542///   clause).
543/// * Direct call `rng(args...)` → `rng.val(args...)` (the underlying
544///   function call site).
545///
546/// Recursive over the whole expression. In nested expressions like
547/// `Result.Ok(rng(p, n, 1, 6))` or `pairSpec(BranchPath.Root, rng)`,
548/// each oracle reference is projected exactly once.
549fn project_oracle_direct_calls(
550    expr: &crate::ast::Spanned<Expr>,
551    oracle_names: &std::collections::HashSet<String>,
552) -> crate::ast::Spanned<Expr> {
553    use crate::ast::Spanned;
554    let line = expr.line;
555    let project_ident = |name: &str, line: usize| -> Spanned<Expr> {
556        Spanned {
557            node: Expr::Attr(
558                Box::new(Spanned {
559                    node: Expr::Ident(name.to_string()),
560                    line,
561                }),
562                "val".to_string(),
563            ),
564            line,
565        }
566    };
567    let new_node = match &expr.node {
568        // Bare ident reference to a subtype-carried oracle — project.
569        // Catches helper-call args (`pairSpec(root, rng)`) and any
570        // other position where the oracle name appears as a value.
571        Expr::Ident(name) if oracle_names.contains(name) => {
572            return project_ident(name, line);
573        }
574        Expr::FnCall(callee, args) => {
575            let new_args: Vec<Spanned<Expr>> = args
576                .iter()
577                .map(|a| project_oracle_direct_calls(a, oracle_names))
578                .collect();
579            // `rng(...)` direct call — project the callee.
580            let new_callee = if let Expr::Ident(name) = &callee.node
581                && oracle_names.contains(name)
582            {
583                project_ident(name, callee.line)
584            } else {
585                project_oracle_direct_calls(callee, oracle_names)
586            };
587            Expr::FnCall(Box::new(new_callee), new_args)
588        }
589        Expr::Constructor(name, Some(arg)) => Expr::Constructor(
590            name.clone(),
591            Some(Box::new(project_oracle_direct_calls(arg, oracle_names))),
592        ),
593        Expr::Attr(obj, field) => Expr::Attr(
594            Box::new(project_oracle_direct_calls(obj, oracle_names)),
595            field.clone(),
596        ),
597        Expr::BinOp(op, l, r) => Expr::BinOp(
598            *op,
599            Box::new(project_oracle_direct_calls(l, oracle_names)),
600            Box::new(project_oracle_direct_calls(r, oracle_names)),
601        ),
602        other => other.clone(),
603    };
604    Spanned {
605        node: new_node,
606        line,
607    }
608}
609
610fn rewrite_effectful_call(
611    expr: &crate::ast::Spanned<Expr>,
612    injection_by_effect: &std::collections::HashMap<String, crate::ast::Spanned<Expr>>,
613    ctx: &CodegenContext,
614) -> crate::ast::Spanned<Expr> {
615    use crate::ast::Spanned;
616    use crate::types::checker::effect_classification::{EffectDimension, classify};
617
618    match &expr.node {
619        Expr::FnCall(callee, args) => {
620            let rewritten_args: Vec<Spanned<Expr>> = args
621                .iter()
622                .map(|a| rewrite_effectful_call(a, injection_by_effect, ctx))
623                .collect();
624            let rewritten_callee =
625                Box::new(rewrite_effectful_call(callee, injection_by_effect, ctx));
626
627            let callee_name = match &callee.node {
628                Expr::Ident(name) => Some(name.clone()),
629                Expr::Resolved { name, .. } => Some(name.clone()),
630                _ => None,
631            };
632
633            if let Some(name) = callee_name
634                && let Some(fd) = ctx.fn_defs.iter().find(|fd| fd.name == name)
635                && !fd.effects.is_empty()
636                && fd
637                    .effects
638                    .iter()
639                    .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
640            {
641                let mut injected: Vec<Spanned<Expr>> = Vec::new();
642                let needs_path = fd.effects.iter().any(|e| {
643                    matches!(
644                        classify(&e.node).map(|c| c.dimension),
645                        Some(EffectDimension::Generative | EffectDimension::GenerativeOutput)
646                    )
647                });
648                if needs_path {
649                    injected.push(Spanned {
650                        // `BranchPath.Root` — nullary value
651                        // constructor (PascalCase, no parens),
652                        // symmetric with `Option.None`.
653                        node: Expr::Attr(
654                            Box::new(Spanned {
655                                node: Expr::Ident("BranchPath".to_string()),
656                                line: expr.line,
657                            }),
658                            "Root".to_string(),
659                        ),
660                        line: expr.line,
661                    });
662                }
663                let mut seen = std::collections::HashSet::new();
664                for e in &fd.effects {
665                    if !seen.insert(e.node.clone()) {
666                        continue;
667                    }
668                    let Some(c) = classify(&e.node) else { continue };
669                    if matches!(c.dimension, EffectDimension::Output) {
670                        continue;
671                    }
672                    if let Some(inj) = injection_by_effect.get(&e.node) {
673                        injected.push(inj.clone());
674                    }
675                }
676                injected.extend(rewritten_args);
677                return Spanned {
678                    node: Expr::FnCall(rewritten_callee, injected),
679                    line: expr.line,
680                };
681            }
682
683            Spanned {
684                node: Expr::FnCall(rewritten_callee, rewritten_args),
685                line: expr.line,
686            }
687        }
688        Expr::BinOp(op, l, r) => Spanned {
689            node: Expr::BinOp(
690                *op,
691                Box::new(rewrite_effectful_call(l, injection_by_effect, ctx)),
692                Box::new(rewrite_effectful_call(r, injection_by_effect, ctx)),
693            ),
694            line: expr.line,
695        },
696        Expr::Tuple(items) => Spanned {
697            node: Expr::Tuple(
698                items
699                    .iter()
700                    .map(|i| rewrite_effectful_call(i, injection_by_effect, ctx))
701                    .collect(),
702            ),
703            line: expr.line,
704        },
705        _ => expr.clone(),
706    }
707}
708
709/// Oracle v1: set of user fn names that are reachable from any verify
710/// block — directly (`verify f ...`) or through the call graph (fn
711/// body of a reachable fn mentions them). Used by proof backends to
712/// skip emission of effectful fns that nobody verifies. Dead code in
713/// a proof output isn't just ugly — a non-terminating effectful fn
714/// (e.g. a REPL loop) will make Lean reject the whole module because
715/// it can't prove termination for a fn with no decreasing argument.
716/// If the user never asked for a proof about that fn, don't force
717/// the backend to invent one.
718pub(crate) fn verify_reachable_fn_names(items: &[TopLevel]) -> HashSet<String> {
719    let mut reachable: HashSet<String> = HashSet::new();
720    for item in items {
721        if let TopLevel::Verify(vb) = item {
722            collect_verify_block_refs(vb, &mut reachable);
723        }
724    }
725    // Fixed-point closure through the call graph.
726    loop {
727        let mut changed = false;
728        for item in items {
729            if let TopLevel::FnDef(fd) = item
730                && reachable.contains(&fd.name)
731            {
732                let mut called = HashSet::new();
733                collect_called_idents_in_body(&fd.body, &mut called);
734                for name in called {
735                    if reachable.insert(name) {
736                        changed = true;
737                    }
738                }
739            }
740        }
741        if !changed {
742            break;
743        }
744    }
745    reachable
746}
747
748fn collect_verify_block_refs(vb: &VerifyBlock, out: &mut HashSet<String>) {
749    out.insert(vb.fn_name.clone());
750    for (lhs, rhs) in &vb.cases {
751        collect_called_idents(lhs, out);
752        collect_called_idents(rhs, out);
753    }
754    if let VerifyKind::Law(law) = &vb.kind {
755        collect_called_idents(&law.lhs, out);
756        collect_called_idents(&law.rhs, out);
757        if let Some(when) = &law.when {
758            collect_called_idents(when, out);
759        }
760        for given in &law.givens {
761            if let VerifyGivenDomain::Explicit(values) = &given.domain {
762                for v in values {
763                    collect_called_idents(v, out);
764                }
765            }
766        }
767    }
768    for given in &vb.cases_givens {
769        if let VerifyGivenDomain::Explicit(values) = &given.domain {
770            for v in values {
771                collect_called_idents(v, out);
772            }
773        }
774    }
775}
776
777fn collect_called_idents_in_body(body: &FnBody, out: &mut HashSet<String>) {
778    for stmt in body.stmts() {
779        match stmt {
780            Stmt::Binding(_, _, e) | Stmt::Expr(e) => collect_called_idents(e, out),
781        }
782    }
783}
784
785fn collect_called_idents(expr: &Spanned<Expr>, out: &mut HashSet<String>) {
786    match &expr.node {
787        Expr::FnCall(callee, args) => {
788            if let Expr::Ident(name) | Expr::Resolved { name, .. } = &callee.node {
789                out.insert(name.clone());
790            } else {
791                collect_called_idents(callee, out);
792            }
793            for a in args {
794                collect_called_idents(a, out);
795            }
796        }
797        Expr::TailCall(boxed) => {
798            let TailCallData { target, args, .. } = boxed.as_ref();
799            out.insert(target.clone());
800            for a in args {
801                collect_called_idents(a, out);
802            }
803        }
804        Expr::Ident(name) | Expr::Resolved { name, .. } => {
805            out.insert(name.clone());
806        }
807        Expr::BinOp(_, l, r) => {
808            collect_called_idents(l, out);
809            collect_called_idents(r, out);
810        }
811        Expr::Match { subject, arms, .. } => {
812            collect_called_idents(subject, out);
813            for arm in arms {
814                collect_called_idents(&arm.body, out);
815            }
816        }
817        Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
818            collect_called_idents(inner, out);
819        }
820        Expr::Constructor(_, Some(inner)) => {
821            collect_called_idents(inner, out);
822        }
823        Expr::InterpolatedStr(parts) => {
824            for part in parts {
825                if let StrPart::Parsed(inner) = part {
826                    collect_called_idents(inner, out);
827                }
828            }
829        }
830        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
831            for i in items {
832                collect_called_idents(i, out);
833            }
834        }
835        Expr::MapLiteral(entries) => {
836            for (k, v) in entries {
837                collect_called_idents(k, out);
838                collect_called_idents(v, out);
839            }
840        }
841        Expr::RecordCreate { fields, .. } => {
842            for (_, v) in fields {
843                collect_called_idents(v, out);
844            }
845        }
846        Expr::RecordUpdate { base, updates, .. } => {
847            collect_called_idents(base, out);
848            for (_, v) in updates {
849                collect_called_idents(v, out);
850            }
851        }
852        Expr::Literal(_) | Expr::Constructor(_, None) => {}
853    }
854}
855
856/// Sections gathered per emission scope ("" for entry, module prefix
857/// otherwise). Each backend appends to the bucket for the scope a fn
858/// (or its SCC component) belongs to.
859pub(crate) struct PerScopeSections {
860    pub by_scope: std::collections::HashMap<String, Vec<String>>,
861}
862
863impl PerScopeSections {
864    pub(crate) fn take(&mut self, scope: &str) -> Vec<String> {
865        self.by_scope.remove(scope).unwrap_or_default()
866    }
867}
868
869/// Run SCC analysis on each scope's pure fns independently and route the
870/// rendered output through the supplied closure. Lean and Dafny share
871/// this — each scope (entry or dependent module) is SCC-analyzed in
872/// isolation so a `def foo` in one module and an unrelated `def foo` in
873/// another module don't get conflated.
874///
875/// `is_pure` filters which fns participate; `emit` renders one SCC
876/// component (>= 1 fn) into the lines to append to that scope's bucket.
877pub(crate) fn route_pure_components_per_scope<F, G>(
878    ctx: &CodegenContext,
879    is_pure: F,
880    mut emit: G,
881) -> PerScopeSections
882where
883    F: Fn(&FnDef) -> bool,
884    G: FnMut(&[&FnDef]) -> Vec<String>,
885{
886    let mut by_scope: std::collections::HashMap<String, Vec<String>> =
887        std::collections::HashMap::new();
888
889    let mut process =
890        |fns: Vec<&FnDef>,
891         scope: String,
892         by_scope: &mut std::collections::HashMap<String, Vec<String>>| {
893            let comps = crate::call_graph::ordered_fn_components(&fns, &ctx.module_prefixes);
894            let bucket = by_scope.entry(scope).or_default();
895            for comp in comps {
896                bucket.extend(emit(&comp));
897            }
898        };
899
900    for module in &ctx.modules {
901        let pure: Vec<&FnDef> = module.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
902        process(pure, module.prefix.clone(), &mut by_scope);
903    }
904    let entry_pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
905    process(entry_pure, String::new(), &mut by_scope);
906
907    PerScopeSections { by_scope }
908}