use std::borrow::Cow;
use crate::ast::{Commutativity, ConstValue, PolydatNode};
use crate::kernel::WireSource;
#[derive(Debug, Clone)]
pub enum FusionPattern {
Node {
op: &'static str,
inputs: Vec<FusionPattern>,
bind: Cow<'static, str>,
},
Any {
bind: Cow<'static, str>,
},
VariadicNode {
op: &'static str,
child_pattern: Box<FusionPattern>,
bind: Cow<'static, str>,
min_children: usize,
},
}
impl FusionPattern {
pub fn node(
op: &'static str,
inputs: Vec<FusionPattern>,
bind: impl Into<Cow<'static, str>>,
) -> Self {
FusionPattern::Node {
op,
inputs,
bind: bind.into(),
}
}
pub fn any(bind: impl Into<Cow<'static, str>>) -> Self {
FusionPattern::Any { bind: bind.into() }
}
pub fn root_op(&self) -> Option<&'static str> {
match self {
FusionPattern::Node { op, .. } => Some(op),
FusionPattern::VariadicNode { op, .. } => Some(op),
FusionPattern::Any { .. } => None,
}
}
}
#[derive(Debug, Clone)]
pub struct MatchResult {
pub wires: Vec<(String, WireSource)>,
pub constants: Vec<(String, Vec<u64>)>,
pub typed_constants: Vec<(String, Vec<ConstValue>)>,
pub consumed_nodes: Vec<usize>,
}
impl MatchResult {
fn new() -> Self {
Self {
wires: Vec::new(),
constants: Vec::new(),
typed_constants: Vec::new(),
consumed_nodes: Vec::new(),
}
}
pub fn wire(&self, name: &str) -> &WireSource {
self.wires
.iter()
.find(|(n, _)| n == name)
.map(|(_, w)| w)
.unwrap_or_else(|| panic!("no wire bound as '{name}'"))
}
pub fn const_vec(&self, name: &str) -> &[u64] {
self.constants
.iter()
.find(|(n, _)| n == name)
.map(|(_, c)| c.as_slice())
.unwrap_or_else(|| panic!("no constants bound as '{name}'"))
}
pub fn const_u64(&self, name: &str) -> u64 {
self.const_vec(name)[0]
}
pub fn typed_consts(&self, name: &str) -> &[ConstValue] {
self.typed_constants
.iter()
.find(|(n, _)| n == name)
.map(|(_, c)| c.as_slice())
.unwrap_or(&[])
}
fn merge(&mut self, other: MatchResult) {
self.wires.extend(other.wires);
self.constants.extend(other.constants);
self.typed_constants.extend(other.typed_constants);
self.consumed_nodes.extend(other.consumed_nodes);
}
}
pub struct FusionRule {
pub name: &'static str,
pub pattern: FusionPattern,
pub replacement: fn(&MatchResult) -> Box<dyn PolydatNode>,
pub input_bindings: &'static [&'static str],
}
struct NodeView<'a> {
nodes: &'a [Option<Box<dyn PolydatNode>>],
wiring: &'a [Vec<WireSource>],
}
fn try_match(
pattern: &FusionPattern,
source: &WireSource,
view: &NodeView<'_>,
) -> Option<MatchResult> {
match pattern {
FusionPattern::Any { bind } => {
let mut result = MatchResult::new();
result.wires.push((bind.to_string(), source.clone()));
Some(result)
}
FusionPattern::Node { op, inputs, bind } => {
let node_idx = match source {
WireSource::NodeOutput(idx, 0) => *idx,
_ => return None,
};
let node = view.nodes[node_idx].as_ref()?;
if node.meta().name != *op {
return None;
}
let node_wiring = &view.wiring[node_idx];
if node_wiring.len() != inputs.len() {
return None;
}
let matched = match_inputs(inputs, node_wiring, &node.commutativity(), view)?;
let mut result = matched;
result
.constants
.push((bind.to_string(), node.jit_constants()));
let typed: Vec<ConstValue> = node
.meta()
.const_slots()
.iter()
.map(|c| c.1.clone())
.collect();
if !typed.is_empty() {
result.typed_constants.push((bind.to_string(), typed));
}
result.consumed_nodes.push(node_idx);
Some(result)
}
FusionPattern::VariadicNode {
op,
child_pattern,
bind,
min_children,
} => {
let node_idx = match source {
WireSource::NodeOutput(idx, 0) => *idx,
_ => return None,
};
let node = view.nodes[node_idx].as_ref()?;
if node.meta().name != *op {
return None;
}
let node_wiring = &view.wiring[node_idx];
if node_wiring.len() < *min_children {
return None;
}
let mut result = MatchResult::new();
for (i, wire) in node_wiring.iter().enumerate() {
let child_bind = format!("{bind}_{i}");
let indexed_pattern = match child_pattern.as_ref() {
FusionPattern::Any { .. } => FusionPattern::Any {
bind: Cow::Owned(child_bind.clone()),
},
_ => *child_pattern.clone(),
};
let m = try_match(&indexed_pattern, wire, view)?;
result.merge(m);
}
result
.constants
.push((bind.to_string(), node.jit_constants()));
let typed: Vec<ConstValue> = node
.meta()
.const_slots()
.iter()
.map(|c| c.1.clone())
.collect();
if !typed.is_empty() {
result.typed_constants.push((bind.to_string(), typed));
}
result.consumed_nodes.push(node_idx);
Some(result)
}
}
}
fn match_inputs(
patterns: &[FusionPattern],
wires: &[WireSource],
commutativity: &Commutativity,
view: &NodeView<'_>,
) -> Option<MatchResult> {
match commutativity {
Commutativity::Positional => match_positional(patterns, wires, view),
Commutativity::AllCommutative => {
let indices: Vec<usize> = (0..wires.len()).collect();
for perm in permutations(&indices) {
let reordered: Vec<&WireSource> = perm.iter().map(|&i| &wires[i]).collect();
if let Some(m) = match_ordered(patterns, &reordered, view) {
return Some(m);
}
}
None
}
Commutativity::Groups(groups) => {
let mut in_group = vec![false; wires.len()];
for g in groups {
for &idx in g {
if idx < in_group.len() {
in_group[idx] = true;
}
}
}
try_groups_match(patterns, wires, groups, &in_group, view)
}
}
}
fn match_positional(
patterns: &[FusionPattern],
wires: &[WireSource],
view: &NodeView<'_>,
) -> Option<MatchResult> {
let refs: Vec<&WireSource> = wires.iter().collect();
match_ordered(patterns, &refs, view)
}
fn match_ordered(
patterns: &[FusionPattern],
wires: &[&WireSource],
view: &NodeView<'_>,
) -> Option<MatchResult> {
let mut result = MatchResult::new();
for (pat, wire) in patterns.iter().zip(wires.iter()) {
let m = try_match(pat, wire, view)?;
result.merge(m);
}
Some(result)
}
fn try_groups_match(
patterns: &[FusionPattern],
wires: &[WireSource],
groups: &[Vec<usize>],
_in_group: &[bool],
view: &NodeView<'_>,
) -> Option<MatchResult> {
let mut index_map: Vec<usize> = (0..wires.len()).collect();
fn recurse(
group_idx: usize,
groups: &[Vec<usize>],
index_map: &mut Vec<usize>,
patterns: &[FusionPattern],
wires: &[WireSource],
view: &NodeView<'_>,
) -> Option<MatchResult> {
if group_idx >= groups.len() {
let reordered: Vec<&WireSource> = index_map.iter().map(|&i| &wires[i]).collect();
return match_ordered(patterns, &reordered, view);
}
let group = &groups[group_idx];
let original_values: Vec<usize> = group.iter().map(|&i| index_map[i]).collect();
for perm in permutations(&original_values) {
for (slot, &val) in group.iter().zip(perm.iter()) {
index_map[*slot] = val;
}
if let Some(m) = recurse(group_idx + 1, groups, index_map, patterns, wires, view) {
return Some(m);
}
}
for (slot, val) in group.iter().zip(original_values.iter()) {
index_map[*slot] = *val;
}
None
}
recurse(0, groups, &mut index_map, patterns, wires, view)
}
fn permutations(items: &[usize]) -> Vec<Vec<usize>> {
if items.len() <= 1 {
return vec![items.to_vec()];
}
let mut result = Vec::new();
for (i, &item) in items.iter().enumerate() {
let rest: Vec<usize> = items
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, &v)| v)
.collect();
for mut perm in permutations(&rest) {
perm.insert(0, item);
result.push(perm);
}
}
result
}
pub fn apply_fusions(
nodes: &mut Vec<Option<Box<dyn PolydatNode>>>,
wiring: &mut Vec<Vec<WireSource>>,
name_to_idx: &mut std::collections::HashMap<String, usize>,
rules: &[FusionRule],
output_nodes: &[usize],
) -> usize {
let mut total_fused = 0;
loop {
let mut fused_this_pass = false;
let consumer_counts = compute_consumer_counts(nodes, wiring);
let view = NodeView { nodes, wiring };
let mut best_match: Option<(usize, MatchResult, &FusionRule)> = None;
'rule_loop: for rule in rules {
let root_op = match rule.pattern.root_op() {
Some(op) => op,
None => continue,
};
for node_idx in 0..view.nodes.len() {
let node = match &view.nodes[node_idx] {
Some(n) => n,
None => continue, };
if node.meta().name != root_op {
continue;
}
let source = WireSource::NodeOutput(node_idx, 0);
let result = match try_match(&rule.pattern, &source, &view) {
Some(r) => r,
None => continue,
};
if !check_consumer_guard(&result, node_idx, &consumer_counts, output_nodes) {
continue;
}
best_match = Some((node_idx, result, rule));
break 'rule_loop;
}
}
if let Some((root_idx, result, rule)) = best_match {
apply_single_fusion(root_idx, &result, rule, nodes, wiring, name_to_idx);
total_fused += 1;
fused_this_pass = true;
}
if !fused_this_pass {
break;
}
}
total_fused
}
fn check_consumer_guard(
result: &MatchResult,
root_idx: usize,
consumer_counts: &[usize],
output_nodes: &[usize],
) -> bool {
for &consumed in &result.consumed_nodes {
if consumed == root_idx {
continue;
}
if output_nodes.contains(&consumed) {
return false;
}
if consumer_counts[consumed] > 1 {
return false;
}
}
true
}
fn compute_consumer_counts(
nodes: &[Option<Box<dyn PolydatNode>>],
wiring: &[Vec<WireSource>],
) -> Vec<usize> {
let mut counts = vec![0usize; nodes.len()];
for (node_idx, node_wiring) in wiring.iter().enumerate() {
if nodes[node_idx].is_none() {
continue;
}
for source in node_wiring {
if let WireSource::NodeOutput(upstream, _) = source {
counts[*upstream] += 1;
}
}
}
counts
}
fn apply_single_fusion(
root_idx: usize,
result: &MatchResult,
rule: &FusionRule,
nodes: &mut [Option<Box<dyn PolydatNode>>],
wiring: &mut [Vec<WireSource>],
name_to_idx: &mut std::collections::HashMap<String, usize>,
) {
let fused_node = (rule.replacement)(result);
let _fused_name = fused_node.meta().name.clone();
let fused_wiring: Vec<WireSource> = rule
.input_bindings
.iter()
.map(|bind_name| result.wire(bind_name).clone())
.collect();
for &consumed in &result.consumed_nodes {
if consumed != root_idx {
nodes[consumed] = None;
wiring[consumed] = Vec::new();
}
}
nodes[root_idx] = Some(fused_node);
wiring[root_idx] = fused_wiring;
name_to_idx.retain(|_, &mut idx| !result.consumed_nodes.contains(&idx) || idx == root_idx);
}
pub struct FusionRuleRegistration {
pub priority: u32,
pub build: fn() -> FusionRule,
}
inventory::collect!(FusionRuleRegistration);
pub fn default_rules() -> Vec<FusionRule> {
let mut regs: Vec<&FusionRuleRegistration> = inventory::iter::<FusionRuleRegistration>
.into_iter()
.collect();
regs.sort_by_key(|r| (r.priority, (r.build)().name));
regs.into_iter().map(|r| (r.build)()).collect()
}
pub struct DecomposedGraph {
pub input_count: usize,
pub nodes: Vec<(Box<dyn PolydatNode>, Vec<DecomposedWire>)>,
pub output_wires: Vec<DecomposedWire>,
}
#[derive(Debug, Clone)]
pub enum DecomposedWire {
Input(usize),
Node(usize, usize),
}
impl DecomposedGraph {
pub fn new(input_count: usize) -> Self {
Self {
input_count,
nodes: Vec::new(),
output_wires: Vec::new(),
}
}
pub fn add_node(&mut self, node: Box<dyn PolydatNode>, wires: Vec<DecomposedWire>) -> usize {
let idx = self.nodes.len();
self.nodes.push((node, wires));
idx
}
pub fn set_outputs(&mut self, wires: Vec<DecomposedWire>) {
self.output_wires = wires;
}
pub fn eval(&self, inputs: &[crate::ast::Value]) -> Vec<crate::ast::Value> {
use crate::ast::Value;
let mut node_outputs: Vec<Vec<Value>> = Vec::new();
for (node, wire_sources) in &self.nodes {
let node_inputs: Vec<Value> = wire_sources
.iter()
.map(|w| match w {
DecomposedWire::Input(i) => inputs[*i].clone(),
DecomposedWire::Node(n, p) => node_outputs[*n][*p].clone(),
})
.collect();
let output_count = node.meta().outs.len();
let mut outputs = vec![Value::None; output_count];
node.eval(&node_inputs, &mut outputs);
node_outputs.push(outputs);
}
self.output_wires
.iter()
.map(|w| match w {
DecomposedWire::Input(i) => inputs[*i].clone(),
DecomposedWire::Node(n, p) => node_outputs[*n][*p].clone(),
})
.collect()
}
}
pub trait FusedNode: PolydatNode {
fn decomposed(&self) -> DecomposedGraph;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permutations_small() {
let p = permutations(&[0, 1]);
assert_eq!(p.len(), 2);
assert!(p.contains(&vec![0, 1]));
assert!(p.contains(&vec![1, 0]));
let p3 = permutations(&[0, 1, 2]);
assert_eq!(p3.len(), 6);
}
#[test]
fn permutations_single() {
let p = permutations(&[42]);
assert_eq!(p, vec![vec![42]]);
}
#[test]
fn permutations_empty() {
let p: Vec<Vec<usize>> = permutations(&[]);
assert_eq!(p, vec![Vec::<usize>::new()]);
}
#[test]
fn match_result_string_bindings() {
let mut m = MatchResult::new();
m.wires.push(("x".to_string(), WireSource::Input(0)));
m.constants.push(("mod_node".to_string(), vec![42]));
m.typed_constants.push((
"mod_node".to_string(),
vec![crate::ast::ConstValue::U64(42)],
));
assert_eq!(m.const_u64("mod_node"), 42);
assert_eq!(m.typed_consts("mod_node").len(), 1);
assert!(matches!(m.wire("x"), WireSource::Input(0)));
}
}