use crate::const_fold::{evaluate, is_pure};
use rlx_ir::numeric_check::{BadValue, first_bad, fix_hint};
use rlx_ir::provenance::node_label;
use rlx_ir::{Graph, NodeId, Op};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct NumericLint {
pub node: NodeId,
pub label: String,
pub kind: BadValue,
pub reason: &'static str,
pub fix: Option<&'static str>,
}
impl std::fmt::Display for NumericLint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} \"{}\": {}",
self.node,
self.kind.as_str(),
self.label,
self.reason
)?;
if let Some(fix) = self.fix {
write!(f, "\n fix: {fix}")?;
}
Ok(())
}
}
fn decode_f32(data: &[u8]) -> Vec<f32> {
data.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
pub fn lint_numerics(graph: &Graph) -> Vec<NumericLint> {
let mut values: HashMap<NodeId, Vec<f32>> = HashMap::new();
let mut findings = Vec::new();
for node in graph.nodes() {
match &node.op {
Op::Constant { data } => {
let vals = decode_f32(data);
if let Some(hit) = first_bad(&vals) {
findings.push(NumericLint {
node: node.id,
label: node_label(graph, node.id),
kind: hit.kind,
reason: "constant literal is non-finite",
fix: fix_hint(&node.op),
});
}
values.insert(node.id, vals);
}
Op::Input { .. } | Op::Param { .. } => {}
op if is_pure(op) && node.inputs.iter().all(|i| values.contains_key(i)) => {
let inputs: Vec<&Vec<f32>> = node.inputs.iter().map(|i| &values[i]).collect();
let in_dims: Option<Vec<Vec<usize>>> = node
.inputs
.iter()
.map(|i| crate::const_fold::static_dims(&graph.node(*i).shape))
.collect();
if let Some(in_dims) = in_dims
&& let Some(result) = evaluate(node, &inputs, &in_dims)
{
if let Some(hit) = first_bad(&result) {
findings.push(NumericLint {
node: node.id,
label: node_label(graph, node.id),
kind: hit.kind,
reason: "constant subgraph folds to a non-finite value",
fix: fix_hint(&node.op),
});
}
values.insert(node.id, result);
}
}
_ => {}
}
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
use rlx_ir::{DType, Shape, op::BinaryOp};
fn constant(g: &mut Graph, v: f32) -> NodeId {
g.add_node(
Op::Constant {
data: v.to_le_bytes().to_vec(),
},
vec![],
Shape::new(&[1], DType::F32),
)
}
#[test]
fn flags_div_by_zero_constant() {
let mut g = Graph::new("t");
let a = constant(&mut g, 1.0);
let b = constant(&mut g, 0.0);
let d = g.binary(BinaryOp::Div, a, b, Shape::new(&[1], DType::F32));
g.set_outputs(vec![d]);
let lints = lint_numerics(&g);
assert_eq!(lints.len(), 1, "one finding expected, got {lints:?}");
assert_eq!(lints[0].node, d);
assert_eq!(lints[0].kind, BadValue::PosInf);
assert!(lints[0].fix.is_some());
}
#[test]
fn flags_literal_nan_constant() {
let mut g = Graph::new("t");
let n = constant(&mut g, f32::NAN);
g.set_outputs(vec![n]);
let lints = lint_numerics(&g);
assert_eq!(lints.len(), 1);
assert_eq!(lints[0].kind, BadValue::Nan);
}
#[test]
fn clean_constant_arithmetic_is_silent() {
let mut g = Graph::new("t");
let a = constant(&mut g, 2.0);
let b = constant(&mut g, 4.0);
let d = g.binary(BinaryOp::Div, a, b, Shape::new(&[1], DType::F32));
g.set_outputs(vec![d]);
assert!(lint_numerics(&g).is_empty());
}
#[test]
fn runtime_div_is_not_flagged() {
let mut g = Graph::new("t");
let x = g.input("x", Shape::new(&[1], DType::F32));
let z = constant(&mut g, 0.0);
let d = g.binary(BinaryOp::Div, x, z, Shape::new(&[1], DType::F32));
g.set_outputs(vec![d]);
assert!(
lint_numerics(&g).is_empty(),
"runtime-dependent div must not be flagged"
);
}
}