use std::path::Path;
use candle_core::Result;
use crate::inference::InferencePipeline;
use crate::prime::collect_source_files;
use crate::skeleton::{build_digest, expand_symbol};
const DEFAULT_MAX_FILES: usize = 2000;
const DECL_KEYWORDS: &[&str] = &[
"fn ",
"func ",
"def ",
"function ",
"struct ",
"enum ",
"trait ",
"interface ",
"class ",
"type ",
];
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.filter(|n| *n > 0)
.unwrap_or(default)
}
fn symbol_from_signature(line: &str) -> Option<String> {
let t = line.trim_start();
for kw in DECL_KEYWORDS {
if let Some(rest) = t.split_once(kw).map(|(_, r)| r) {
let prefix = &t[..t.len() - rest.len() - kw.len()];
if !prefix.is_empty()
&& prefix
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
{
continue;
}
let name: String = rest
.trim_start()
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
return Some(name);
}
}
}
None
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BenchReport {
pub files: usize,
pub original_tokens: usize,
pub skeleton_tokens: usize,
pub symbols_total: usize,
pub symbols_recovered: usize,
}
impl BenchReport {
pub fn savings_ratio(&self) -> f64 {
if self.original_tokens == 0 {
return 0.0;
}
1.0 - (self.skeleton_tokens as f64 / self.original_tokens as f64)
}
pub fn fidelity_ratio(&self) -> f64 {
if self.symbols_total == 0 {
return 1.0;
}
self.symbols_recovered as f64 / self.symbols_total as f64
}
}
pub fn run_bench(target: &Path, pipeline: &InferencePipeline) -> Result<BenchReport> {
let max_files = env_usize("AXIOM_BENCH_MAX_FILES", DEFAULT_MAX_FILES);
let files = collect_source_files(target, max_files);
let mut report = BenchReport::default();
for path in &files {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
if source.trim().is_empty() {
continue;
}
let original_tokens = pipeline.token_count(&source);
let digest = build_digest(&source, "bench", original_tokens, 0.0, "bench", 3);
let skeleton_tokens = pipeline.token_count(&digest);
report.files += 1;
report.original_tokens += original_tokens;
report.skeleton_tokens += skeleton_tokens;
for line in digest.lines() {
if let Some(symbol) = symbol_from_signature(line) {
report.symbols_total += 1;
if expand_symbol(&source, &symbol).is_some() {
report.symbols_recovered += 1;
}
}
}
}
print_report(target, &report);
Ok(report)
}
fn print_report(target: &Path, r: &BenchReport) {
println!("[bench] target: {}", target.display());
println!("[bench] files measured : {}", r.files);
println!("[bench] original tokens : {}", r.original_tokens);
println!("[bench] skeleton tokens : {}", r.skeleton_tokens);
println!(
"[bench] token savings : {:.1}% ({} -> {} tokens)",
r.savings_ratio() * 100.0,
r.original_tokens,
r.skeleton_tokens
);
println!(
"[bench] signatures kept : {} (expand round-trip {}/{} = {:.1}%)",
r.symbols_total,
r.symbols_recovered,
r.symbols_total,
r.fidelity_ratio() * 100.0
);
println!(
"[bench] note: token savings + structural fidelity are measured offline. \
Answer-quality delta needs an upstream model and is not asserted here."
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_extraction_handles_keywords_and_boundaries() {
assert_eq!(symbol_from_signature("pub fn run(a: u8) {").as_deref(), Some("run"));
assert_eq!(symbol_from_signature(" def go(self):").as_deref(), Some("go"));
assert_eq!(symbol_from_signature("class Beta {").as_deref(), Some("Beta"));
assert_eq!(symbol_from_signature("let transform = 1;"), None);
assert_eq!(symbol_from_signature("// just a comment"), None);
}
#[test]
fn report_ratios_are_well_defined_on_empty() {
let r = BenchReport::default();
assert_eq!(r.savings_ratio(), 0.0);
assert_eq!(r.fidelity_ratio(), 1.0); }
}