Skip to content

Commit fd6d8b5

Browse files
authored
BE-248, BE-252: HashQL: Implement Dead Local Elimination (DLE) and use bump allocator throughout passes (#8198)
1 parent 1404042 commit fd6d8b5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1440
-565
lines changed

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use std::{
44
};
55

66
use hashql_ast::node::expr::Expr;
7-
use hashql_core::{heap::Heap, r#type::environment::Environment};
7+
use hashql_core::{
8+
heap::{Heap, Scratch},
9+
r#type::environment::Environment,
10+
};
811
use hashql_diagnostics::DiagnosticIssues;
912
use hashql_mir::{
1013
body::Body,
@@ -45,7 +48,7 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>(
4548
render: impl FnOnce(&'heap Heap, &Environment<'heap>, DefId, &DefIdSlice<Body<'heap>>),
4649
environment: &mut Environment<'heap>,
4750
diagnostics: &mut Vec<SuiteDiagnostic>,
48-
) -> Result<(DefId, DefIdVec<Body<'heap>>), SuiteDiagnostic> {
51+
) -> Result<(DefId, DefIdVec<Body<'heap>>, Scratch), SuiteDiagnostic> {
4952
let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?;
5053

5154
render(heap, environment, root, &bodies);
@@ -56,15 +59,16 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>(
5659
interner,
5760
diagnostics: DiagnosticIssues::new(),
5861
};
62+
let mut scratch = Scratch::new();
5963

60-
let mut pass = CfgSimplify::new();
64+
let mut pass = CfgSimplify::new_in(&mut scratch);
6165
for body in bodies.as_mut_slice() {
6266
pass.run(&mut context, body);
6367
}
6468

6569
process_issues(diagnostics, context.diagnostics)?;
6670

67-
Ok((root, bodies))
71+
Ok((root, bodies, scratch))
6872
}
6973

7074
pub(crate) struct MirPassTransformCfgSimplify;
@@ -104,7 +108,7 @@ impl Suite for MirPassTransformCfgSimplify {
104108
let mut buffer = Vec::new();
105109
let mut d2 = d2_output_enabled(self, suite_directives, reports).then(mir_spawn_d2);
106110

107-
let (root, bodies) = mir_pass_transform_cfg_simplify(
111+
let (root, bodies, _) = mir_pass_transform_cfg_simplify(
108112
heap,
109113
expr,
110114
&interner,

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::io::Write as _;
22

33
use hashql_ast::node::expr::Expr;
4-
use hashql_core::{heap::Heap, r#type::environment::Environment};
4+
use hashql_core::{
5+
heap::{Heap, Scratch},
6+
r#type::environment::Environment,
7+
};
58
use hashql_diagnostics::DiagnosticIssues;
69
use hashql_mir::{
710
body::Body,
@@ -28,8 +31,8 @@ pub(crate) fn mir_pass_transform_dse<'heap>(
2831
render: impl FnOnce(&'heap Heap, &Environment<'heap>, DefId, &DefIdSlice<Body<'heap>>),
2932
environment: &mut Environment<'heap>,
3033
diagnostics: &mut Vec<SuiteDiagnostic>,
31-
) -> Result<(DefId, DefIdVec<Body<'heap>>), SuiteDiagnostic> {
32-
let (root, mut bodies) =
34+
) -> Result<(DefId, DefIdVec<Body<'heap>>, Scratch), SuiteDiagnostic> {
35+
let (root, mut bodies, mut scratch) =
3336
mir_pass_transform_sroa(heap, expr, interner, render, environment, diagnostics)?;
3437

3538
let mut context = MirContext {
@@ -39,13 +42,13 @@ pub(crate) fn mir_pass_transform_dse<'heap>(
3942
diagnostics: DiagnosticIssues::new(),
4043
};
4144

42-
let mut pass = DeadStoreElimination::new();
45+
let mut pass = DeadStoreElimination::new_in(&mut scratch);
4346
for body in bodies.as_mut_slice() {
4447
pass.run(&mut context, body);
4548
}
4649

4750
process_issues(diagnostics, context.diagnostics)?;
48-
Ok((root, bodies))
51+
Ok((root, bodies, scratch))
4952
}
5053

5154
pub(crate) struct MirPassTransformDse;
@@ -85,7 +88,7 @@ impl Suite for MirPassTransformDse {
8588
let mut buffer = Vec::new();
8689
let mut d2 = d2_output_enabled(self, suite_directives, reports).then(mir_spawn_d2);
8790

88-
let (root, bodies) = mir_pass_transform_dse(
91+
let (root, bodies, _) = mir_pass_transform_dse(
8992
heap,
9093
expr,
9194
&interner,

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_sroa.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::io::Write as _;
22

33
use hashql_ast::node::expr::Expr;
4-
use hashql_core::{heap::Heap, r#type::environment::Environment};
4+
use hashql_core::{
5+
heap::{Heap, Scratch},
6+
r#type::environment::Environment,
7+
};
58
use hashql_diagnostics::DiagnosticIssues;
69
use hashql_mir::{
710
body::Body,
@@ -28,8 +31,8 @@ pub(crate) fn mir_pass_transform_sroa<'heap>(
2831
render: impl FnOnce(&'heap Heap, &Environment<'heap>, DefId, &DefIdSlice<Body<'heap>>),
2932
environment: &mut Environment<'heap>,
3033
diagnostics: &mut Vec<SuiteDiagnostic>,
31-
) -> Result<(DefId, DefIdVec<Body<'heap>>), SuiteDiagnostic> {
32-
let (root, mut bodies) =
34+
) -> Result<(DefId, DefIdVec<Body<'heap>>, Scratch), SuiteDiagnostic> {
35+
let (root, mut bodies, mut scratch) =
3336
mir_pass_transform_cfg_simplify(heap, expr, interner, render, environment, diagnostics)?;
3437

3538
let mut context = MirContext {
@@ -39,13 +42,13 @@ pub(crate) fn mir_pass_transform_sroa<'heap>(
3942
diagnostics: DiagnosticIssues::new(),
4043
};
4144

42-
let mut pass = Sroa::new();
45+
let mut pass = Sroa::new_in(&mut scratch);
4346
for body in bodies.as_mut_slice() {
4447
pass.run(&mut context, body);
4548
}
4649

4750
process_issues(diagnostics, context.diagnostics)?;
48-
Ok((root, bodies))
51+
Ok((root, bodies, scratch))
4952
}
5053

5154
pub(crate) struct MirPassTransformSroa;
@@ -85,7 +88,7 @@ impl Suite for MirPassTransformSroa {
8588
let mut buffer = Vec::new();
8689
let mut d2 = d2_output_enabled(self, suite_directives, reports).then(mir_spawn_d2);
8790

88-
let (root, bodies) = mir_pass_transform_sroa(
91+
let (root, bodies, _) = mir_pass_transform_sroa(
8992
heap,
9093
expr,
9194
&interner,

libs/@local/hashql/core/src/collections/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ use hashbrown::{HashMap, HashSet};
99

1010
pub use self::{hash_map::HashMapExt, work_queue::WorkQueue};
1111

12-
pub type FastHashMap<K, V, A = Global> = HashMap<K, V, foldhash::fast::RandomState, A>;
12+
pub type FastHasher = foldhash::fast::RandomState;
13+
14+
pub type FastHashMap<K, V, A = Global> = HashMap<K, V, FastHasher, A>;
1315
pub type FastHashMapEntry<'map, K, V, A = Global> =
14-
hashbrown::hash_map::Entry<'map, K, V, foldhash::fast::RandomState, A>;
15-
pub type FastHashSet<T, A = Global> = HashSet<T, foldhash::fast::RandomState, A>;
16-
pub type FastHashSetEntry<'map, T, A = Global> =
17-
hashbrown::hash_set::Entry<'map, T, foldhash::fast::RandomState, A>;
16+
hashbrown::hash_map::Entry<'map, K, V, FastHasher, A>;
17+
pub type FastHashSet<T, A = Global> = HashSet<T, FastHasher, A>;
18+
pub type FastHashSetEntry<'map, T, A = Global> = hashbrown::hash_set::Entry<'map, T, FastHasher, A>;
1819

1920
#[inline]
2021
#[must_use]

libs/@local/hashql/core/src/heap/allocator.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use core::{alloc, ptr};
44

55
use bumpalo::Bump;
66

7+
use super::BumpAllocator;
8+
79
/// Internal arena allocator.
810
#[derive(Debug)]
911
pub(super) struct Allocator(Bump);
@@ -22,12 +24,6 @@ impl Allocator {
2224
Self(Bump::with_capacity(capacity))
2325
}
2426

25-
/// Resets the allocator, invalidating all allocations but retaining capacity.
26-
#[inline]
27-
pub(crate) fn reset(&mut self) {
28-
self.0.reset();
29-
}
30-
3127
/// Allocates a value using a closure to avoid moving before allocation.
3228
#[inline]
3329
pub(crate) fn alloc_with<T>(&self, func: impl FnOnce() -> T) -> &mut T {
@@ -44,13 +40,17 @@ impl Allocator {
4440
.try_alloc_slice_copy(slice)
4541
.map_err(|_err| alloc::AllocError)
4642
}
43+
}
44+
45+
impl BumpAllocator for Allocator {
46+
#[inline]
47+
fn allocate_slice_copy<T: Copy>(&self, slice: &[T]) -> Result<&mut [T], alloc::AllocError> {
48+
self.try_alloc_slice_copy(slice)
49+
}
4750

48-
/// Copies a string into the arena.
4951
#[inline]
50-
pub(crate) fn try_alloc_str(&self, string: &str) -> Result<&mut str, alloc::AllocError> {
51-
self.0
52-
.try_alloc_str(string)
53-
.map_err(|_err| alloc::AllocError)
52+
fn reset(&mut self) {
53+
self.0.reset();
5454
}
5555
}
5656

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! Bump allocator trait for arena-style memory management.
2+
//!
3+
//! This module provides the [`BumpAllocator`] trait, an extension to the standard
4+
//! [`Allocator`] trait that adds support for bulk deallocation via [`reset`](BumpAllocator::reset).
5+
//!
6+
//! # Overview
7+
//!
8+
//! Bump allocators (also known as arena allocators) allocate memory by incrementing
9+
//! a pointer ("bumping") within a contiguous memory region. Individual deallocations
10+
//! are not supported; instead, all allocations are freed at once by resetting the
11+
//! bump pointer.
12+
//!
13+
//! This trade-off makes bump allocators ideal for:
14+
//!
15+
//! - **Compiler passes**: Temporary data structures that live for a single pass
16+
//! - **AST construction**: Nodes allocated during parsing, freed after compilation
17+
//! - **Batch processing**: Work items processed together, then discarded
18+
//!
19+
//! # Usage
20+
//!
21+
//! The trait is implemented by [`Heap`] and [`Scratch`] allocators:
22+
//!
23+
//! ```
24+
//! # #![feature(allocator_api)]
25+
//! use hashql_core::heap::{BumpAllocator, CollectIn, Scratch};
26+
//!
27+
//! let mut scratch = Scratch::new();
28+
//!
29+
//! // Allocate some data
30+
//! let vec: Vec<u32, &Scratch> = (1..=3).collect_in(&scratch);
31+
//! drop(vec);
32+
//!
33+
//! // Reset frees all allocations at once
34+
//! scratch.reset();
35+
//! ```
36+
//!
37+
//! # Pass Pattern
38+
//!
39+
//! Compiler passes commonly reset the allocator at the start of `run()` to reuse
40+
//! memory from previous invocations:
41+
//!
42+
//! ```ignore
43+
//! impl TransformPass for MyPass<A: BumpAllocator> {
44+
//! fn run(&mut self, context: &mut Context, body: &mut Body) {
45+
//! self.alloc.reset(); // Reuse memory from previous run
46+
//! // ... pass implementation using self.alloc ...
47+
//! }
48+
//! }
49+
//! ```
50+
//!
51+
//! [`Heap`]: super::Heap
52+
//! [`Scratch`]: super::Scratch
53+
#![expect(clippy::mut_from_ref, reason = "allocator")]
54+
use core::alloc::{AllocError, Allocator};
55+
56+
/// A bump allocator that supports bulk deallocation.
57+
///
58+
/// This trait extends [`Allocator`] with arena-style memory management:
59+
/// allocations are made by bumping a pointer, and all memory is freed at once
60+
/// via [`reset`](Self::reset).
61+
///
62+
/// # Implementors
63+
///
64+
/// - [`Heap`](super::Heap): Full-featured arena with string interning
65+
/// - [`Scratch`](super::Scratch): Lightweight arena for temporary allocations
66+
pub trait BumpAllocator: Allocator {
67+
/// Copies a slice into the arena, returning a mutable reference to the copy.
68+
///
69+
/// This is useful for transferring borrowed data into arena-owned memory.
70+
/// The source slice is copied element-by-element into freshly allocated arena memory.
71+
///
72+
/// # Type Requirements
73+
///
74+
/// The element type must be [`Copy`] to ensure safe bitwise copying without
75+
/// running destructors on the source data.
76+
///
77+
/// # Errors
78+
///
79+
/// Returns [`AllocError`] if memory allocation fails.
80+
fn allocate_slice_copy<T: Copy>(&self, slice: &[T]) -> Result<&mut [T], AllocError>;
81+
82+
/// Resets the allocator, freeing all allocations at once.
83+
///
84+
/// After calling `reset`, the allocator's memory is available for reuse.
85+
/// All previously allocated references become invalid; using them is
86+
/// undefined behavior (prevented by Rust's borrow checker in safe code).
87+
///
88+
/// The allocator retains its current capacity, avoiding reallocation
89+
/// on subsequent use.
90+
fn reset(&mut self);
91+
}
92+
93+
/// Blanket implementation allowing `&mut A` to be used where `A: BumpAllocator`.
94+
///
95+
/// This enables passes to store `&mut Scratch` or `&mut Heap` while still
96+
/// calling [`reset`](BumpAllocator::reset) through the mutable reference.
97+
impl<A> BumpAllocator for &mut A
98+
where
99+
A: BumpAllocator,
100+
{
101+
#[inline]
102+
fn allocate_slice_copy<T: Copy>(&self, slice: &[T]) -> Result<&mut [T], AllocError> {
103+
A::allocate_slice_copy(self, slice)
104+
}
105+
106+
#[inline]
107+
fn reset(&mut self) {
108+
A::reset(self);
109+
}
110+
}

libs/@local/hashql/core/src/heap/mod.rs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
//! ```
9494
#![expect(unsafe_code)]
9595
mod allocator;
96+
mod bump;
9697
mod clone;
9798
mod convert;
9899
mod iter;
@@ -107,6 +108,7 @@ use hashbrown::HashSet;
107108

108109
use self::allocator::Allocator;
109110
pub use self::{
111+
bump::BumpAllocator,
110112
clone::{CloneIn, TryCloneIn},
111113
convert::{FromIn, IntoIn},
112114
iter::{CollectIn, FromIteratorIn},
@@ -239,27 +241,6 @@ impl Heap {
239241
}
240242
}
241243

242-
/// Resets the heap, invalidating all previous allocations.
243-
///
244-
/// Clears all allocations and re-primes with common symbols.
245-
/// The allocator retains its current capacity.
246-
///
247-
/// # Panics
248-
///
249-
/// Panics if the internal mutex is poisoned.
250-
pub fn reset(&mut self) {
251-
// IMPORTANT: Clear strings BEFORE resetting the arena to prevent dangling references.
252-
// The HashSet stores `&'static str` that actually point into arena memory.
253-
{
254-
let mut strings = self.strings.lock().expect("lock should not be poisoned");
255-
strings.clear();
256-
Self::prime_symbols(&mut strings);
257-
drop(strings);
258-
}
259-
260-
self.inner.reset();
261-
}
262-
263244
/// Allocates a value in the arena, returning a mutable reference.
264245
///
265246
/// Only accepts types that do **not** require [`Drop`]. Types requiring destructors
@@ -321,6 +302,35 @@ impl Default for Heap {
321302
}
322303
}
323304

305+
impl BumpAllocator for Heap {
306+
#[inline]
307+
fn allocate_slice_copy<T: Copy>(&self, slice: &[T]) -> Result<&mut [T], alloc::AllocError> {
308+
self.inner.try_alloc_slice_copy(slice)
309+
}
310+
311+
/// Resets the heap, invalidating all previous allocations.
312+
///
313+
/// Clears all allocations and re-primes with common symbols.
314+
/// The allocator retains its current capacity.
315+
///
316+
/// # Panics
317+
///
318+
/// Panics if the internal mutex is poisoned.
319+
#[inline]
320+
fn reset(&mut self) {
321+
// IMPORTANT: Clear strings BEFORE resetting the arena to prevent dangling references.
322+
// The HashSet stores `&'static str` that actually point into arena memory.
323+
{
324+
let mut strings = self.strings.lock().expect("lock should not be poisoned");
325+
strings.clear();
326+
Self::prime_symbols(&mut strings);
327+
drop(strings);
328+
}
329+
330+
self.inner.reset();
331+
}
332+
}
333+
324334
// SAFETY: Delegates to bumpalo::Bump via the internal Allocator.
325335
#[expect(unsafe_code, reason = "proxy to internal allocator")]
326336
unsafe impl alloc::Allocator for Heap {

0 commit comments

Comments
 (0)