Skip to main content

cljrs_runtime/tiered/
lower.rs

1//! Rust-native IR lowering orchestration.
2//!
3//! Calls the Rust `cljrs_ir::lower` pipeline directly (no Clojure interpreter
4//! round-trip).  Macro expansion still runs through the interpreter since
5//! macros are user-defined Clojure functions.
6
7use std::sync::Arc;
8
9use cljrs_ir::lower::CoreShadows;
10use cljrs_ir::{IrFunction, Repr};
11use cljrs_reader::Form;
12use cljrs_value::TypeHint;
13
14use crate::builtins::form::resolve_auto_forms;
15use crate::env::env::{Env, GlobalEnv};
16
17/// Map per-parameter primitive type hints onto representation seeds for type
18/// inference, positional with the function's fixed parameters.  `^long`/`^int`
19/// → [`Repr::Long`], `^double`/`^float` → [`Repr::Double`], `^boolean` →
20/// [`Repr::Bool`].  Array hints and unrecognized/absent hints seed [`Repr::Boxed`]
21/// (no scalar unboxing).  Returns an empty vec when no hint is usable, so the
22/// caller leaves `IrFunction::seed_reprs` empty.
23pub fn seed_reprs_from_hints(param_hints: &[Option<TypeHint>]) -> Vec<Repr> {
24    if !param_hints.iter().any(|h| {
25        matches!(
26            h,
27            Some(
28                TypeHint::Long
29                    | TypeHint::Int
30                    | TypeHint::Double
31                    | TypeHint::Float
32                    | TypeHint::Bool
33                    | TypeHint::LongArray
34                    | TypeHint::DoubleArray
35            )
36        )
37    }) {
38        return Vec::new();
39    }
40    param_hints
41        .iter()
42        .map(|h| match h {
43            Some(TypeHint::Long | TypeHint::Int) => Repr::Long,
44            Some(TypeHint::Double | TypeHint::Float) => Repr::Double,
45            Some(TypeHint::Bool) => Repr::Bool,
46            // `^longs`/`^doubles` enable unboxed element access via aget/aset.
47            Some(TypeHint::LongArray) => Repr::LongArray,
48            Some(TypeHint::DoubleArray) => Repr::DoubleArray,
49            // Other array hints / absent / non-primitive hints stay boxed (they
50            // still work through the boxed array bridge).
51            _ => Repr::Boxed,
52        })
53        .collect()
54}
55
56/// What `ns` binds that `clojure.core` also publishes, for the lowerer's
57/// call-position core check (see [`cljrs_ir::lower::CoreShadows`]).
58///
59/// Must be sampled on the mutator thread: it reads the namespace tables, which
60/// the background lowering worker may not touch.
61pub fn core_shadows_for(globals: &GlobalEnv, ns: &str) -> CoreShadows {
62    CoreShadows::new(globals.core_shadowed_names(ns))
63}
64
65// ── Error type ──────────────────────────────────────────────────────────────
66
67#[derive(Debug)]
68pub enum LowerError {
69    /// The Rust lowering function failed.
70    LowerFailed(String),
71}
72
73impl std::fmt::Display for LowerError {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            LowerError::LowerFailed(msg) => write!(f, "lowering failed: {msg}"),
77        }
78    }
79}
80
81// ── Public entry point ──────────────────────────────────────────────────────
82
83/// Lower a function arity's body to IR using the native Rust compiler pipeline.
84///
85/// `destructure_params` carries the original destructuring patterns for any
86/// parameters the interpreter replaced with gensym placeholders (each paired
87/// with its index into `params`); `destructure_rest` is the rest parameter's
88/// pattern, if it is itself destructured.  Both are expanded into explicit
89/// bindings in the IR prologue.
90#[allow(clippy::too_many_arguments)]
91pub fn lower_arity(
92    name: Option<&str>,
93    params: &[Arc<str>],
94    rest_param: Option<&Arc<str>>,
95    destructure_params: &[(usize, Form)],
96    destructure_rest: Option<&Form>,
97    body: &[Form],
98    ns: &Arc<str>,
99    env: &mut Env,
100    is_async: bool,
101) -> Result<IrFunction, LowerError> {
102    lower_arity_inner(
103        name,
104        params,
105        rest_param,
106        destructure_params,
107        destructure_rest,
108        body,
109        ns,
110        env,
111        false,
112        is_async,
113    )
114    .map(|(ir, _)| ir)
115}
116
117/// Like [`lower_arity`], but also runs the region-optimization pass.
118#[allow(clippy::too_many_arguments)]
119pub fn lower_and_optimize_arity(
120    name: Option<&str>,
121    params: &[Arc<str>],
122    rest_param: Option<&Arc<str>>,
123    destructure_params: &[(usize, Form)],
124    destructure_rest: Option<&Form>,
125    body: &[Form],
126    ns: &Arc<str>,
127    env: &mut Env,
128    is_async: bool,
129) -> Result<IrFunction, LowerError> {
130    lower_and_optimize_arity_tracked(
131        name,
132        params,
133        rest_param,
134        destructure_params,
135        destructure_rest,
136        body,
137        ns,
138        env,
139        is_async,
140    )
141    .map(|(ir, _)| ir)
142}
143
144/// Like [`lower_and_optimize_arity`], but also returns the `(ns, name)` set
145/// of cross-defn externals (see [`crate::tiered::defn_registry`]) the optimizer
146/// consulted — the caller must register those as invalidation dependencies.
147#[allow(clippy::too_many_arguments)]
148pub fn lower_and_optimize_arity_tracked(
149    name: Option<&str>,
150    params: &[Arc<str>],
151    rest_param: Option<&Arc<str>>,
152    destructure_params: &[(usize, Form)],
153    destructure_rest: Option<&Form>,
154    body: &[Form],
155    ns: &Arc<str>,
156    env: &mut Env,
157    is_async: bool,
158) -> Result<(IrFunction, Vec<(Arc<str>, Arc<str>)>), LowerError> {
159    lower_arity_inner(
160        name,
161        params,
162        rest_param,
163        destructure_params,
164        destructure_rest,
165        body,
166        ns,
167        env,
168        true,
169        is_async,
170    )
171}
172
173#[allow(clippy::too_many_arguments)]
174fn lower_arity_inner(
175    name: Option<&str>,
176    params: &[Arc<str>],
177    rest_param: Option<&Arc<str>>,
178    destructure_params: &[(usize, Form)],
179    destructure_rest: Option<&Form>,
180    body: &[Form],
181    ns: &Arc<str>,
182    env: &mut Env,
183    do_optimize: bool,
184    is_async: bool,
185) -> Result<(IrFunction, Vec<(Arc<str>, Arc<str>)>), LowerError> {
186    // Temporarily set current_ns to the function's defining namespace so that
187    // ::kw resolution in macroexpand_body uses the correct namespace.
188    let prev_ns = std::mem::replace(&mut env.current_ns, ns.clone());
189    let expanded_body = macroexpand_body(body, env);
190    // Auto-resolved identifiers are qualified here, the last boundary holding
191    // an Env; the lowerer refuses any that survive. An unresolvable one is left
192    // in place for the lowerer to refuse, so the tree-walker reports it.
193    let resolved: Result<Vec<Form>, _> = expanded_body
194        .iter()
195        .map(|f| resolve_auto_forms(f, env))
196        .collect();
197    let expanded_body = resolved.unwrap_or(expanded_body);
198    env.current_ns = prev_ns;
199    let shadows = core_shadows_for(&env.globals, ns);
200    lower_expanded_arity(
201        name,
202        params,
203        rest_param,
204        destructure_params,
205        destructure_rest,
206        &expanded_body,
207        ns,
208        &shadows,
209        env.globals.id(),
210        None,
211        do_optimize,
212        is_async,
213    )
214}
215
216/// Macro-expand a function body on the calling thread.
217///
218/// Macros are user-defined Clojure functions, so expansion must run through
219/// the interpreter with a live `Env` — this is the only part of lowering that
220/// cannot move to the background worker.  Forms that fail to expand are kept
221/// unexpanded (lowering will reject them if they matter).
222pub fn macroexpand_body(body: &[Form], env: &mut Env) -> Vec<Form> {
223    // Guard against re-entrant lowering during macro expansion.
224    use crate::tiered::apply::IR_LOWERING_ACTIVE;
225    let was_active = IR_LOWERING_ACTIVE.get();
226    IR_LOWERING_ACTIVE.set(true);
227
228    let expanded_body: Vec<Form> = body
229        .iter()
230        .map(|f| crate::interp::macros::macroexpand_all(f, env).unwrap_or_else(|_| f.clone()))
231        .collect();
232
233    IR_LOWERING_ACTIVE.with(|c| c.set(was_active));
234    expanded_body
235}
236
237/// Lower an already macro-expanded arity body to (optionally optimized) IR.
238///
239/// Env-free and callable from the background lowering worker (Phase 10.7):
240/// everything below operates on plain `Form`/IR data.  `shadows` is the one
241/// piece that has to be sampled from the environment first — see
242/// [`core_shadows_for`].  `globals_id` is
243/// `GlobalEnv::id`, scoping the cross-defn registry lookups to one runtime.
244///
245/// `arity_id` selects the externals protocol:
246/// - `Some(id)` (background worker): externals are fetched via
247///   `defn_registry::snapshot_externals`, which atomically records the
248///   dependent edges while holding the registry lock — required off the
249///   mutator thread, where a rebind can interleave with lowering.
250/// - `None` (synchronous mutator-thread callers): legacy `externals_for`;
251///   the caller records dependents from the returned `used` set, which is
252///   race-free on the single mutator thread.
253#[allow(clippy::too_many_arguments)]
254pub fn lower_expanded_arity(
255    name: Option<&str>,
256    params: &[Arc<str>],
257    rest_param: Option<&Arc<str>>,
258    destructure_params: &[(usize, Form)],
259    destructure_rest: Option<&Form>,
260    expanded_body: &[Form],
261    ns: &Arc<str>,
262    shadows: &CoreShadows,
263    globals_id: u64,
264    arity_id: Option<u64>,
265    do_optimize: bool,
266    is_async: bool,
267) -> Result<(IrFunction, Vec<(Arc<str>, Arc<str>)>), LowerError> {
268    tracing::debug!(
269        target: "lower",
270        "lowering {:?}/{:?} optimize? {}",
271        ns,
272        name,
273        do_optimize
274    );
275
276    // Build the flat params list (includes rest param as last element if present).
277    let mut all_params: Vec<Arc<str>> = params.to_vec();
278    if let Some(rest) = rest_param {
279        all_params.push(rest.clone());
280    }
281
282    // Build the combined destructuring list, indexed into `all_params`.  Fixed
283    // params keep their recorded index; the rest param, if destructured, sits at
284    // the final position (`params.len()`).
285    let mut destructures: Vec<(usize, Form)> = destructure_params.to_vec();
286    if let Some(rest_pat) = destructure_rest {
287        destructures.push((params.len(), rest_pat.clone()));
288    }
289
290    let ir = cljrs_ir::lower::lower_fn_body_shadowed(
291        name,
292        ns,
293        &all_params,
294        &destructures,
295        expanded_body,
296        is_async,
297        shadows,
298    )
299    .map_err(|e| LowerError::LowerFailed(format!("{e:?}")))?;
300
301    if !do_optimize {
302        return Ok((ir, Vec::new()));
303    }
304
305    // Make previously-lowered defns this function references visible to
306    // escape analysis and stage-4 cross-function region promotion (the
307    // script/REPL counterpart of AOT's whole-program lowering).
308    let referenced = referenced_globals(&ir);
309    let externals = match arity_id {
310        Some(id) => crate::tiered::defn_registry::snapshot_externals(globals_id, id, &referenced),
311        None => crate::tiered::defn_registry::externals_for(globals_id, &referenced),
312    };
313    let (ir, used) = cljrs_ir::lower::optimize_with_externals(ir, &externals);
314    Ok((ir, used.into_iter().collect()))
315}
316
317/// Collect every `(ns, name)` pair the IR tree loads as a global — the
318/// candidate set of cross-defn externals.
319fn referenced_globals(ir: &IrFunction) -> std::collections::HashSet<(Arc<str>, Arc<str>)> {
320    use cljrs_ir::Inst;
321    let mut out = std::collections::HashSet::new();
322    fn walk(f: &IrFunction, out: &mut std::collections::HashSet<(Arc<str>, Arc<str>)>) {
323        for block in &f.blocks {
324            for inst in block.phis.iter().chain(block.insts.iter()) {
325                if let Inst::LoadGlobal(_, ns, name) | Inst::LoadVar(_, ns, name) = inst {
326                    out.insert((ns.clone(), name.clone()));
327                }
328            }
329        }
330        for sub in &f.subfunctions {
331            walk(sub, out);
332        }
333    }
334    walk(ir, &mut out);
335    out
336}