#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClaimKind {
Index(usize),
Length(usize),
}
#[derive(Debug, Clone)]
pub struct Claim {
pub line: usize,
pub ops: Vec<String>,
pub needs: usize,
pub kind: ClaimKind,
}
pub fn blank_noncode(s: &str) -> String {
let b = s.as_bytes();
let mut out = vec![b' '; b.len()];
let keep_newlines = |out: &mut Vec<u8>, from: usize, to: usize| {
for (k, item) in out.iter_mut().enumerate().take(to).skip(from) {
if b[k] == b'\n' {
*item = b'\n';
}
}
};
let mut i = 0;
while i < b.len() {
if b[i..].starts_with(b"//") {
let end = s[i..].find('\n').map_or(b.len(), |p| i + p);
keep_newlines(&mut out, i, end);
i = end;
} else if b[i..].starts_with(b"/*") {
let mut depth = 0usize;
let mut j = i;
while j < b.len() {
if b[j..].starts_with(b"/*") {
depth += 1;
j += 2;
} else if b[j..].starts_with(b"*/") {
depth -= 1;
j += 2;
if depth == 0 {
break;
}
} else {
j += 1;
}
}
keep_newlines(&mut out, i, j.min(b.len()));
i = j.min(b.len());
} else if let Some(j) = raw_string_end(b, i) {
keep_newlines(&mut out, i, j);
i = j;
} else if b[i] == b'"' {
let mut j = i + 1;
while j < b.len() {
if b[j] == b'\\' {
j += 2;
continue;
}
if b[j] == b'"' {
j += 1;
break;
}
j += 1;
}
let j = j.min(b.len());
keep_newlines(&mut out, i, j);
i = j;
} else if let Some(j) = char_literal_end(b, i) {
i = j; } else {
out[i] = b[i];
i += 1;
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn raw_string_end(b: &[u8], i: usize) -> Option<usize> {
if b[i] != b'r' {
return None;
}
if i > 0 && (b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_') {
return None;
}
let mut j = i + 1;
let mut hashes = 0usize;
while j < b.len() && b[j] == b'#' {
hashes += 1;
j += 1;
}
if j >= b.len() || b[j] != b'"' {
return None;
}
j += 1;
while j < b.len() {
if b[j] == b'"' {
let mut k = j + 1;
let mut seen = 0usize;
while k < b.len() && b[k] == b'#' && seen < hashes {
seen += 1;
k += 1;
}
if seen == hashes {
return Some(k);
}
}
j += 1;
}
Some(b.len())
}
fn char_literal_end(b: &[u8], i: usize) -> Option<usize> {
if b[i] != b'\'' {
return None;
}
if b.get(i + 1) == Some(&b'\\') {
let mut j = i + 2;
while j < b.len() && b[j] != b'\'' && b[j] != b'\n' {
j += 1;
}
return (b.get(j) == Some(&b'\'')).then_some(j + 1);
}
let mut j = i + 1;
if j >= b.len() {
return None;
}
j += 1;
while j < b.len() && (b[j] & 0xC0) == 0x80 {
j += 1; }
(b.get(j) == Some(&b'\'')).then_some(j + 1)
}
#[derive(Debug, Clone)]
pub struct Arm {
pub start: usize,
pub end: usize,
pub ops: Vec<String>,
}
pub fn arms(src: &str) -> Vec<Arm> {
let b = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while let Some(p) = src[i..].find("=>") {
let at = i + p;
i = at + 2;
let pat = &src[pattern_start(b, at)..at];
let ops = op_names(pat);
if ops.is_empty() {
continue;
}
let mut k = at + 2;
while k < b.len() && (b[k] as char).is_whitespace() {
k += 1;
}
if k >= b.len() {
continue;
}
let end = if b[k] == b'{' {
balanced_end(b, k)
} else {
expr_arm_end(b, k)
};
out.push(Arm { start: k, end, ops });
}
out
}
fn pattern_start(b: &[u8], arrow: usize) -> usize {
let mut j = arrow as isize - 1;
let mut depth = 0i32;
while j >= 0 {
let c = b[j as usize];
match c {
b')' | b']' | b'}' => depth += 1,
b'(' | b'[' | b'{' => {
if depth == 0 {
break;
}
depth -= 1;
}
b',' | b';' if depth == 0 => break,
b'>' if depth == 0 && j > 0 && b[j as usize - 1] == b'=' => break,
_ => {}
}
j -= 1;
}
(j + 1) as usize
}
fn op_names(pat: &str) -> Vec<String> {
let pb = pat.as_bytes();
pat.match_indices("Op::")
.filter(|(o, _)| *o == 0 || !(pb[o - 1].is_ascii_alphanumeric() || pb[o - 1] == b'_'))
.map(|(o, _)| {
pat[o + 4..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>()
})
.filter(|n: &String| n.chars().next().is_some_and(char::is_uppercase))
.collect()
}
fn balanced_end(b: &[u8], open: usize) -> usize {
let mut d = 0i32;
let mut e = open;
while e < b.len() {
if b[e] == b'{' {
d += 1;
} else if b[e] == b'}' {
d -= 1;
if d == 0 {
return e;
}
}
e += 1;
}
b.len()
}
fn expr_arm_end(b: &[u8], from: usize) -> usize {
let mut d = 0i32;
let mut e = from;
while e < b.len() {
match b[e] {
b'(' | b'[' | b'{' => d += 1,
b')' | b']' | b'}' => {
if d == 0 {
return e;
}
d -= 1;
}
b',' if d == 0 => return e,
_ => {}
}
e += 1;
}
b.len()
}
fn operand_reads(src: &str) -> Vec<(usize, usize)> {
let mut out = Vec::new();
for (o, _) in src.match_indices("node.inputs") {
let rest = &src[o + "node.inputs".len()..];
let digits: String = if let Some(t) = rest.strip_prefix(".get(") {
t.chars().take_while(char::is_ascii_digit).collect()
} else if let Some(t) = rest.strip_prefix('[') {
t.chars().take_while(char::is_ascii_digit).collect()
} else {
continue;
};
if let Ok(k) = digits.parse::<usize>() {
out.push((o, k));
}
}
out
}
fn operand_count_claims(src: &str) -> Vec<(usize, usize)> {
let mut out = Vec::new();
for (o, _) in src.match_indices("node.inputs.len()") {
let rest = src[o + "node.inputs.len()".len()..].trim_start();
let (op, tail) = if let Some(t) = rest.strip_prefix("==") {
("==", t)
} else if let Some(t) = rest.strip_prefix(">=") {
(">=", t)
} else if let Some(t) = rest.strip_prefix('>') {
(">", t)
} else {
continue;
};
let digits: String = tail
.trim_start()
.chars()
.take_while(char::is_ascii_digit)
.collect();
let Ok(n) = digits.parse::<usize>() else {
continue;
};
out.push((o, if op == ">" { n + 1 } else { n }));
}
out
}
pub fn scan(src: &str) -> Vec<Claim> {
let blanked = blank_noncode(src);
let arm_list = arms(&blanked);
let indexed = operand_reads(&blanked)
.into_iter()
.map(|(o, k)| (o, k + 1, ClaimKind::Index(k)));
let asserted = operand_count_claims(&blanked)
.into_iter()
.map(|(o, n)| (o, n, ClaimKind::Length(n)));
indexed
.chain(asserted)
.filter_map(|(off, needs, kind)| {
let arm = arm_list
.iter()
.filter(|a| a.start <= off && off < a.end)
.min_by_key(|a| a.end - a.start)?;
Some(Claim {
line: blanked[..off].matches('\n').count() + 1,
ops: arm.ops.clone(),
needs,
kind,
})
})
.collect()
}
pub fn arm_count(src: &str) -> usize {
arms(&blank_noncode(src)).len()
}
#[cfg(test)]
mod tests {
use super::*;
fn ops_at(src: &str, needle: &str) -> Vec<String> {
let blanked = blank_noncode(src);
let off = blanked.find(needle).expect("needle survives blanking");
arms(&blanked)
.into_iter()
.filter(|a| a.start <= off && off < a.end)
.min_by_key(|a| a.end - a.start)
.map(|a| a.ops)
.unwrap_or_default()
}
#[test]
fn multi_line_struct_pattern_keeps_its_op_name() {
let src = "match &node.op {\n Op::Conv {\n kernel_size,\n stride,\n } => {\n \
let b = node.inputs.get(2);\n }\n}";
assert_eq!(ops_at(src, "node.inputs.get(2)"), ["Conv"]);
}
#[test]
fn a_previous_arm_does_not_leak_into_the_next_pattern() {
let src = "match &node.op {\n Op::Attention { .. } => { let _ = 1; }\n \
Op::Cast { .. } => { let a = node.inputs[0]; }\n}";
assert_eq!(ops_at(src, "node.inputs[0]"), ["Cast"]);
}
#[test]
fn enum_variants_containing_op_are_not_op_names() {
let src = "match &node.op {\n Op::Reduce { op: ReduceOp::Sum, .. } => \
{ let a = node.inputs[0]; }\n}";
assert_eq!(ops_at(src, "node.inputs[0]"), ["Reduce"]);
}
#[test]
fn a_nested_arm_owns_its_reads() {
let src = "match &node.op {\n Op::Softmax { .. } => {\n match &other {\n \
Op::Binary(_) => { let a = node.inputs[1]; }\n }\n }\n}";
assert_eq!(ops_at(src, "node.inputs[1]"), ["Binary"]);
}
#[test]
fn a_shared_arm_reports_every_op_it_matches() {
let src = "match &node.op {\n Op::Gru { .. } | Op::Rnn { .. } => \
{ let a = node.inputs[4]; }\n}";
let mut got = ops_at(src, "node.inputs[4]");
got.sort();
assert_eq!(got, ["Gru", "Rnn"]);
}
#[test]
fn only_the_current_nodes_operands_are_read() {
let got = operand_reads("let a = graph.node(o).inputs[0]; let b = node.inputs[1];");
assert_eq!(got.len(), 1, "{got:?}");
assert_eq!(got[0].1, 1);
}
#[test]
fn only_asserting_length_comparisons_count() {
let claims = operand_count_claims(
"if node.inputs.len() == 3 {} if node.inputs.len() > 1 {} \
if node.inputs.len() < 9 {} if node.inputs.len() <= 7 {}",
);
let implied: Vec<usize> = claims.iter().map(|c| c.1).collect();
assert_eq!(implied, [3, 2], "== 3 implies 3; > 1 implies 2");
}
#[test]
fn comments_and_strings_are_ignored_without_shifting_offsets() {
let src = "// node.inputs[9]\nlet m = \"node.inputs[8]\";\nlet a = node.inputs[0];";
let blanked = blank_noncode(src);
assert_eq!(blanked.len(), src.len(), "offsets must be preserved");
let got = operand_reads(&blanked);
assert_eq!(got.len(), 1, "{got:?}");
assert_eq!(blanked[..got[0].0].matches('\n').count() + 1, 3);
}
#[test]
fn raw_strings_are_blanked_whole() {
let src = "let s = r#\"node.inputs[7] and a \" quote\"#;\nlet a = node.inputs[0];";
let blanked = blank_noncode(src);
assert_eq!(blanked.len(), src.len());
let got = operand_reads(&blanked);
assert_eq!(got.len(), 1, "raw-string content leaked: {got:?}");
assert_eq!(got[0].1, 0);
}
#[test]
fn a_quote_char_literal_does_not_open_a_string() {
let src = "let q = '\"'; let a = node.inputs[0]; let s = \"x\";";
let blanked = blank_noncode(src);
let got = operand_reads(&blanked);
assert_eq!(
got.len(),
1,
"code after a quote char was swallowed: {got:?}"
);
}
#[test]
fn lifetimes_are_not_char_literals() {
let src = "fn f<'a>(x: &'a str) -> &'a str { let _ = node.inputs[0]; x }";
let blanked = blank_noncode(src);
assert_eq!(operand_reads(&blanked).len(), 1);
}
#[test]
fn nested_block_comments_close_correctly() {
let src = "/* outer /* inner */ still comment node.inputs[9] */\nlet a = node.inputs[0];";
let blanked = blank_noncode(src);
let got = operand_reads(&blanked);
assert_eq!(got.len(), 1, "{got:?}");
assert_eq!(got[0].1, 0);
}
#[test]
fn scan_attributes_both_claim_kinds() {
let src = "match &node.op {\n Op::Conv { .. } => {\n \
if node.inputs.len() == 3 { let a = node.inputs[2]; }\n }\n}";
let claims = scan(src);
assert_eq!(claims.len(), 2, "{claims:?}");
assert!(claims.iter().all(|c| c.ops == ["Conv"]));
assert!(
claims
.iter()
.any(|c| c.kind == ClaimKind::Index(2) && c.needs == 3)
);
assert!(
claims
.iter()
.any(|c| c.kind == ClaimKind::Length(3) && c.needs == 3)
);
}
}