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