Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::io::Write as _;

use hashql_ast::node::expr::Expr;
use hashql_core::{
heap::{Heap, Scratch},
r#type::environment::Environment,
};
use hashql_diagnostics::DiagnosticIssues;
use hashql_mir::{
body::Body,
context::MirContext,
def::{DefId, DefIdSlice, DefIdVec},
intern::Interner,
pass::{Changed, GlobalTransformPass as _, transform::AdministrativeReduction},
};

use super::{
RunContext, Suite, SuiteDiagnostic, common::process_issues,
mir_pass_transform_cfg_simplify::mir_pass_transform_cfg_simplify,
};
use crate::suite::{
common::Header,
mir_pass_transform_cfg_simplify::mir_pass_transform_cfg_simplify_default_renderer,
mir_reify::{d2_output_enabled, mir_format_d2, mir_format_text, mir_spawn_d2},
};

pub(crate) fn mir_pass_transform_administrative_reduction<'heap>(
heap: &'heap Heap,
expr: Expr<'heap>,
interner: &Interner<'heap>,
render: impl FnOnce(&'heap Heap, &Environment<'heap>, DefId, &DefIdSlice<Body<'heap>>),
environment: &mut Environment<'heap>,
diagnostics: &mut Vec<SuiteDiagnostic>,
) -> Result<(DefId, DefIdVec<Body<'heap>>, Scratch), SuiteDiagnostic> {
let (root, mut bodies, mut scratch) =
mir_pass_transform_cfg_simplify(heap, expr, interner, render, environment, diagnostics)?;

let mut context = MirContext {
heap,
env: environment,
interner,
diagnostics: DiagnosticIssues::new(),
};

let mut pass = AdministrativeReduction::new_in(&mut scratch);
let _: Changed = pass.run(&mut context, &mut bodies);

process_issues(diagnostics, context.diagnostics)?;
Ok((root, bodies, scratch))
}

pub(crate) struct MirPassTransformAdministrativeReduction;

impl Suite for MirPassTransformAdministrativeReduction {
fn priority(&self) -> usize {
1
}

fn name(&self) -> &'static str {
"mir/pass/transform/administrative-reduction"
}

fn description(&self) -> &'static str {
"Administrative Reduction in the MIR"
}

fn secondary_file_extensions(&self) -> &[&str] {
&["svg"]
}

fn run<'heap>(
&self,
RunContext {
heap,
diagnostics,
suite_directives,
reports,
secondary_outputs,
..
}: RunContext<'_, 'heap>,
expr: Expr<'heap>,
) -> Result<String, SuiteDiagnostic> {
let mut environment = Environment::new(heap);
let interner = Interner::new(heap);

let mut buffer = Vec::new();
let mut d2 = d2_output_enabled(self, suite_directives, reports).then(mir_spawn_d2);

let (root, bodies, _) = mir_pass_transform_administrative_reduction(
heap,
expr,
&interner,
mir_pass_transform_cfg_simplify_default_renderer(
&mut buffer,
d2.as_mut().map(|(writer, _)| writer),
),
&mut environment,
diagnostics,
)?;

let _ = writeln!(buffer, "\n{}\n", Header::new("MIR after AR"));
mir_format_text(heap, &environment, &mut buffer, root, &bodies);

if let Some((mut writer, handle)) = d2 {
writeln!(writer, "final: 'MIR after AR' {{")
.expect("should be able to write to buffer");
mir_format_d2(heap, &environment, &mut writer, root, &bodies);
writeln!(writer, "}}").expect("should be able to write to buffer");

writer.flush().expect("should be able to write to buffer");
drop(writer);

let diagram = handle.join().expect("should be able to join handle");
let diagram = String::from_utf8_lossy_owned(diagram);

secondary_outputs.insert("svg", diagram);
}

Ok(String::from_utf8_lossy_owned(buffer))
}
}
3 changes: 3 additions & 0 deletions libs/@local/hashql/compiletest/src/suite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod hir_lower_specialization;
mod hir_lower_thunking;
mod hir_reify;
mod mir_pass_analysis_data_dependency;
mod mir_pass_transform_administrative_reduction;
mod mir_pass_transform_cfg_simplify;
mod mir_pass_transform_dse;
mod mir_pass_transform_inst_simplify;
Expand Down Expand Up @@ -55,6 +56,7 @@ use self::{
hir_lower_specialization::HirLowerSpecializationSuite,
hir_lower_thunking::HirLowerThunkingSuite, hir_reify::HirReifySuite,
mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency,
mir_pass_transform_administrative_reduction::MirPassTransformAdministrativeReduction,
mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify,
mir_pass_transform_dse::MirPassTransformDse,
mir_pass_transform_inst_simplify::MirPassTransformInstSimplify,
Expand Down Expand Up @@ -154,6 +156,7 @@ const SUITES: &[&dyn Suite] = &[
&HirLowerTypeInferenceSuite,
&HirReifySuite,
&MirPassAnalysisDataDependency,
&MirPassTransformAdministrativeReduction,
&MirPassTransformCfgSimplify,
&MirPassTransformDse,
&MirPassTransformInstSimplify,
Expand Down
155 changes: 153 additions & 2 deletions libs/@local/hashql/core/src/graph/algorithms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod dominators;
pub mod tarjan;

use alloc::collections::VecDeque;
use core::iter::FusedIterator;

pub use self::{
dominators::{
Expand All @@ -38,7 +39,10 @@ pub use self::{
tarjan::Tarjan,
};
use super::{DirectedGraph, Successors};
use crate::id::{Id, bit_vec::MixedBitSet};
use crate::id::{
Id,
bit_vec::{DenseBitSet, MixedBitSet},
};

/// Iterator for depth-first traversal of a directed graph.
///
Expand Down Expand Up @@ -589,6 +593,153 @@ where
// Lower bound: at least the nodes currently in the stack
// Upper bound: could visit up to all remaining unvisited nodes
// Note: Due to disconnected components, we may not visit all nodes
(self.stack.len(), Some(remaining))
(self.stack.len(), Some(self.stack.len() + remaining))
}
}

/// Iterator for post-order depth-first traversal of an entire graph forest.
///
/// Unlike [`DepthFirstTraversalPostOrder`], this iterator automatically discovers and
/// traverses all nodes in the graph, including disconnected components. It guarantees
/// that every node is visited exactly once, with all descendants visited before their
/// ancestors (post-order).
///
/// This traversal is useful for:
/// - Processing all nodes in a graph without knowing the structure upfront
/// - Topological-like ordering across disconnected components
/// - Computing properties that require visiting all nodes bottom-up
///
/// The iterator implements [`ExactSizeIterator`] since it will visit exactly
/// `graph.node_count()` nodes.
///
/// # Examples
///
/// ```rust
/// # use hashql_core::graph::{LinkedGraph, algorithms::DepthFirstForestPostOrder};
/// #
/// let mut graph = LinkedGraph::new();
/// let n1 = graph.add_node("A");
/// let n2 = graph.add_node("B");
/// let n3 = graph.add_node("C");
/// graph.add_edge(n1, n2, ());
/// // n3 is disconnected
///
/// let traversal = DepthFirstForestPostOrder::new(&graph);
/// let visited: Vec<_> = traversal.collect();
///
/// // All nodes are visited, with descendants before ancestors
/// assert_eq!(visited.len(), 3);
/// # // n2 comes before n1 (post-order within component)
/// # assert!(visited.iter().position(|&n| n == n2) < visited.iter().position(|&n| n == n1));
/// ```
pub struct DepthFirstForestPostOrder<'graph, G: ?Sized, N, I> {
graph: &'graph G,
stack: Vec<PostOrderFrame<N, I>>,
visited: DenseBitSet<N>,
}

impl<'graph, G: ?Sized, N, I> DepthFirstForestPostOrder<'graph, G, N, I> {
/// Creates a new post-order forest traversal over the entire graph.
///
/// The traversal will visit all nodes in the graph, automatically discovering
/// disconnected components by iterating through unvisited node IDs.
///
/// # Examples
///
/// ```rust
/// # use hashql_core::graph::{LinkedGraph, algorithms::DepthFirstForestPostOrder};
/// #
/// let mut graph = LinkedGraph::new();
/// let n1 = graph.add_node("A");
/// let n2 = graph.add_node("B");
/// graph.add_edge(n1, n2, ());
///
/// let traversal = DepthFirstForestPostOrder::new(&graph);
/// assert_eq!(traversal.len(), 2);
/// ```
pub fn new(graph: &'graph G) -> Self
where
G: DirectedGraph,
N: Id,
{
Self {
graph,
stack: Vec::new(),
visited: DenseBitSet::new_empty(graph.node_count()),
}
}
}

impl<'graph, G: DirectedGraph<NodeId = N> + Successors<SuccIter<'graph> = I> + ?Sized, N, I>
Iterator for DepthFirstForestPostOrder<'graph, G, N, I>
where
N: Id,
I: Iterator<Item = N>,
{
type Item = N;

fn next(&mut self) -> Option<Self::Item> {
let node = 'recurse: loop {
if self.stack.is_empty()
&& let Some(node) = self.visited.first_unset()
{
self.visited.insert(node);
self.stack.push(PostOrderFrame {
node,
successors: self.graph.successors(node),
});
}

let PostOrderFrame { node, successors } = self.stack.last_mut()?;
let node = *node;

// Process successors until we find an unvisited one
for successor in successors {
if !self.visited.insert(successor) {
// Already visited, skip
continue;
}

// Found unvisited successor - push it and "recurse" by continuing outer loop
self.stack.push(PostOrderFrame {
node: successor,
successors: self.graph.successors(successor),
});

continue 'recurse;
}

// All successors processed - we can now yield this node
self.stack.pop();
break node;
};

Some(node)
}

fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.graph.node_count() - self.visited.count();

let total = self.stack.len() + remaining;

// Lower bound: at least the nodes currently in the stack
// Upper bound: could visit up to all remaining unvisited nodes
(total, Some(total))
}
}

impl<'graph, G: DirectedGraph<NodeId = N> + Successors<SuccIter<'graph> = I> + ?Sized, N, I>
ExactSizeIterator for DepthFirstForestPostOrder<'graph, G, N, I>
where
N: Id,
I: Iterator<Item = N>,
{
}

impl<'graph, G: DirectedGraph<NodeId = N> + Successors<SuccIter<'graph> = I> + ?Sized, N, I>
FusedIterator for DepthFirstForestPostOrder<'graph, G, N, I>
where
N: Id,
I: Iterator<Item = N>,
{
}
37 changes: 36 additions & 1 deletion libs/@local/hashql/core/src/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ pub mod linked;
#[cfg(test)]
mod tests;

use self::algorithms::{BreadthFirstTraversal, DepthFirstTraversal, DepthFirstTraversalPostOrder};
use self::algorithms::{
BreadthFirstTraversal, DepthFirstForestPostOrder, DepthFirstTraversal,
DepthFirstTraversalPostOrder,
};
pub use self::linked::LinkedGraph;
use crate::id::{HasId, Id, newtype};

Expand Down Expand Up @@ -269,6 +272,38 @@ pub trait Traverse: DirectedGraph + Successors {
traversal
}

/// Performs a post-order depth-first traversal over all nodes in the graph.
///
/// Unlike [`depth_first_traversal_post_order`], this method automatically discovers
/// and traverses all nodes, including disconnected components. Every node is visited
/// exactly once, with descendants visited before ancestors.
///
/// Returns an [`ExactSizeIterator`] since it visits exactly `node_count()` nodes.
///
/// [`depth_first_traversal_post_order`]: Traverse::depth_first_traversal_post_order
///
/// # Examples
///
/// ```rust
/// # use hashql_core::graph::{LinkedGraph, Traverse};
/// #
/// let mut graph = LinkedGraph::new();
/// let n1 = graph.add_node("A");
/// let n2 = graph.add_node("B");
/// let n3 = graph.add_node("C");
/// graph.add_edge(n1, n2, ());
/// // n3 is disconnected
///
/// let visited: Vec<_> = graph.depth_first_forest_post_order().collect();
///
/// // All nodes visited, with descendants before ancestors
/// assert_eq!(visited.len(), 3);
/// # assert!(visited.iter().position(|&n| n == n2) < visited.iter().position(|&n| n == n1));
/// ```
fn depth_first_forest_post_order(&self) -> impl ExactSizeIterator<Item = Self::NodeId> {
DepthFirstForestPostOrder::new(self)
}

/// Performs a breadth-first traversal starting from the given nodes.
///
/// # Examples
Expand Down
Loading
Loading