use std::collections::BTreeMap;
use fusevm::{Chunk, ChunkBuilder, JitCompiler, Op};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Loop {
pub anchor: usize,
pub trace_eligible: bool,
pub traced: bool,
pub blacklisted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub ops: usize,
pub block_eligible: bool,
pub block_compiled: bool,
pub largest_eligible_region: Option<(usize, usize)>,
pub loops: Vec<Loop>,
pub ineligible: BTreeMap<String, usize>,
}
impl Report {
pub fn reaches_native(&self) -> bool {
self.block_compiled || self.loops.iter().any(|l| l.traced)
}
}
impl std::fmt::Display for Report {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "ops {}", self.ops)?;
writeln!(f, "block-JIT eligible {}", self.block_eligible)?;
writeln!(f, "block-JIT compiled {}", self.block_compiled)?;
match self.largest_eligible_region {
Some((s, e)) => writeln!(f, "largest eligible region {s}..{e} ({} ops)", e - s)?,
None => writeln!(f, "largest eligible region none")?,
}
if self.loops.is_empty() {
writeln!(f, "loops none")?;
}
for l in &self.loops {
writeln!(
f,
"loop @{:<4} trace-eligible={} traced={} blacklisted={}",
l.anchor, l.trace_eligible, l.traced, l.blacklisted
)?;
}
if self.ineligible.is_empty() {
writeln!(f, "block-ineligible ops none")?;
} else {
writeln!(f, "block-ineligible ops")?;
for (name, count) in &self.ineligible {
writeln!(f, " {name:<22}{count}")?;
}
}
write!(f, "reaches native code {}", self.reaches_native())
}
}
pub fn report(src: &str) -> Result<Report, String> {
let chunk = crate::runtime::compile(src)?;
let mut interp = crate::Interp::capturing();
interp.eval(src).map_err(|e| e.to_string())?;
Ok(inspect(&chunk))
}
pub fn inspect(chunk: &Chunk) -> Report {
let jit = JitCompiler::new();
let loops = loop_anchors(&chunk.ops)
.into_iter()
.map(|anchor| Loop {
anchor,
trace_eligible: body_of(&chunk.ops, anchor)
.is_some_and(|body| jit.is_trace_eligible(body, anchor)),
traced: jit.trace_is_compiled(chunk, anchor),
blacklisted: jit.trace_is_blacklisted(chunk, anchor),
})
.collect();
let mut ineligible: BTreeMap<String, usize> = BTreeMap::new();
for op in &chunk.ops {
if !op_is_eligible(&jit, op) {
*ineligible.entry(op_name(op)).or_default() += 1;
}
}
Report {
ops: chunk.ops.len(),
block_eligible: jit.is_block_eligible(chunk),
block_compiled: jit.block_jit_is_compiled(chunk),
largest_eligible_region: jit.find_jit_region(chunk),
loops,
ineligible,
}
}
fn loop_anchors(ops: &[Op]) -> Vec<usize> {
let mut anchors: Vec<usize> = ops
.iter()
.enumerate()
.filter_map(|(ip, op)| match op {
Op::Jump(t)
| Op::JumpIfTrue(t)
| Op::JumpIfFalse(t)
| Op::JumpIfTrueKeep(t)
| Op::JumpIfFalseKeep(t)
if *t <= ip =>
{
Some(*t)
}
_ => None,
})
.collect();
anchors.sort_unstable();
anchors.dedup();
anchors
}
fn body_of(ops: &[Op], anchor: usize) -> Option<&[Op]> {
let close = ops.iter().enumerate().position(|(ip, op)| {
ip >= anchor
&& matches!(
op,
Op::Jump(t) | Op::JumpIfTrue(t) | Op::JumpIfFalse(t)
if *t == anchor
)
})?;
Some(&ops[anchor..=close])
}
fn op_is_eligible(jit: &JitCompiler, op: &Op) -> bool {
let mut b = ChunkBuilder::new();
b.emit(op.clone(), 1);
jit.is_block_eligible(&b.build())
}
fn op_name(op: &Op) -> String {
let text = format!("{op:?}");
match text.split_once('(') {
Some((name, _)) => name.to_string(),
None => text,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_slot_counter_loop_is_accepted_by_both_tiers() {
let mut b = ChunkBuilder::new();
b.emit(Op::GetSlot(0), 1); b.emit(Op::LoadInt(1000), 1); b.emit(Op::NumLt, 1); b.emit(Op::JumpIfFalse(9), 1); b.emit(Op::GetSlot(0), 1); b.emit(Op::LoadInt(1), 1); b.emit(Op::Add, 1); b.emit(Op::SetSlot(0), 1); b.emit(Op::Jump(0), 1); b.emit(Op::GetSlot(0), 1); let report = inspect(&b.build());
assert!(report.block_eligible, "{report}");
assert!(report.ineligible.is_empty(), "{report}");
assert_eq!(report.loops.len(), 1, "{report}");
assert!(report.loops[0].trace_eligible, "{report}");
}
#[test]
fn the_tcl_counter_loop_is_traced_through_its_globals() {
let report = report("set i 0\nwhile {$i < 1000} {incr i}").expect("runs");
assert!(!report.block_eligible, "{report}");
assert!(
report.ineligible.contains_key("GetVar") && report.ineligible.contains_key("SetVar"),
"the variable ops are what keep the whole chunk out: {report}"
);
assert_eq!(report.loops.len(), 1, "{report}");
assert!(report.loops[0].trace_eligible, "{report}");
assert!(report.reaches_native(), "{report}");
}
#[test]
fn a_proc_local_counter_loop_reaches_a_compiled_trace() {
let report =
report("proc f {} {set i 0; while {$i < 200000} {incr i}; return $i}\nputs [f]")
.expect("runs");
assert_eq!(report.loops.len(), 1, "{report}");
assert!(
report.loops[0].trace_eligible,
"slot ops make the body traceable: {report}"
);
assert!(
!report.ineligible.contains_key("GetVar") && !report.ineligible.contains_key("SetVar"),
"a procedure's locals are slots, not globals: {report}"
);
assert!(report.loops[0].traced, "{report}");
assert!(!report.loops[0].blacklisted, "{report}");
assert!(report.reaches_native(), "{report}");
}
#[test]
fn the_unrotated_shape_of_that_loop_installs_no_trace() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
let anchor = b.current_pos();
b.emit(Op::GetSlot(0), 1);
b.emit(Op::LoadInt(200_000), 1);
b.emit(Op::NumLt, 1);
let exit = b.emit(Op::JumpIfFalse(usize::MAX), 1);
b.emit(Op::GetSlot(0), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::Add, 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::Jump(anchor), 1);
let end = b.current_pos();
b.patch_jump(exit, end);
b.emit(Op::GetSlot(0), 1);
let chunk = b.build();
let mut vm = fusevm::VM::new(chunk.clone());
vm.enable_tracing_jit();
vm.run();
let report = inspect(&chunk);
assert_eq!(report.loops.len(), 1, "{report}");
assert!(
report.loops[0].trace_eligible,
"the recorded sequence is eligible; it is the compile that declines: {report}"
);
assert!(!report.loops[0].traced, "{report}");
}
#[test]
fn the_rotated_shape_of_that_loop_installs_a_trace() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
let enter = b.emit(Op::Jump(usize::MAX), 1);
let body = b.current_pos();
b.emit(Op::GetSlot(0), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::Add, 1);
b.emit(Op::SetSlot(0), 1);
let cond = b.current_pos();
b.patch_jump(enter, cond);
b.emit(Op::GetSlot(0), 1);
b.emit(Op::LoadInt(200_000), 1);
b.emit(Op::NumLt, 1);
b.emit(Op::JumpIfTrue(body), 1);
b.emit(Op::GetSlot(0), 1);
let chunk = b.build();
let mut vm = fusevm::VM::new(chunk.clone());
vm.enable_tracing_jit();
vm.run();
let report = inspect(&chunk);
assert_eq!(report.loops.len(), 1, "{report}");
assert!(report.loops[0].traced, "{report}");
assert!(report.reaches_native(), "{report}");
}
#[test]
fn expr_arithmetic_lowers_to_eligible_ops() {
let report = report("expr {2 + 3 * 4 << 1}").expect("runs");
assert!(
report.ineligible.is_empty(),
"an expression should lower to eligible ops only: {report}"
);
assert!(report.block_eligible, "{report}");
}
#[test]
fn an_expr_assignment_lowers_to_eligible_ops() {
let report = report("set i 0\nset i [expr {$i + 1}]").expect("runs");
assert!(
report
.ineligible
.keys()
.all(|op| op == "GetVar" || op == "SetVar"),
"only the global-variable ops should be left: {report}"
);
}
}