Skip to main content

logicaffeine_compile/codegen/
hoist.rs

1//! O1 — borrow hoisting (scoped slice extraction).
2//!
3//! A loop body that indexes `LogosSeq` handles pays one `RefCell` flag
4//! operation per access. When the handles provably keep their identity for
5//! the whole loop, the borrow moves to a scope around the loop and the body
6//! indexes plain slices:
7//!
8//! ```text
9//! {
10//!     let __prev_g = prev.borrow();
11//!     let prev = &__prev_g[..];
12//!     let mut __curr_g = curr.borrow_mut();
13//!     let curr = &mut __curr_g[..];
14//!     for w in 0..n { curr[w as usize] = prev[w as usize]; }
15//! }
16//! ```
17//!
18//! Shadowing the original name and flipping its tracked type to `&[T]` /
19//! `&mut [T]` means every existing emission path (indexing, SetIndex, swap
20//! fusion, zero-based lowering, length) works unchanged through its slice
21//! arms.
22//!
23//! SOUNDNESS. A mut-hoisted handle holds a `RefMut` across the whole loop;
24//! a shared-hoisted handle holds a `Ref`. Any other access to the same
25//! allocation while one is held panics at runtime. A handle is therefore
26//! hoisted only when ALL of these hold:
27//!
28//!  - its tracked type is `LogosSeq<T>` (Maps, Strings, zone arenas, and
29//!    already-sliced params are out);
30//!  - the body neither rebinds, redeclares, resizes, nor pops it;
31//!  - it never appears as a bare value (pushed into a container, passed to
32//!    a call, given away, shown whole, interpolated, field-stored);
33//!  - the body contains no calls and no opaque/concurrent statements
34//!    (anything could touch any handle behind them);
35//!  - the alias oracle PROVES it distinct, at this loop's invariant, from
36//!    every other handle the loop touches (for mut hoists), or from every
37//!    handle the loop mutates (for shared hoists). Read-read aliasing is
38//!    RefCell-legal and allowed.
39//!
40//! Refusal is always the default: no oracle, no snapshot, an unknown
41//! statement kind, an unknown expression kind — hoist nothing.
42
43use std::collections::{HashMap, HashSet};
44use std::fmt::Write;
45
46use crate::analysis::types::RustNames;
47use crate::ast::stmt::{Expr, Stmt};
48use crate::intern::{Interner, Symbol};
49
50use super::context::RefinementContext;
51use super::peephole::body_modifies_var;
52
53thread_local! {
54    /// Per-thread test override for the borrow-hoist kill switch. Tests set
55    /// this on their own thread (race-free), unlike the process-global
56    /// `LOGOS_HOIST` env var.
57    static FORCE_DISABLE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
58}
59
60/// Force borrow hoisting off (or back on) for the current thread only.
61/// Used by tests to verify the kill switch without the env-var data race.
62pub fn force_disable_for_test(disabled: bool) {
63    FORCE_DISABLE.with(|c| c.set(disabled));
64}
65
66/// Is borrow hoisting disabled? True when the thread-local test override is
67/// set, or the operational `LOGOS_HOIST=0` env var is present.
68pub fn hoisting_disabled() -> bool {
69    FORCE_DISABLE.with(|c| c.get())
70        || !crate::optimize::active_config().is_on(crate::optimization::Opt::HoistBorrows)
71}
72
73/// Could a value of this tracked type share a Seq's `Rc<RefCell>` — i.e.
74/// could it conflict with a hoisted Seq borrow? Only Seq-shaped types can;
75/// known scalars, strings, and maps cannot. Unknown types are treated
76/// conservatively as "could" so an untyped handle never escapes the check.
77fn could_alias_seq(ty: Option<&String>) -> bool {
78    let t = match ty {
79        Some(t) => t,
80        None => return true,
81    };
82    let base = t.split("|__hl:").next().unwrap_or(t.as_str());
83    let definitely_not = matches!(
84        base,
85        "i64" | "f64" | "bool" | "char" | "u8" | "usize" | "i32" | "u64"
86            | "String" | "&str" | "()" | "__zero_based_i64" | "__single_char_u8"
87    ) || base.starts_with("LogosMap")
88        || base.starts_with("LogosI64Map")
89        || base.starts_with("LogosI64Set")
90        || base.starts_with("HashMap")
91        || base.starts_with("FxHashMap")
92        || base.starts_with("std::collections::HashMap")
93        || base.starts_with("rustc_hash::FxHashMap");
94    !definitely_not
95}
96
97/// How a handle is hoisted out of the loop.
98#[derive(Clone, Copy, PartialEq)]
99pub(crate) enum HoistKind {
100    /// Read-only: `let __g = x.borrow(); let x = &__g[..];` (`&[T]`).
101    Shared,
102    /// Indexed read/write, never resized: `&mut __g[..]` (`&mut [T]`).
103    MutSlice,
104    /// Resized (pushed/popped): hold the `RefMut` as a `&mut Vec` so push
105    /// goes through it — `let mut __g = x.borrow_mut(); let x = &mut *__g;`.
106    MutVec,
107}
108
109/// One handle's hoist decision.
110pub(crate) struct HoistEntry {
111    pub sym: Symbol,
112    pub kind: HoistKind,
113    pub elem_ty: String,
114    pub old_type: String,
115    /// True for a de-Rc'd `Vec<T>`: extract the noalias slice WITHOUT a
116    /// `.borrow()` (there is no `RefCell`). The slice still matters — it hands
117    /// LLVM the same `&[T]`/`&mut [T]` noalias the LogosSeq path gives, which
118    /// is what unlocks bounds-check elision and vectorization.
119    pub is_vec: bool,
120}
121
122/// A pure scalar builtin that maps to a Rust method call (sqrt, abs, …) —
123/// takes scalars, returns a scalar, never touches a Seq's `RefCell`. The
124/// name+arity pairs mirror the dispatch in codegen/expr.rs, so a body that
125/// only calls these can still be hoisted. Anything else is opaque and bails.
126fn is_pure_scalar_builtin(name: &str, argc: usize) -> bool {
127    matches!(
128        (name, argc),
129        ("sqrt", 1) | ("abs", 1) | ("floor", 1) | ("ceil", 1) | ("round", 1)
130            | ("pow", 2) | ("min", 2) | ("max", 2)
131    )
132}
133
134struct AccessRoles<'i> {
135    /// Read through a collection position (Index/Slice/Length/Contains).
136    read: HashSet<Symbol>,
137    /// Written through SetIndex.
138    written: HashSet<Symbol>,
139    /// Resized in place (Push/Pop/Add/Remove collection target). Hoistable
140    /// as a held `&mut Vec` (push goes through the RefMut) when not also
141    /// leaked.
142    resized: HashSet<Symbol>,
143    /// Leaked: bare-value uses, call args, whole-handle Show/Give/Return,
144    /// pushed-as-a-value into another collection, Repeat iterables. Never
145    /// hoistable; mut-hoists must be proven distinct from these too.
146    other: HashSet<Symbol>,
147    /// Symbols (re)bound inside the body: `Let`, `Pop into`, Repeat
148    /// patterns, Inspect bindings. A guard for these would capture a stale
149    /// or undeclared name.
150    rebound: HashSet<Symbol>,
151    /// Insertion order of first sighting, for deterministic emission.
152    order: Vec<Symbol>,
153    /// Something opaque appeared — hoist nothing in this loop.
154    bail: bool,
155    /// For resolving called-function names against the builtin whitelist.
156    interner: &'i Interner,
157}
158
159impl<'i> AccessRoles<'i> {
160    fn new(interner: &'i Interner) -> Self {
161        AccessRoles {
162            read: HashSet::new(),
163            written: HashSet::new(),
164            resized: HashSet::new(),
165            other: HashSet::new(),
166            rebound: HashSet::new(),
167            order: Vec::new(),
168            bail: false,
169            interner,
170        }
171    }
172
173    fn note(&mut self, sym: Symbol) {
174        if !self.order.contains(&sym) {
175            self.order.push(sym);
176        }
177    }
178    fn read(&mut self, sym: Symbol) {
179        self.note(sym);
180        self.read.insert(sym);
181    }
182    fn written(&mut self, sym: Symbol) {
183        self.note(sym);
184        self.written.insert(sym);
185    }
186    fn resized(&mut self, sym: Symbol) {
187        self.note(sym);
188        self.resized.insert(sym);
189    }
190    fn other(&mut self, sym: Symbol) {
191        self.note(sym);
192        self.other.insert(sym);
193    }
194}
195
196/// Walk an expression in VALUE position: collection positions of the
197/// indexing forms count as reads; any bare identifier is an `other` use
198/// (it may flow anywhere); opaque forms bail.
199fn scan_value_expr(e: &Expr, roles: &mut AccessRoles) {
200    match e {
201        Expr::Identifier(s) => roles.other(*s),
202        Expr::Literal(_) | Expr::OptionNone => {}
203        Expr::Index { collection, index } => {
204            scan_collection_pos(collection, roles);
205            scan_value_expr(index, roles);
206        }
207        Expr::Slice { collection, start, end } => {
208            scan_collection_pos(collection, roles);
209            scan_value_expr(start, roles);
210            scan_value_expr(end, roles);
211        }
212        Expr::Length { collection } => scan_collection_pos(collection, roles),
213        Expr::Contains { collection, value } => {
214            scan_collection_pos(collection, roles);
215            scan_value_expr(value, roles);
216        }
217        Expr::BinaryOp { left, right, .. } => {
218            scan_value_expr(left, roles);
219            scan_value_expr(right, roles);
220        }
221        Expr::Not { operand } => scan_value_expr(operand, roles),
222        Expr::Range { start, end } => {
223            scan_value_expr(start, roles);
224            scan_value_expr(end, roles);
225        }
226        Expr::List(items) | Expr::Tuple(items) => {
227            for it in items {
228                scan_value_expr(it, roles);
229            }
230        }
231        Expr::New { init_fields, .. } => {
232            for (_, v) in init_fields {
233                scan_value_expr(v, roles);
234            }
235        }
236        Expr::NewVariant { fields, .. } => {
237            for (_, v) in fields {
238                scan_value_expr(v, roles);
239            }
240        }
241        Expr::FieldAccess { object, .. } => scan_value_expr(object, roles),
242        Expr::Copy { expr } => scan_value_expr(expr, roles),
243        Expr::WithCapacity { value, capacity } => {
244            scan_value_expr(value, roles);
245            scan_value_expr(capacity, roles);
246        }
247        Expr::OptionSome { value } => scan_value_expr(value, roles),
248        Expr::Give { value } => scan_value_expr(value, roles),
249        Expr::InterpolatedString(parts) => {
250            for p in parts {
251                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
252                    scan_value_expr(value, roles);
253                }
254            }
255        }
256        Expr::Union { left, right } | Expr::Intersection { left, right } => {
257            scan_value_expr(left, roles);
258            scan_value_expr(right, roles);
259        }
260        Expr::Call { function, args } => {
261            // A pure scalar builtin (sqrt/abs/min/…) can't touch a Seq's
262            // RefCell — scan its args for index reads and carry on. Any
263            // other call may alias or mutate any handle: bail.
264            if is_pure_scalar_builtin(roles.interner.resolve(*function), args.len()) {
265                for a in args {
266                    scan_value_expr(a, roles);
267                }
268            } else {
269                roles.bail = true;
270            }
271        }
272        // Closures, escapes, zone accessors, opaque calls — bail.
273        _ => roles.bail = true,
274    }
275}
276
277/// A collection position: a bare identifier here is a READ borrow; any
278/// other expression is scanned as a value.
279fn scan_collection_pos(e: &Expr, roles: &mut AccessRoles) {
280    if let Expr::Identifier(s) = e {
281        roles.read(*s);
282    } else {
283        scan_value_expr(e, roles);
284    }
285}
286
287fn scan_stmts(stmts: &[Stmt], roles: &mut AccessRoles) {
288    for stmt in stmts {
289        if roles.bail {
290            return;
291        }
292        match stmt {
293            Stmt::Let { var, value, .. } => {
294                roles.rebound.insert(*var);
295                scan_value_expr(value, roles);
296            }
297            Stmt::Set { target, value } => {
298                // The rebind itself is caught by body_modifies_var; the
299                // RHS may leak a handle.
300                let _ = target;
301                scan_value_expr(value, roles);
302            }
303            Stmt::SetIndex { collection, index, value } => {
304                if let Expr::Identifier(s) = collection {
305                    roles.written(*s);
306                } else {
307                    scan_value_expr(collection, roles);
308                }
309                scan_value_expr(index, roles);
310                scan_value_expr(value, roles);
311            }
312            Stmt::Push { value, collection }
313            | Stmt::Add { value, collection }
314            | Stmt::Remove { value, collection } => {
315                // The pushed VALUE may leak a handle (a Seq pushed into a
316                // Seq-of-Seq escapes); the COLLECTION is resized in place.
317                scan_value_expr(value, roles);
318                if let Expr::Identifier(s) = collection {
319                    roles.resized(*s);
320                } else {
321                    scan_value_expr(collection, roles);
322                }
323            }
324            Stmt::Pop { collection, into } => {
325                if let Expr::Identifier(s) = collection {
326                    roles.resized(*s);
327                } else {
328                    scan_value_expr(collection, roles);
329                }
330                if let Some(v) = into {
331                    roles.rebound.insert(*v);
332                }
333            }
334            Stmt::If { cond, then_block, else_block } => {
335                scan_value_expr(cond, roles);
336                scan_stmts(then_block, roles);
337                if let Some(eb) = else_block {
338                    scan_stmts(eb, roles);
339                }
340            }
341            Stmt::While { cond, body, .. } => {
342                scan_value_expr(cond, roles);
343                scan_stmts(body, roles);
344            }
345            Stmt::Repeat { pattern, iterable, body } => {
346                if let Expr::Identifier(s) = iterable {
347                    roles.other(*s);
348                } else {
349                    scan_value_expr(iterable, roles);
350                }
351                if let crate::ast::stmt::Pattern::Identifier(s) = pattern {
352                    roles.rebound.insert(*s);
353                }
354                scan_stmts(body, roles);
355            }
356            Stmt::Show { object, recipient } => {
357                // Indexed reads are fine; showing a whole handle is opaque
358                // (display borrows it in library code).
359                scan_value_expr(object, roles);
360                scan_value_expr(recipient, roles);
361            }
362            Stmt::Return { value } => {
363                if let Some(v) = value {
364                    scan_value_expr(v, roles);
365                }
366            }
367            Stmt::Break => {}
368            Stmt::SetField { object, value, .. } => {
369                scan_value_expr(object, roles);
370                scan_value_expr(value, roles);
371            }
372            Stmt::Inspect { target, arms, .. } => {
373                scan_value_expr(target, roles);
374                for arm in arms {
375                    for (_, binding) in &arm.bindings {
376                        roles.rebound.insert(*binding);
377                    }
378                    scan_stmts(arm.body, roles);
379                }
380            }
381            Stmt::RuntimeAssert { condition, .. } => scan_value_expr(condition, roles),
382            // Calls, concurrency, zones, escapes, IO — opaque: bail.
383            _ => roles.bail = true,
384        }
385    }
386}
387
388/// Decide which handles to hoist around this loop. Empty when anything is
389/// uncertain.
390pub(crate) fn plan_borrow_hoist<'a>(
391    loop_stmt: &Stmt<'a>,
392    cond: Option<&Expr<'a>>,
393    body: &[Stmt<'a>],
394    ctx: &RefinementContext<'a>,
395    interner: &Interner,
396) -> Vec<HoistEntry> {
397    // The kill switch (env var or test thread-local) nulls the oracle in
398    // codegen_program, so a present oracle already means hoisting is on.
399    let oracle = match ctx.oracle() {
400        Some(o) => o,
401        None => return Vec::new(),
402    };
403
404    let mut roles = AccessRoles::new(interner);
405    if let Some(c) = cond {
406        scan_value_expr(c, &mut roles);
407    }
408    scan_stmts(body, &mut roles);
409    if roles.bail {
410        return Vec::new();
411    }
412
413    // Candidates: handles accessed through collection positions whose
414    // identity provably survives the loop. A resized (pushed/popped) handle
415    // becomes a held `&mut Vec` (MutVec); a written-but-not-resized handle a
416    // `&mut [T]` (MutSlice); a read-only handle a `&[T]` (Shared).
417    let types = ctx.get_variable_types();
418    let mut hoisted: Vec<(Symbol, HoistKind, String, String)> = Vec::new();
419    // De-Rc'd `Vec<T>` handles are owned and provably non-aliased (that is the
420    // de-Rc precondition), so they bypass the alias gating and always get the
421    // noalias slice extraction.
422    let mut derc_hoisted: Vec<(Symbol, HoistKind, String, String)> = Vec::new();
423    for sym in &roles.order {
424        let sym = *sym;
425        if roles.other.contains(&sym) || roles.rebound.contains(&sym) {
426            continue;
427        }
428        let resized = roles.resized.contains(&sym);
429        let written = roles.written.contains(&sym);
430        let read = roles.read.contains(&sym);
431        if !resized && !written && !read {
432            continue;
433        }
434        let full_ty = match types.get(&sym) {
435            Some(t) => t.clone(),
436            None => continue,
437        };
438        // A length-hoisted handle (`|__hl:` sentinel) already has a `_len`
439        // binding the loop uses; shadowing it as a slice would orphan that
440        // binding. Leave such handles per-access.
441        if full_ty.contains("|__hl:") {
442            continue;
443        }
444        let base_ty = full_ty.split("|__hl:").next().unwrap_or(&full_ty);
445        // A LogosSeq hoists with a `.borrow()`; a de-Rc'd `Vec<T>` hoists the
446        // slice directly (no borrow) — both give LLVM the noalias `&[T]`.
447        // `is_de_rc` is authoritative: the registered type string can lag a
448        // de-Rc'd decl, but the emitted variable is the `Vec`.
449        let elem_ty = if let Some(e) = base_ty.strip_prefix("LogosSeq<").and_then(|s| s.strip_suffix('>')) {
450            e.to_string()
451        } else if let Some(e) = base_ty.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
452            e.to_string()
453        } else {
454            continue;
455        };
456        let is_vec = ctx.is_de_rc(sym) || base_ty.starts_with("Vec<");
457        // A rebind (`Set x to …`) inside the body makes the held borrow
458        // stale; resize (push/pop) is fine — that's exactly the MutVec case.
459        if body_modifies_var(body, sym) {
460            continue;
461        }
462        let kind = if resized {
463            HoistKind::MutVec
464        } else if written {
465            HoistKind::MutSlice
466        } else {
467            HoistKind::Shared
468        };
469        if is_vec {
470            derc_hoisted.push((sym, kind, elem_ty, full_ty));
471        } else {
472            hoisted.push((sym, kind, elem_ty, full_ty));
473        }
474    }
475
476    // Alias gating to a fixpoint: a dropped handle becomes an unhoisted
477    // per-access toucher, which can invalidate remaining ones.
478    //
479    // Only handles that could share a Seq's `Rc<RefCell>` matter — a scalar,
480    // String, or Map can never conflict with a Seq borrow, and demanding the
481    // oracle prove `arr ≠ i` for a scalar `i` would needlessly require a loop
482    // snapshot. Any REAL aliaser is Seq-typed (codegen propagates the type
483    // through `Let`/`Set`, the oracle tracks the edge), so restricting the
484    // distinctness checks to Seq-typed touched symbols is sound.
485    let all_touched: Vec<Symbol> = roles
486        .order
487        .iter()
488        .copied()
489        .filter(|s| could_alias_seq(types.get(s)))
490        .collect();
491    loop {
492        let hoisted_syms: HashSet<Symbol> = hoisted.iter().map(|(s, _, _, _)| *s).collect();
493        let mut_hoisted: Vec<Symbol> = hoisted
494            .iter()
495            .filter(|(_, k, _, _)| *k != HoistKind::Shared)
496            .map(|(s, _, _, _)| *s)
497            .collect();
498        let unhoisted_mut: Vec<Symbol> = all_touched
499            .iter()
500            .filter(|s| {
501                !hoisted_syms.contains(s)
502                    && (roles.written.contains(s)
503                        || roles.resized.contains(s)
504                        || roles.other.contains(s))
505            })
506            .cloned()
507            .collect();
508        let keep: Vec<bool> = hoisted
509            .iter()
510            .map(|(sym, kind, _, _)| {
511                if *kind != HoistKind::Shared {
512                    // A RefMut must be alone: distinct from every other
513                    // touched handle, hoisted or not.
514                    all_touched
515                        .iter()
516                        .filter(|t| **t != *sym)
517                        .all(|t| oracle.loop_handles_definitely_distinct(loop_stmt, *sym, *t))
518                } else {
519                    // A Ref tolerates other Refs; it must be distinct from
520                    // every mut-hoisted handle and every unhoisted mutator.
521                    mut_hoisted
522                        .iter()
523                        .filter(|s| **s != *sym)
524                        .all(|s| oracle.loop_handles_definitely_distinct(loop_stmt, *sym, *s))
525                        && unhoisted_mut
526                            .iter()
527                            .all(|t| oracle.loop_handles_definitely_distinct(loop_stmt, *sym, *t))
528                }
529            })
530            .collect();
531        if keep.iter().all(|k| *k) {
532            break;
533        }
534        let mut it = keep.into_iter();
535        hoisted.retain(|_| it.next().unwrap());
536    }
537
538    let entries: Vec<HoistEntry> = derc_hoisted
539        .into_iter()
540        .map(|(sym, kind, elem_ty, old_type)| HoistEntry { sym, kind, elem_ty, old_type, is_vec: true })
541        .chain(
542            hoisted
543                .into_iter()
544                .map(|(sym, kind, elem_ty, old_type)| HoistEntry { sym, kind, elem_ty, old_type, is_vec: false }),
545        )
546        .collect();
547    // Reaching here with entries means the oracle is present, which (per the kill
548    // switch nulling it in codegen_program) means hoisting is enabled.
549    if !entries.is_empty() {
550        crate::optimize::mark_fired(crate::optimization::Opt::HoistBorrows);
551    }
552    entries
553}
554
555/// Open the hoist scope: guards, shadows, and the slice type flips.
556/// Returns nothing to restore beyond what the entries carry — call
557/// [`emit_hoist_close`] with the same entries on the way out.
558pub(crate) fn emit_hoist_open(
559    entries: &[HoistEntry],
560    interner: &Interner,
561    indent_str: &str,
562    ctx: &mut RefinementContext,
563    output: &mut String,
564) {
565    if entries.is_empty() {
566        return;
567    }
568    let names = RustNames::new(interner);
569    writeln!(output, "{}{{", indent_str).unwrap();
570    for e in entries {
571        let n = names.ident(e.sym);
572        match (e.kind, e.is_vec) {
573            // LogosSeq: a `.borrow()` guard + slice shadow.
574            (HoistKind::Shared, false) => {
575                writeln!(output, "{}    let __{}_g = {}.borrow();", indent_str, n, n).unwrap();
576                writeln!(output, "{}    let {} = &__{}_g[..];", indent_str, n, n).unwrap();
577                ctx.register_variable_type(e.sym, format!("&[{}]", e.elem_ty));
578            }
579            (HoistKind::MutSlice, false) => {
580                writeln!(output, "{}    let mut __{}_g = {}.borrow_mut();", indent_str, n, n).unwrap();
581                writeln!(output, "{}    let {} = &mut __{}_g[..];", indent_str, n, n).unwrap();
582                ctx.register_variable_type(e.sym, format!("&mut [{}]", e.elem_ty));
583            }
584            (HoistKind::MutVec, false) => {
585                // Hold the RefMut as a `&mut Vec` so push/pop go through it
586                // without a per-call borrow. Registered as `Vec<T>` to reuse
587                // the existing owned-Vec emission (index, push, length all
588                // auto-deref through the `&mut Vec` shadow).
589                writeln!(output, "{}    let mut __{}_g = {}.borrow_mut();", indent_str, n, n).unwrap();
590                writeln!(output, "{}    let {} = &mut *__{}_g;", indent_str, n, n).unwrap();
591                ctx.register_variable_type(e.sym, format!("Vec<{}>", e.elem_ty));
592            }
593            // De-Rc'd Vec: the SAME noalias slice, but no `.borrow()`.
594            (HoistKind::Shared, true) => {
595                writeln!(output, "{}    let {} = &{}[..];", indent_str, n, n).unwrap();
596                ctx.register_variable_type(e.sym, format!("&[{}]", e.elem_ty));
597            }
598            (HoistKind::MutSlice, true) => {
599                writeln!(output, "{}    let {} = &mut {}[..];", indent_str, n, n).unwrap();
600                ctx.register_variable_type(e.sym, format!("&mut [{}]", e.elem_ty));
601            }
602            (HoistKind::MutVec, true) => {
603                // Resized in the loop — keep it a `&mut Vec` (a slice can't
604                // push) but reborrow so LLVM gets the `&mut` noalias. The
605                // binding is `mut` so a NESTED loop can reborrow it again
606                // (`let mut a = &mut a;` then inner `let mut a = &mut a;`).
607                writeln!(output, "{}    let mut {} = &mut {};", indent_str, n, n).unwrap();
608                ctx.register_variable_type(e.sym, format!("Vec<{}>", e.elem_ty));
609            }
610        }
611    }
612}
613
614/// Close the hoist scope and restore the handles' tracked types.
615pub(crate) fn emit_hoist_close(
616    entries: &[HoistEntry],
617    indent_str: &str,
618    ctx: &mut RefinementContext,
619    output: &mut String,
620) {
621    if entries.is_empty() {
622        return;
623    }
624    for e in entries {
625        ctx.register_variable_type(e.sym, e.old_type.clone());
626    }
627    writeln!(output, "{}}}", indent_str).unwrap();
628}