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> {
139 let named = program_chunks(&crate::compile(src)?);
140 crate::eval_str(src)?;
141 Ok(inspect_all(&named))
142}
143
144fn program_chunks(prog: &crate::compiler::Program) -> Vec<(String, Chunk)> {
151 let mut out = vec![("main".to_string(), prog.main.clone())];
152 for (name, f) in &prog.functions {
153 out.push((format!("function {name}"), f.chunk.clone()));
154 }
155 for (i, t) in prog.tries.iter().enumerate() {
156 out.push((format!("try #{i}"), t.block.clone()));
157 if let Some((_, ch)) = &t.handler {
158 out.push((format!("try #{i} catch"), ch.clone()));
159 }
160 if let Some(ch) = &t.finalizer {
161 out.push((format!("try #{i} finally"), ch.clone()));
162 }
163 }
164 out
165}
166
167
168pub fn inspect(chunk: &Chunk) -> Report {
171 Report {
172 chunks: vec![inspect_chunk("main", chunk)],
173 }
174}
175
176pub fn inspect_all(named: &[(String, Chunk)]) -> Report {
178 Report {
179 chunks: named.iter().map(|(n, c)| inspect_chunk(n, c)).collect(),
180 }
181}
182
183pub fn inspect_chunk(name: &str, chunk: &Chunk) -> ChunkTiers {
185 let jit = JitCompiler::new();
186 let loops = loop_anchors(&chunk.ops)
187 .into_iter()
188 .map(|anchor| Loop {
189 anchor,
190 trace_eligible: body_of(&chunk.ops, anchor)
191 .is_some_and(|body| jit.is_trace_eligible(body, anchor)),
192 traced: jit.trace_is_compiled(chunk, anchor),
193 blacklisted: jit.trace_is_blacklisted(chunk, anchor),
194 })
195 .collect();
196
197 let mut ineligible: BTreeMap<String, usize> = BTreeMap::new();
198 for op in &chunk.ops {
199 if !op_is_eligible(&jit, op) {
200 *ineligible.entry(op_name(op)).or_default() += 1;
201 }
202 }
203
204 ChunkTiers {
205 name: name.to_string(),
206 ops: chunk.ops.len(),
207 block_eligible: jit.is_block_eligible(chunk),
208 block_compiled: jit.block_jit_is_compiled(chunk),
209 largest_eligible_region: jit.find_jit_region(chunk),
210 loops,
211 ineligible,
212 }
213}
214
215fn loop_anchors(ops: &[Op]) -> Vec<usize> {
217 let mut anchors: Vec<usize> = ops
218 .iter()
219 .enumerate()
220 .filter_map(|(ip, op)| match op {
221 Op::Jump(t)
222 | Op::JumpIfTrue(t)
223 | Op::JumpIfFalse(t)
224 | Op::JumpIfTrueKeep(t)
225 | Op::JumpIfFalseKeep(t)
226 if *t <= ip =>
227 {
228 Some(*t)
229 }
230 _ => None,
231 })
232 .collect();
233 anchors.sort_unstable();
234 anchors.dedup();
235 anchors
236}
237
238fn body_of(ops: &[Op], anchor: usize) -> Option<&[Op]> {
241 let close = ops.iter().enumerate().position(|(ip, op)| {
242 ip >= anchor
243 && matches!(
244 op,
245 Op::Jump(t) | Op::JumpIfTrue(t) | Op::JumpIfFalse(t)
246 if *t == anchor
247 )
248 })?;
249 Some(&ops[anchor..=close])
250}
251
252fn op_is_eligible(jit: &JitCompiler, op: &Op) -> bool {
256 let mut b = ChunkBuilder::new();
257 b.emit(op.clone(), 1);
258 jit.is_block_eligible(&b.build())
259}
260
261fn op_name(op: &Op) -> String {
263 let text = format!("{op:?}");
264 match text.split_once('(') {
265 Some((name, _)) => name.to_string(),
266 None => text,
267 }
268}
269
270
271const PROGRAM: &str = "function f(n) {\n let t = 0;\n let i = 0;\n while (i < n) { t += i; i += 1; }\n return t;\n}\nf(200000);\n";
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
285 fn a_rotated_slot_loop_reaches_a_compiled_trace() {
286 let mut b = ChunkBuilder::new();
287 b.emit(Op::LoadInt(0), 1);
288 b.emit(Op::SetSlot(0), 1);
289 let enter = b.emit(Op::Jump(usize::MAX), 1);
290 let body = b.current_pos();
291 b.emit(Op::GetSlot(0), 1);
292 b.emit(Op::LoadInt(1), 1);
293 b.emit(Op::Add, 1);
294 b.emit(Op::SetSlot(0), 1);
295 let cond = b.current_pos();
296 b.patch_jump(enter, cond);
297 b.emit(Op::GetSlot(0), 1);
298 b.emit(Op::LoadInt(200_000), 1);
299 b.emit(Op::NumLt, 1);
300 b.emit(Op::JumpIfTrue(body), 1);
301 b.emit(Op::GetSlot(0), 1);
302 let chunk = b.build();
303
304 let mut vm = fusevm::VM::new(chunk.clone());
305 vm.enable_tracing_jit();
306 vm.run();
307
308 let report = inspect(&chunk);
309 assert_eq!(report.chunks[0].loops.len(), 1, "{report}");
310 assert!(report.chunks[0].loops[0].traced, "{report}");
311 assert!(report.reaches_native(), "{report}");
312 }
313
314 #[test]
318 fn the_loop_lives_in_a_chunk_other_than_main() {
319 let report = report(PROGRAM).expect("runs");
320 assert!(report.chunks.len() > 1, "{report}");
321 assert!(report.chunks[0].loops.is_empty(), "main has no loop: {report}");
322 assert!(
323 report.chunks[1..].iter().any(|c| !c.loops.is_empty()),
324 "{report}"
325 );
326 }
327
328 #[test]
334 fn the_counted_loop_is_refused_by_the_tracing_tier() {
335 let report = report(PROGRAM).expect("runs");
336 let looped = report
337 .chunks
338 .iter()
339 .find(|c| !c.loops.is_empty())
340 .unwrap_or_else(|| panic!("a chunk with a loop: {report}"));
341 assert!(!looped.loops[0].trace_eligible, "{report}");
342 assert!(!looped.loops[0].traced, "{report}");
343 assert!(!looped.ineligible.is_empty(), "{report}");
344 assert!(!report.reaches_native(), "{report}");
345 }
346}