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 ChunkTiers {
pub name: String,
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 ChunkTiers {
pub fn reaches_native(&self) -> bool {
self.block_compiled || self.loops.iter().any(|l| l.traced)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub chunks: Vec<ChunkTiers>,
}
impl Report {
pub fn reaches_native(&self) -> bool {
self.chunks.iter().any(|c| c.reaches_native())
}
}
impl std::fmt::Display for ChunkTiers {
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}")?;
}
}
Ok(())
}
}
impl std::fmt::Display for Report {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = self.chunks.len() > 1;
for c in &self.chunks {
if label {
writeln!(f, "== {} ==", c.name)?;
}
write!(f, "{c}")?;
}
write!(f, "reaches native code {}", self.reaches_native())
}
}
pub fn report(src: &str) -> Result<Report, String> {
let program = crate::vm_helper::parse_isolated(src);
let main = crate::compile_zsh::ZshCompiler::new().compile(&program);
let mut exec = crate::vm_helper::ShellExecutor::new();
exec.execute_script_zsh_pipeline(src)?;
let mut named = vec![("main".to_string(), main)];
let mut funcs: Vec<(String, Chunk)> = exec
.functions_compiled
.iter()
.map(|(name, chunk)| (format!("function {name}"), chunk.clone()))
.collect();
funcs.sort_by(|a, b| a.0.cmp(&b.0));
named.extend(funcs);
Ok(inspect_all(&named))
}
pub fn inspect(chunk: &Chunk) -> Report {
Report {
chunks: vec![inspect_chunk("main", chunk)],
}
}
pub fn inspect_all(named: &[(String, Chunk)]) -> Report {
Report {
chunks: named.iter().map(|(n, c)| inspect_chunk(n, c)).collect(),
}
}
pub fn inspect_chunk(name: &str, chunk: &Chunk) -> ChunkTiers {
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;
}
}
ChunkTiers {
name: name.to_string(),
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,
}
}
const PROGRAM: &str = "f() {\n local t=0\n local i=0\n while (( i < 2000 )); do\n (( t += i ))\n (( i += 1 ))\n done\n}\nf\n";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rotated_slot_loop_reaches_a_compiled_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.chunks[0].loops.len(), 1, "{report}");
assert!(report.chunks[0].loops[0].traced, "{report}");
assert!(report.reaches_native(), "{report}");
}
#[test]
fn the_loop_lives_in_a_chunk_other_than_main() {
let report = report(PROGRAM).expect("runs");
assert!(report.chunks.len() > 1, "{report}");
assert!(report.chunks[0].loops.is_empty(), "main has no loop: {report}");
assert!(
report.chunks[1..].iter().any(|c| !c.loops.is_empty()),
"{report}"
);
}
#[test]
fn the_counted_loop_is_refused_by_the_tracing_tier() {
let report = report(PROGRAM).expect("runs");
let looped = report
.chunks
.iter()
.find(|c| !c.loops.is_empty())
.unwrap_or_else(|| panic!("a chunk with a loop: {report}"));
assert!(!looped.loops[0].trace_eligible, "{report}");
assert!(!looped.loops[0].traced, "{report}");
assert!(!looped.ineligible.is_empty(), "{report}");
assert!(!report.reaches_native(), "{report}");
}
}