Skip to main content

aver/codegen/rust/
emit_ctx.rs

1/// Rust-specific emission context for type and borrow policy.
2///
3/// Clone/move decisions now come from `last_use` annotations on
4/// `ResolvedExpr::Resolved` nodes (set by `ir::last_use` and lifted by the
5/// resolver pass), NOT from name-based liveness sets. EmitCtx provides
6/// only Rust-specific policy: Copy types, borrow semantics, Rc wrapping.
7use crate::ir::hir::ResolvedExpr;
8use crate::types::Type;
9use std::collections::{HashMap, HashSet};
10
11/// Emission context carrying Rust-specific type/borrow policy.
12#[derive(Clone)]
13pub struct EmitCtx {
14    /// Local variable types (from fn params) for copy-type elision.
15    pub local_types: HashMap<String, Type>,
16    /// Parameters passed as `Rc<T>` (self-TCO) or `&T` (mutual-TCO pass-through).
17    pub rc_wrapped: HashSet<String>,
18    /// Parameters emitted as `&T` borrows (borrow-by-default for non-Copy, non-Str params).
19    pub borrowed_params: HashSet<String>,
20    /// Owning module prefix for the function whose body this context
21    /// is emitting (`Some("Domain.Eval.Core")` inside a dep module's
22    /// fn body, `None` for entry-scope fns). Threaded into
23    /// `ctx.resolve_expr` / `ctx.resolve_stmt` / `ctx.resolve_pattern`
24    /// from the on-demand legacy emit helpers so cross-module ctor /
25    /// fn classification uses the right resolver `current_module`
26    /// instead of falling back to the entry's. Pre-PR-9.4 the legacy
27    /// helpers used the entry-module name uniformly, which silently
28    /// mis-resolved cross-module `Type.Variant(...)` calls inside
29    /// trampoline arms (self-host regen exposure).
30    pub current_module_scope: Option<String>,
31}
32
33impl EmitCtx {
34    /// Empty context — conservative (clones everything non-Copy).
35    pub fn empty() -> Self {
36        EmitCtx {
37            local_types: HashMap::new(),
38            rc_wrapped: HashSet::new(),
39            borrowed_params: HashSet::new(),
40            current_module_scope: None,
41        }
42    }
43
44    /// Build context for a function with known parameter types.
45    /// Automatically computes `borrowed_params` from param types.
46    pub fn for_fn(param_types: HashMap<String, Type>) -> Self {
47        let borrowed_params = param_types
48            .iter()
49            .filter(|(_, ty)| should_borrow_param(ty))
50            .map(|(name, _)| name.clone())
51            .collect();
52        EmitCtx {
53            local_types: param_types,
54            rc_wrapped: HashSet::new(),
55            borrowed_params,
56            current_module_scope: None,
57        }
58    }
59
60    /// Build context for a function WITHOUT borrow-by-default (e.g. TCO).
61    pub fn for_fn_no_borrow(param_types: HashMap<String, Type>) -> Self {
62        EmitCtx {
63            local_types: param_types,
64            rc_wrapped: HashSet::new(),
65            borrowed_params: HashSet::new(),
66            current_module_scope: None,
67        }
68    }
69
70    /// Stamp the owning module prefix onto a context — chains
71    /// fluently after `for_fn` / `for_fn_no_borrow`. `None` for
72    /// entry-scope fns keeps the field default.
73    pub fn with_scope(mut self, scope: Option<&str>) -> Self {
74        self.current_module_scope = scope.map(String::from);
75        self
76    }
77
78    /// Is this variable a Copy type in Rust (i64, f64, bool, ())?
79    pub fn is_copy(&self, name: &str) -> bool {
80        self.local_types.get(name).is_some_and(is_copy_type)
81    }
82
83    /// Is this variable a pass-through parameter (Rc<T> in self-TCO, &T in mutual-TCO)?
84    pub fn is_rc_wrapped(&self, name: &str) -> bool {
85        self.rc_wrapped.contains(name)
86    }
87
88    /// Is this variable a borrowed parameter (`&T` from borrow-by-default)?
89    pub fn is_borrowed_param(&self, name: &str) -> bool {
90        self.borrowed_params.contains(name)
91    }
92
93    /// Create a context with specified Rc-wrapped parameters (TCO pass-through).
94    pub fn with_rc_wrapped(&self, rc: HashSet<String>) -> Self {
95        EmitCtx {
96            local_types: self.local_types.clone(),
97            rc_wrapped: rc,
98            borrowed_params: self.borrowed_params.clone(),
99            current_module_scope: self.current_module_scope.clone(),
100        }
101    }
102}
103
104// ── Expression-level move/clone decisions ───────────────────────────────
105
106/// Can this expression be moved (not cloned)?
107/// Checks `last_use` on Resolved nodes; Ident without local_types entry
108/// is treated as a global (always moveable).
109pub fn expr_can_move(expr: &ResolvedExpr) -> bool {
110    match expr {
111        ResolvedExpr::Resolved { last_use, .. } => last_use.0,
112        ResolvedExpr::Ident(_) => true, // globals/namespaces never need clone
113        _ => false,
114    }
115}
116
117/// Should `.clone()` be skipped for this expression?
118/// True for: Copy types, last-use locals, globals/namespaces.
119/// False for: rc_wrapped, borrowed_params (need special clone paths).
120///
121/// For `ResolvedExpr::Ident`: in Rust codegen, Ident is used for both
122/// globals AND locals (resolver still leaves bare globals as Ident).
123/// Check ectx to distinguish:
124/// - If name is in local_types → local/param, apply rc_wrapped/borrowed checks
125/// - If name is NOT in local_types → global/namespace, skip clone
126pub fn expr_skip_clone(expr: &ResolvedExpr, ectx: &EmitCtx) -> bool {
127    match expr {
128        ResolvedExpr::Resolved { name, last_use, .. } => {
129            if ectx.rc_wrapped.contains(name.as_str()) {
130                return false;
131            }
132            if ectx.borrowed_params.contains(name.as_str()) {
133                return false;
134            }
135            last_use.0 || ectx.is_copy(name)
136        }
137        ResolvedExpr::Ident(name) => {
138            // If not a known local, treat as global/namespace — skip clone
139            if !ectx.local_types.contains_key(name.as_str()) {
140                return true;
141            }
142            // Known local: check special categories
143            if ectx.rc_wrapped.contains(name.as_str()) {
144                return false;
145            }
146            if ectx.borrowed_params.contains(name.as_str()) {
147                return false;
148            }
149            // Without last_use info, only skip for Copy types
150            ectx.is_copy(name)
151        }
152        _ => false,
153    }
154}
155
156// ── Rust-specific policy ──────────────────────────────────────────────
157
158/// Is a Type Copy in Rust? (Float, Bool, Unit)
159///
160/// `Int` is NOT Copy: it now lowers to `aver_rt::AverInt`, which is
161/// `Clone`-only (the `Big` variant boxes a `BigInt`). Dropping `Int` from
162/// this set is what flips every owning-position non-last-use Int read to a
163/// `.clone()` — cheap for the common `Small` case (an `i64` copy + tag).
164pub fn is_copy_type(ty: &Type) -> bool {
165    matches!(ty, Type::Float | Type::Bool | Type::Unit)
166}
167
168/// Should this param be borrowed (`&T`) instead of owned?
169pub fn should_borrow_param(ty: &Type) -> bool {
170    matches!(
171        ty,
172        Type::Map(_, _)
173            | Type::List(_)
174            | Type::Vector(_)
175            | Type::Result(_, _)
176            | Type::Option(_)
177            | Type::Tuple(_)
178            | Type::Named { .. }
179    )
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_is_copy_type() {
188        // Int lowers to the non-Copy `aver_rt::AverInt`.
189        assert!(!is_copy_type(&Type::Int));
190        assert!(is_copy_type(&Type::Float));
191        assert!(is_copy_type(&Type::Bool));
192        assert!(is_copy_type(&Type::Unit));
193        assert!(!is_copy_type(&Type::Str));
194        assert!(!is_copy_type(&Type::List(Box::new(Type::Int))));
195        assert!(!is_copy_type(&Type::named("Foo")));
196    }
197}