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}