1use super::{resolve_cycles, ExecutionResult, FlamegraphConfig, Runner};
2use crate::error::{HostError, Result};
3use crate::receipt::Receipt;
4use riscv_transpiler::abstractions::non_determinism::QuasiUARTSource;
5use riscv_transpiler::common_constants::{
6 rom::ROM_SECOND_WORD_BITS, INITIAL_TIMESTAMP, TIMESTAMP_STEP,
7};
8use riscv_transpiler::cycle::CycleMarkerHooks;
9use riscv_transpiler::ir::{preprocess_bytecode, FullUnsignedMachineDecoderConfig};
10#[cfg(target_arch = "x86_64")]
11use riscv_transpiler::jit::JittedCode;
12use riscv_transpiler::jit::RAM_SIZE;
13use riscv_transpiler::vm::{
14 DelegationsCounters, FlamegraphConfig as VmFlamegraphConfig, RamWithRomRegion, SimpleTape,
15 State, VmFlamegraphProfiler, VM,
16};
17use std::io::Read;
18use std::path::{Path, PathBuf};
19
20pub struct TranspilerRunnerBuilder {
22 app_bin_path: PathBuf,
23 cycles: Option<usize>,
24 text_path: Option<PathBuf>,
25 flamegraph: Option<FlamegraphConfig>,
26 use_jit: bool,
27}
28
29impl TranspilerRunnerBuilder {
30 pub fn new(app_bin_path: impl AsRef<Path>) -> Self {
31 Self {
32 app_bin_path: app_bin_path.as_ref().to_path_buf(),
33 cycles: None,
34 text_path: None,
35 flamegraph: None,
36 use_jit: false,
37 }
38 }
39
40 pub fn with_cycles(mut self, cycles: usize) -> Self {
41 self.cycles = Some(cycles);
42 self
43 }
44
45 pub fn maybe_cycles(self, cycles: Option<usize>) -> Self {
46 match cycles {
47 Some(v) => self.with_cycles(v),
48 None => self,
49 }
50 }
51
52 pub fn with_text_path(mut self, text_path: impl AsRef<Path>) -> Self {
53 self.text_path = Some(text_path.as_ref().to_path_buf());
54 self
55 }
56
57 pub fn maybe_text_path(self, text_path: Option<impl AsRef<Path>>) -> Self {
58 match text_path {
59 Some(v) => self.with_text_path(v),
60 None => self,
61 }
62 }
63
64 pub fn with_flamegraph(mut self, flamegraph: FlamegraphConfig) -> Self {
65 self.flamegraph = Some(flamegraph);
66 self
67 }
68
69 pub fn with_jit(mut self) -> Self {
70 self.use_jit = true;
71 self
72 }
73
74 pub fn build(self) -> Result<TranspilerRunner> {
75 if self.use_jit && cfg!(not(target_arch = "x86_64")) {
76 return Err(HostError::Transpiler(
77 "JIT execution is only available on x86_64 targets".to_string(),
78 ));
79 }
80
81 let app_bin_path = resolve_app_bin_path(&self.app_bin_path)?;
82 let app_text_path = self
83 .text_path
84 .as_deref()
85 .map(resolve_text_path)
86 .unwrap_or_else(|| resolve_text_path(&derive_text_path(&app_bin_path)))?;
87 let cycles = resolve_cycles(self.cycles)?;
88
89 Ok(TranspilerRunner {
90 app_bin_path,
91 app_text_path,
92 cycles,
93 flamegraph: self.flamegraph,
94 use_jit: self.use_jit,
95 })
96 }
97}
98
99pub struct TranspilerRunner {
101 app_bin_path: PathBuf,
102 app_text_path: PathBuf,
103 cycles: usize,
104 flamegraph: Option<FlamegraphConfig>,
105 use_jit: bool,
106}
107
108impl Runner for TranspilerRunner {
109 fn run(&self, input_words: &[u32]) -> Result<ExecutionResult> {
110 if self.flamegraph.is_some() {
111 return self.run_without_jit_with_flamegraph(input_words);
112 }
113
114 if self.use_jit {
115 return self.run_with_jit(input_words);
116 }
117
118 self.run_without_jit(input_words)
119 }
120}
121
122impl TranspilerRunner {
123 #[cfg(target_arch = "x86_64")]
124 fn run_with_jit(&self, input_words: &[u32]) -> Result<ExecutionResult> {
125 let bin_words = read_u32_words(&self.app_bin_path)?;
126 let text_words = read_u32_words(&self.app_text_path)?;
127 let mut non_determinism_source = QuasiUARTSource::new_with_reads(input_words.to_vec());
128
129 let cycles_bound = match u32::try_from(self.cycles) {
130 Ok(value) => Some(value),
131 Err(_) => {
132 tracing::warn!(
133 "cycles limit {} exceeds u32::MAX; running transpiler without a cycle bound",
134 self.cycles
135 );
136 None
137 }
138 };
139
140 let (state, _memory) = JittedCode::run_alternative_simulator(
141 &text_words,
142 &mut non_determinism_source,
143 &bin_words,
144 cycles_bound,
145 );
146 let cycles_executed = ((state.timestamp - INITIAL_TIMESTAMP) / TIMESTAMP_STEP) as usize;
147
148 Ok(ExecutionResult {
149 receipt: Receipt::from_registers(state.registers),
150 cycles_executed,
151 reached_end: true,
152 cycle_markers: None,
153 })
154 }
155
156 #[cfg(not(target_arch = "x86_64"))]
157 fn run_with_jit(&self, _input_words: &[u32]) -> Result<ExecutionResult> {
158 Err(HostError::Transpiler(
159 "JIT execution is only available on x86_64 targets".to_string(),
160 ))
161 }
162
163 fn run_without_jit(&self, input_words: &[u32]) -> Result<ExecutionResult> {
164 self.run_without_jit_internal(input_words, None)
165 }
166
167 fn run_without_jit_with_flamegraph(&self, input_words: &[u32]) -> Result<ExecutionResult> {
168 let flamegraph = self
169 .flamegraph
170 .as_ref()
171 .ok_or_else(|| HostError::Transpiler("flamegraph options are missing".to_string()))?;
172
173 let symbols_path = flamegraph
174 .elf_path
175 .clone()
176 .unwrap_or_else(|| derive_elf_path(&self.app_bin_path));
177 let mut profiler_config = VmFlamegraphConfig::new(symbols_path, flamegraph.output.clone());
178 profiler_config.frequency_recip = flamegraph.sampling_rate;
179 profiler_config.reverse_graph = flamegraph.inverse;
180 let mut profiler = VmFlamegraphProfiler::new(profiler_config).map_err(|err| {
181 HostError::Transpiler(format!("failed to initialize flamegraph profiler: {err}"))
182 })?;
183
184 self.run_without_jit_internal(input_words, Some(&mut profiler))
185 }
186
187 fn run_without_jit_internal(
188 &self,
189 input_words: &[u32],
190 profiler: Option<&mut VmFlamegraphProfiler>,
191 ) -> Result<ExecutionResult> {
192 let bin_words = read_u32_words(&self.app_bin_path)?;
193 let text_words = read_u32_words(&self.app_text_path)?;
194 let instructions = preprocess_bytecode::<FullUnsignedMachineDecoderConfig>(&text_words);
195 let instruction_tape = SimpleTape::new(&instructions);
196 let mut ram =
197 RamWithRomRegion::<{ ROM_SECOND_WORD_BITS }>::from_rom_content(&bin_words, RAM_SIZE);
198 let mut state = State::initial_with_counters(DelegationsCounters::default());
199 let mut non_determinism_source = QuasiUARTSource::new_with_reads(input_words.to_vec());
200
201 let (reached_end, cycle_markers) = CycleMarkerHooks::with(|| match profiler {
202 Some(profiler) => {
203 VM::<DelegationsCounters, CycleMarkerHooks>::run_basic_unrolled_with_flamegraph::<
204 _,
205 _,
206 _,
207 >(
208 &mut state,
209 &mut ram,
210 &mut (),
211 &instruction_tape,
212 self.cycles,
213 &mut non_determinism_source,
214 profiler,
215 )
216 .map_err(|err| {
217 HostError::Transpiler(format!("failed to generate flamegraph: {err}"))
218 })
219 }
220 None => Ok(
221 VM::<DelegationsCounters, CycleMarkerHooks>::run_basic_unrolled::<_, _, _>(
222 &mut state,
223 &mut ram,
224 &mut (),
225 &instruction_tape,
226 self.cycles,
227 &mut non_determinism_source,
228 ),
229 ),
230 });
231 let reached_end = reached_end?;
232
233 let cycles_executed = ((state.timestamp - INITIAL_TIMESTAMP) / TIMESTAMP_STEP) as usize;
234 let registers = state.registers.map(|register| register.value);
235
236 Ok(ExecutionResult {
237 receipt: Receipt::from_registers(registers),
238 cycles_executed,
239 reached_end,
240 cycle_markers: Some(cycle_markers.into()),
241 })
242 }
243}
244
245fn resolve_app_bin_path(path: &Path) -> Result<PathBuf> {
246 if !path.exists() {
247 return Err(HostError::Transpiler(format!(
248 "binary not found: {}",
249 path.display()
250 )));
251 }
252
253 path.canonicalize().map_err(|err| {
254 HostError::Transpiler(format!(
255 "failed to canonicalize binary path {}: {err}",
256 path.display()
257 ))
258 })
259}
260
261fn resolve_text_path(path: &Path) -> Result<PathBuf> {
262 if !path.exists() {
263 return Err(HostError::Transpiler(format!(
264 "text file not found: {}",
265 path.display()
266 )));
267 }
268
269 path.canonicalize().map_err(|err| {
270 HostError::Transpiler(format!(
271 "failed to canonicalize text path {}: {err}",
272 path.display()
273 ))
274 })
275}
276
277fn derive_text_path(bin_path: &Path) -> PathBuf {
278 let mut text_path = bin_path.to_path_buf();
279 text_path.set_extension("text");
280 text_path
281}
282
283fn derive_elf_path(bin_path: &Path) -> PathBuf {
284 let mut elf_path = bin_path.to_path_buf();
285 elf_path.set_extension("elf");
286 elf_path
287}
288
289fn read_u32_words(path: &Path) -> Result<Vec<u32>> {
290 let mut file = std::fs::File::open(path).map_err(|err| {
291 HostError::Transpiler(format!("failed to open {}: {err}", path.display()))
292 })?;
293 let mut bytes = Vec::new();
294 file.read_to_end(&mut bytes).map_err(|err| {
295 HostError::Transpiler(format!("failed to read {}: {err}", path.display()))
296 })?;
297
298 if bytes.len() % 4 != 0 {
299 return Err(HostError::Transpiler(format!(
300 "file length is not a multiple of 4: {}",
301 path.display()
302 )));
303 }
304
305 let mut words = Vec::with_capacity(bytes.len() / 4);
306 for chunk in bytes.as_chunks::<4>().0 {
307 words.push(u32::from_le_bytes(*chunk));
308 }
309 Ok(words)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::TranspilerRunnerBuilder;
315 use crate::runner::Runner;
316 use std::path::Path;
317
318 const MARKER_OPCODE: u32 = 0x7ff01073; const ADDI_OPCODE: u32 = 0x00100093; const LOOP_OPCODE: u32 = 0x0000006f; #[test]
324 fn collects_cycle_markers_for_interpreter_runs() {
325 let dir = tempfile::tempdir().expect("create temp dir");
326 let bin_path = dir.path().join("app.bin");
327 let text_path = dir.path().join("app.text");
328 let program = [MARKER_OPCODE, ADDI_OPCODE, MARKER_OPCODE, LOOP_OPCODE];
329 write_program(&bin_path, &program);
330 write_program(&text_path, &program);
331
332 let runner = TranspilerRunnerBuilder::new(&bin_path)
333 .with_text_path(&text_path)
334 .with_cycles(program.len())
335 .build()
336 .expect("build runner");
337 let execution = runner.run(&[]).expect("run program");
338 let markers = execution.cycle_markers.expect("cycle markers");
339
340 assert!(execution.reached_end);
341 assert_eq!(execution.receipt.registers[1], 1);
342 assert_eq!(markers.markers.len(), 2);
343 assert!(markers.delegation_counter.is_empty());
344 let diff = markers.markers[1].diff(&markers.markers[0]);
345 assert_eq!(diff.cycles, 1);
346 assert!(diff.delegations.is_empty());
347 }
348
349 #[cfg(target_arch = "x86_64")]
350 #[test]
351 fn jit_runs_do_not_collect_cycle_markers() {
352 let dir = tempfile::tempdir().expect("create temp dir");
353 let bin_path = dir.path().join("app.bin");
354 let text_path = dir.path().join("app.text");
355 let program = [ADDI_OPCODE, LOOP_OPCODE];
356 write_program(&bin_path, &program);
357 write_program(&text_path, &program);
358
359 let runner = TranspilerRunnerBuilder::new(&bin_path)
360 .with_text_path(&text_path)
361 .with_cycles(program.len())
362 .with_jit()
363 .build()
364 .expect("build runner");
365 let execution = runner.run(&[]).expect("run program");
366
367 assert_eq!(execution.receipt.registers[1], 1);
368 assert!(execution.cycle_markers.is_none());
369 }
370
371 fn write_program(path: &Path, program: &[u32]) {
372 let bytes: Vec<u8> = program.iter().flat_map(|word| word.to_le_bytes()).collect();
373 std::fs::write(path, bytes).expect("write test program");
374 }
375}