1use std::collections::BTreeMap;
13
14use fusevm::{Chunk, ChunkBuilder, JitCompiler, Op};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Loop {
19 pub anchor: usize,
21 pub trace_eligible: bool,
26 pub traced: bool,
28 pub blacklisted: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ChunkTiers {
35 pub name: String,
37 pub ops: usize,
39 pub block_eligible: bool,
42 pub block_compiled: bool,
44 pub largest_eligible_region: Option<(usize, usize)>,
47 pub loops: Vec<Loop>,
49 pub ineligible: BTreeMap<String, usize>,
57}
58
59impl ChunkTiers {
60 pub fn reaches_native(&self) -> bool {
62 self.block_compiled || self.loops.iter().any(|l| l.traced)
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Report {
69 pub chunks: Vec<ChunkTiers>,
71}
72
73impl Report {
74 pub fn reaches_native(&self) -> bool {
77 self.chunks.iter().any(|c| c.reaches_native())
78 }
79}
80
81impl std::fmt::Display for ChunkTiers {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 writeln!(f, "ops {}", self.ops)?;
84 writeln!(f, "block-JIT eligible {}", self.block_eligible)?;
85 writeln!(f, "block-JIT compiled {}", self.block_compiled)?;
86 match self.largest_eligible_region {
87 Some((s, e)) => writeln!(f, "largest eligible region {s}..{e} ({} ops)", e - s)?,
88 None => writeln!(f, "largest eligible region none")?,
89 }
90 if self.loops.is_empty() {
91 writeln!(f, "loops none")?;
92 }
93 for l in &self.loops {
94 writeln!(
95 f,
96 "loop @{:<4} trace-eligible={} traced={} blacklisted={}",
97 l.anchor, l.trace_eligible, l.traced, l.blacklisted
98 )?;
99 }
100 if self.ineligible.is_empty() {
101 writeln!(f, "block-ineligible ops none")?;
102 } else {
103 writeln!(f, "block-ineligible ops")?;
104 for (name, count) in &self.ineligible {
105 writeln!(f, " {name:<22}{count}")?;
106 }
107 }
108 Ok(())
109 }
110}
111
112impl std::fmt::Display for Report {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 let label = self.chunks.len() > 1;
117 for c in &self.chunks {
118 if label {
119 writeln!(f, "== {} ==", c.name)?;
120 }
121 write!(f, "{c}")?;
122 }
123 write!(f, "reaches native code {}", self.reaches_native())
124 }
125}
126
127pub fn report(src: &str) -> Result<Report, String> {
142 let program = crate::vm_helper::parse_isolated(src);
143 let main = crate::compile_zsh::ZshCompiler::new().compile(&program);
144
145 let mut exec = crate::vm_helper::ShellExecutor::new();
146 exec.execute_script_zsh_pipeline(src)?;
147
148 let mut named = vec![("main".to_string(), main)];
149 let mut funcs: Vec<(String, Chunk)> = exec
150 .functions_compiled
151 .iter()
152 .map(|(name, chunk)| (format!("function {name}"), chunk.clone()))
153 .collect();
154 funcs.sort_by(|a, b| a.0.cmp(&b.0));
157 named.extend(funcs);
158 Ok(inspect_all(&named))
159}
160
161pub fn inspect(chunk: &Chunk) -> Report {
164 Report {
165 chunks: vec![inspect_chunk("main", chunk)],
166 }
167}
168
169pub fn inspect_all(named: &[(String, Chunk)]) -> Report {
171 Report {
172 chunks: named.iter().map(|(n, c)| inspect_chunk(n, c)).collect(),
173 }
174}
175
176pub fn inspect_chunk(name: &str, chunk: &Chunk) -> ChunkTiers {
178 let jit = JitCompiler::new();
179 let loops = loop_anchors(&chunk.ops)
180 .into_iter()
181 .map(|anchor| Loop {
182 anchor,
183 trace_eligible: body_of(&chunk.ops, anchor)
184 .is_some_and(|body| jit.is_trace_eligible(body, anchor)),
185 traced: jit.trace_is_compiled(chunk, anchor),
186 blacklisted: jit.trace_is_blacklisted(chunk, anchor),
187 })
188 .collect();
189
190 let mut ineligible: BTreeMap<String, usize> = BTreeMap::new();
191 for op in &chunk.ops {
192 if !op_is_eligible(&jit, op) {
193 *ineligible.entry(op_name(op)).or_default() += 1;
194 }
195 }
196
197 ChunkTiers {
198 name: name.to_string(),
199 ops: chunk.ops.len(),
200 block_eligible: jit.is_block_eligible(chunk),
201 block_compiled: jit.block_jit_is_compiled(chunk),
202 largest_eligible_region: jit.find_jit_region(chunk),
203 loops,
204 ineligible,
205 }
206}
207
208fn loop_anchors(ops: &[Op]) -> Vec<usize> {
210 let mut anchors: Vec<usize> = ops
211 .iter()
212 .enumerate()
213 .filter_map(|(ip, op)| match op {
214 Op::Jump(t)
215 | Op::JumpIfTrue(t)
216 | Op::JumpIfFalse(t)
217 | Op::JumpIfTrueKeep(t)
218 | Op::JumpIfFalseKeep(t)
219 if *t <= ip =>
220 {
221 Some(*t)
222 }
223 _ => None,
224 })
225 .collect();
226 anchors.sort_unstable();
227 anchors.dedup();
228 anchors
229}
230
231fn body_of(ops: &[Op], anchor: usize) -> Option<&[Op]> {
234 let close = ops.iter().enumerate().position(|(ip, op)| {
235 ip >= anchor
236 && matches!(
237 op,
238 Op::Jump(t) | Op::JumpIfTrue(t) | Op::JumpIfFalse(t)
239 if *t == anchor
240 )
241 })?;
242 Some(&ops[anchor..=close])
243}
244
245fn op_is_eligible(jit: &JitCompiler, op: &Op) -> bool {
249 let mut b = ChunkBuilder::new();
250 b.emit(op.clone(), 1);
251 jit.is_block_eligible(&b.build())
252}
253
254fn op_name(op: &Op) -> String {
256 let text = format!("{op:?}");
257 match text.split_once('(') {
258 Some((name, _)) => name.to_string(),
259 None => text,
260 }
261}
262
263#[cfg(test)]
265const 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";
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
278 fn a_rotated_slot_loop_reaches_a_compiled_trace() {
279 let mut b = ChunkBuilder::new();
280 b.emit(Op::LoadInt(0), 1);
281 b.emit(Op::SetSlot(0), 1);
282 let enter = b.emit(Op::Jump(usize::MAX), 1);
283 let body = b.current_pos();
284 b.emit(Op::GetSlot(0), 1);
285 b.emit(Op::LoadInt(1), 1);
286 b.emit(Op::Add, 1);
287 b.emit(Op::SetSlot(0), 1);
288 let cond = b.current_pos();
289 b.patch_jump(enter, cond);
290 b.emit(Op::GetSlot(0), 1);
291 b.emit(Op::LoadInt(200_000), 1);
292 b.emit(Op::NumLt, 1);
293 b.emit(Op::JumpIfTrue(body), 1);
294 b.emit(Op::GetSlot(0), 1);
295 let chunk = b.build();
296
297 let mut vm = fusevm::VM::new(chunk.clone());
298 vm.enable_tracing_jit();
299 vm.run();
300
301 let report = inspect(&chunk);
302 assert_eq!(report.chunks[0].loops.len(), 1, "{report}");
303 assert!(report.chunks[0].loops[0].traced, "{report}");
304 assert!(report.reaches_native(), "{report}");
305 }
306
307 #[test]
311 fn the_loop_lives_in_a_chunk_other_than_main() {
312 let report = report(PROGRAM).expect("runs");
313 assert!(report.chunks.len() > 1, "{report}");
314 assert!(
315 report.chunks[0].loops.is_empty(),
316 "main has no loop: {report}"
317 );
318 assert!(
319 report.chunks[1..].iter().any(|c| !c.loops.is_empty()),
320 "{report}"
321 );
322 }
323
324 #[test]
330 fn the_counted_loop_is_refused_by_the_tracing_tier() {
331 let report = report(PROGRAM).expect("runs");
332 let looped = report
333 .chunks
334 .iter()
335 .find(|c| !c.loops.is_empty())
336 .unwrap_or_else(|| panic!("a chunk with a loop: {report}"));
337 assert!(!looped.loops[0].trace_eligible, "{report}");
338 assert!(!looped.loops[0].traced, "{report}");
339 assert!(!looped.ineligible.is_empty(), "{report}");
340 assert!(!report.reaches_native(), "{report}");
341 }
342}