use crate::kernel::WireSource;
use crate::node::{Commutativity, ConstValue, GkNode};
#[derive(Debug, Clone)]
pub enum FusionPattern {
Node {
op: &'static str,
inputs: Vec<FusionPattern>,
bind: &'static str,
},
Any {
bind: &'static str,
},
VariadicNode {
op: &'static str,
child_pattern: Box<FusionPattern>,
bind: &'static str,
min_children: usize,
},
}
impl FusionPattern {
pub fn node(
op: &'static str,
inputs: Vec<FusionPattern>,
bind: &'static str,
) -> Self {
FusionPattern::Node { op, inputs, bind }
}
pub fn any(bind: &'static str) -> Self {
FusionPattern::Any { bind }
}
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 GkNode>,
pub input_bindings: &'static [&'static str],
}
struct NodeView<'a> {
nodes: &'a [Option<Box<dyn GkNode>>],
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: Box::leak(child_bind.clone().into_boxed_str()),
},
_ => *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 GkNode>>>,
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 GkNode>>],
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 Vec<Option<Box<dyn GkNode>>>,
wiring: &mut Vec<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 fn default_rules() -> Vec<FusionRule> {
use crate::nodes::hash::{HashRange, HashInterval};
use crate::nodes::lerp::ScaleRange;
vec![
FusionRule {
name: "hash_mod_to_hash_range",
pattern: FusionPattern::node(
"mod",
vec![
FusionPattern::node("hash", vec![FusionPattern::any("x")], "hash_node"),
],
"mod_node",
),
replacement: |m| {
let max = m.const_u64("mod_node");
Box::new(HashRange::new(max))
},
input_bindings: &["x"],
},
FusionRule {
name: "hash_unit_lerp_to_hash_interval",
pattern: FusionPattern::node(
"lerp",
vec![
FusionPattern::node(
"unit_interval",
vec![
FusionPattern::node(
"hash",
vec![FusionPattern::any("x")],
"hash_node",
),
],
"ui_node",
),
],
"lerp_node",
),
replacement: |m| {
let consts = m.const_vec("lerp_node");
let lo = f64::from_bits(consts[0]);
let hi = f64::from_bits(consts[1]);
Box::new(HashInterval::new(lo, hi))
},
input_bindings: &["x"],
},
FusionRule {
name: "unit_lerp_to_scale_range",
pattern: FusionPattern::node(
"lerp",
vec![
FusionPattern::node(
"unit_interval",
vec![FusionPattern::any("x")],
"ui_node",
),
],
"lerp_node",
),
replacement: |m| {
let consts = m.const_vec("lerp_node");
let lo = f64::from_bits(consts[0]);
let hi = f64::from_bits(consts[1]);
Box::new(ScaleRange::new(lo, hi))
},
input_bindings: &["x"],
},
]
}
pub struct DecomposedGraph {
pub input_count: usize,
pub nodes: Vec<(Box<dyn GkNode>, 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 GkNode>,
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::node::Value]) -> Vec<crate::node::Value> {
use crate::node::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: GkNode {
fn decomposed(&self) -> DecomposedGraph;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assembly::{GkAssembler, WireRef};
use crate::node::Value;
use crate::nodes::arithmetic::ModU64;
use crate::nodes::hash::Hash64;
#[test]
fn hash_mod_fuses_to_hash_range() {
let mut asm = GkAssembler::new(vec!["cycle".into()]);
asm.add_node("h", Box::new(Hash64::new()), vec![WireRef::input("cycle")]);
asm.add_node("m", Box::new(ModU64::new(100)), vec![WireRef::node("h")]);
asm.add_output("out", WireRef::node("m"));
let mut kernel = asm.compile().unwrap();
for cycle in 0..1000u64 {
kernel.set_inputs(&[cycle]);
let result = kernel.pull("out").as_u64();
let expected = xxhash_rust::xxh3::xxh3_64(&cycle.to_le_bytes()) % 100;
assert_eq!(result, expected, "cycle {cycle}");
}
}
#[test]
fn fusion_skipped_when_intermediate_has_consumers() {
let mut asm = GkAssembler::new(vec!["cycle".into()]);
asm.add_node("h", Box::new(Hash64::new()), vec![WireRef::input("cycle")]);
asm.add_node("m", Box::new(ModU64::new(100)), vec![WireRef::node("h")]);
asm.add_node("m2", Box::new(ModU64::new(50)), vec![WireRef::node("h")]);
asm.add_output("out1", WireRef::node("m"));
asm.add_output("out2", WireRef::node("m2"));
let mut kernel = asm.compile().unwrap();
for cycle in 0..100u64 {
kernel.set_inputs(&[cycle]);
let h = xxhash_rust::xxh3::xxh3_64(&cycle.to_le_bytes());
assert_eq!(kernel.pull("out1").as_u64(), h % 100, "out1 cycle {cycle}");
assert_eq!(kernel.pull("out2").as_u64(), h % 50, "out2 cycle {cycle}");
}
}
#[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()]);
}
fn assert_equivalence(fused: &dyn FusedNode, test_count: usize) {
let decomposed = fused.decomposed();
let input_count = fused.meta().wire_inputs().len();
let output_count = fused.meta().outs.len();
for seed in 0..test_count as u64 {
let inputs: Vec<Value> = (0..input_count)
.map(|port| {
let v = xxhash_rust::xxh3::xxh3_64(
&(seed.wrapping_mul(31).wrapping_add(port as u64)).to_le_bytes()
);
match fused.meta().wire_inputs()[port].typ {
crate::node::PortType::U64 => Value::U64(v),
crate::node::PortType::F64 => Value::F64(f64::from_bits(v)),
_ => Value::U64(v), }
})
.collect();
let mut fused_outputs = vec![Value::None; output_count];
fused.eval(&inputs, &mut fused_outputs);
let decomposed_outputs = decomposed.eval(&inputs);
for (port_idx, (fused_val, decomposed_val)) in
fused_outputs.iter().zip(decomposed_outputs.iter()).enumerate()
{
match (&fused_val, &decomposed_val) {
(Value::U64(a), Value::U64(b)) => {
assert_eq!(a, b,
"equivalence failed: seed={seed} port={port_idx} fused={a} decomposed={b}"
);
}
(Value::F64(a), Value::F64(b)) => {
let diff = (a - b).abs();
let tolerance = 1e-10 * a.abs().max(b.abs()).max(1.0);
assert!(diff <= tolerance,
"equivalence failed: seed={seed} port={port_idx} fused={a} decomposed={b} diff={diff}"
);
}
_ => {
assert_eq!(
fused_val.to_display_string(),
decomposed_val.to_display_string(),
"equivalence failed: seed={seed} port={port_idx}"
);
}
}
}
}
}
#[test]
fn hash_range_equivalence() {
use crate::nodes::hash::HashRange;
for max in [1, 2, 7, 100, 10_000, u64::MAX] {
let fused = HashRange::new(max);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn hash_interval_equivalence() {
use crate::nodes::hash::HashInterval;
for (lo, hi) in [(0.0, 1.0), (-180.0, 180.0), (0.0, 1000.0), (-1.0, -0.5)] {
let fused = HashInterval::new(lo, hi);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn scale_range_equivalence() {
use crate::nodes::lerp::ScaleRange;
for (lo, hi) in [(0.0, 1.0), (-100.0, 100.0), (0.0, 360.0), (1e6, 1e7)] {
let fused = ScaleRange::new(lo, hi);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn all_default_rules_produce_equivalent_nodes() {
let rules = default_rules();
for rule in &rules {
assert!(
rule.pattern.root_op().is_some(),
"rule '{}' has no root op",
rule.name
);
assert!(
!rule.input_bindings.is_empty(),
"rule '{}' has no input bindings",
rule.name
);
}
}
#[test]
fn variadic_pattern_matches_sum() {
use crate::nodes::arithmetic::SumN;
let mut asm = GkAssembler::new(vec!["a".into(), "b".into(), "c".into()]);
asm.add_node("s", Box::new(SumN::new(3)), vec![
WireRef::input("a"), WireRef::input("b"), WireRef::input("c"),
]);
asm.add_output("out", WireRef::node("s"));
let mut kernel = asm.compile().unwrap();
kernel.set_inputs(&[10, 20, 30]);
assert_eq!(kernel.pull("out").as_u64(), 60);
}
#[test]
fn typed_constants_captured_in_match() {
use crate::nodes::hash::HashRange;
use crate::node::ConstValue;
let mut asm = GkAssembler::new(vec!["cycle".into()]);
asm.add_node("hr", Box::new(HashRange::new(100)), vec![WireRef::input("cycle")]);
asm.add_output("out", WireRef::node("hr"));
let node = HashRange::new(100);
let typed: Vec<ConstValue> = node.meta().const_slots()
.iter()
.map(|c| c.1.clone())
.collect();
assert_eq!(typed.len(), 1);
assert_eq!(typed[0], ConstValue::U64(100));
}
#[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::node::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)));
}
}