1use super::{resolve_cycles, ExecutionResult, FlamegraphConfig, Runner};
2use crate::error::{HostError, Result};
3use crate::machine::{MachineProfile, TranspilerDecoderConfig};
4use crate::receipt::Receipt;
5use riscv_transpiler::abstractions::non_determinism::QuasiUARTSource;
6use riscv_transpiler::common_constants::{
7 rom::ROM_SECOND_WORD_BITS, INITIAL_TIMESTAMP, TIMESTAMP_STEP,
8};
9use riscv_transpiler::cycle::CycleMarkerHooks;
10use riscv_transpiler::ir::DecodingOptions;
11#[cfg(target_arch = "x86_64")]
12use riscv_transpiler::jit::JittedCode;
13use riscv_transpiler::jit::RAM_SIZE;
14use riscv_transpiler::vm::{
15 DelegationsCounters, FlamegraphConfig as VmFlamegraphConfig, RamWithRomRegion, SimpleTape,
16 State, VmFlamegraphProfiler, VM,
17};
18use std::io::Read;
19use std::path::{Path, PathBuf};
20
21pub struct TranspilerRunnerBuilder {
23 app_bin_path: PathBuf,
24 cycles: Option<usize>,
25 text_path: Option<PathBuf>,
26 flamegraph: Option<FlamegraphConfig>,
27 decoder: TranspilerDecoderConfig,
28 use_jit: bool,
29}
30
31impl TranspilerRunnerBuilder {
32 pub fn new(app_bin_path: impl AsRef<Path>) -> Self {
33 Self {
34 app_bin_path: app_bin_path.as_ref().to_path_buf(),
35 cycles: None,
36 text_path: None,
37 flamegraph: None,
38 decoder: TranspilerDecoderConfig::default(),
39 use_jit: false,
40 }
41 }
42
43 pub fn with_cycles(mut self, cycles: usize) -> Self {
44 self.cycles = Some(cycles);
45 self
46 }
47
48 pub fn maybe_cycles(self, cycles: Option<usize>) -> Self {
49 match cycles {
50 Some(v) => self.with_cycles(v),
51 None => self,
52 }
53 }
54
55 pub fn with_text_path(mut self, text_path: impl AsRef<Path>) -> Self {
56 self.text_path = Some(text_path.as_ref().to_path_buf());
57 self
58 }
59
60 pub fn maybe_text_path(self, text_path: Option<impl AsRef<Path>>) -> Self {
61 match text_path {
62 Some(v) => self.with_text_path(v),
63 None => self,
64 }
65 }
66
67 pub fn with_flamegraph(mut self, flamegraph: FlamegraphConfig) -> Self {
68 self.flamegraph = Some(flamegraph);
69 self
70 }
71
72 pub fn with_machine_profile(mut self, profile: MachineProfile) -> Self {
78 self.decoder = TranspilerDecoderConfig::from_profile(profile);
79 self
80 }
81
82 pub fn with_unstable_raw_decoder<D>(mut self, name: &'static str) -> Self
95 where
96 D: DecodingOptions,
97 {
98 self.decoder = TranspilerDecoderConfig::unstable_raw::<D>(name);
99 self
100 }
101
102 pub fn with_jit(mut self) -> Self {
103 self.use_jit = true;
104 self
105 }
106
107 pub fn build(self) -> Result<TranspilerRunner> {
108 if self.use_jit && self.decoder.stable_profile != Some(MachineProfile::FullUnsigned) {
113 return Err(HostError::Transpiler(format!(
114 "JIT execution is currently available only for the full unsigned machine profile; configured decoder is {}",
115 self.decoder.name()
116 )));
117 }
118
119 if self.use_jit && cfg!(not(target_arch = "x86_64")) {
120 return Err(HostError::Transpiler(
121 "JIT execution is only available on x86_64 targets".to_string(),
122 ));
123 }
124
125 let app_bin_path = resolve_app_bin_path(&self.app_bin_path)?;
126 let app_text_path = self
127 .text_path
128 .as_deref()
129 .map(resolve_text_path)
130 .unwrap_or_else(|| resolve_text_path(&derive_text_path(&app_bin_path)))?;
131 let cycles = resolve_cycles(self.cycles)?;
132
133 Ok(TranspilerRunner {
134 app_bin_path,
135 app_text_path,
136 cycles,
137 flamegraph: self.flamegraph,
138 decoder: self.decoder,
139 use_jit: self.use_jit,
140 })
141 }
142}
143
144pub struct TranspilerRunner {
146 app_bin_path: PathBuf,
147 app_text_path: PathBuf,
148 cycles: usize,
149 flamegraph: Option<FlamegraphConfig>,
150 decoder: TranspilerDecoderConfig,
151 use_jit: bool,
152}
153
154impl Runner for TranspilerRunner {
155 fn run(&self, input_words: &[u32]) -> Result<ExecutionResult> {
156 if self.flamegraph.is_some() {
157 return self.run_without_jit_with_flamegraph(input_words);
158 }
159
160 if self.use_jit {
161 return self.run_with_jit(input_words);
162 }
163
164 self.run_without_jit(input_words)
165 }
166}
167
168impl TranspilerRunner {
169 #[cfg(target_arch = "x86_64")]
170 fn run_with_jit(&self, input_words: &[u32]) -> Result<ExecutionResult> {
171 let bin_words = read_u32_words(&self.app_bin_path)?;
172 let text_words = read_u32_words(&self.app_text_path)?;
173 let mut non_determinism_source = QuasiUARTSource::new_with_reads(input_words.to_vec());
174
175 let cycles_bound = match u32::try_from(self.cycles) {
176 Ok(value) => Some(value),
177 Err(_) => {
178 tracing::warn!(
179 "cycles limit {} exceeds u32::MAX; running transpiler without a cycle bound",
180 self.cycles
181 );
182 None
183 }
184 };
185
186 let (state, _memory) = JittedCode::run_alternative_simulator(
187 &text_words,
188 &mut non_determinism_source,
189 &bin_words,
190 cycles_bound,
191 );
192 let cycles_executed = ((state.timestamp - INITIAL_TIMESTAMP) / TIMESTAMP_STEP) as usize;
193
194 Ok(ExecutionResult {
195 receipt: Receipt::from_registers(state.registers),
196 cycles_executed,
197 reached_end: true,
198 cycle_markers: None,
199 })
200 }
201
202 #[cfg(not(target_arch = "x86_64"))]
203 fn run_with_jit(&self, _input_words: &[u32]) -> Result<ExecutionResult> {
204 Err(HostError::Transpiler(
205 "JIT execution is only available on x86_64 targets".to_string(),
206 ))
207 }
208
209 fn run_without_jit(&self, input_words: &[u32]) -> Result<ExecutionResult> {
210 self.run_without_jit_internal(input_words, None)
211 }
212
213 fn run_without_jit_with_flamegraph(&self, input_words: &[u32]) -> Result<ExecutionResult> {
214 let flamegraph = self
215 .flamegraph
216 .as_ref()
217 .ok_or_else(|| HostError::Transpiler("flamegraph options are missing".to_string()))?;
218
219 let symbols_path = flamegraph
220 .elf_path
221 .clone()
222 .unwrap_or_else(|| derive_elf_path(&self.app_bin_path));
223 let mut profiler_config = VmFlamegraphConfig::new(symbols_path, flamegraph.output.clone());
224 profiler_config.frequency_recip = flamegraph.sampling_rate;
225 profiler_config.reverse_graph = flamegraph.inverse;
226 let mut profiler = VmFlamegraphProfiler::new(profiler_config).map_err(|err| {
227 HostError::Transpiler(format!("failed to initialize flamegraph profiler: {err}"))
228 })?;
229
230 self.run_without_jit_internal(input_words, Some(&mut profiler))
231 }
232
233 fn run_without_jit_internal(
234 &self,
235 input_words: &[u32],
236 profiler: Option<&mut VmFlamegraphProfiler>,
237 ) -> Result<ExecutionResult> {
238 let bin_words = read_u32_words(&self.app_bin_path)?;
239 let text_words = read_u32_words(&self.app_text_path)?;
240 let instructions = self.decoder.preprocess(&text_words);
241 let instruction_tape = SimpleTape::new(&instructions);
242 let mut ram =
243 RamWithRomRegion::<{ ROM_SECOND_WORD_BITS }>::from_rom_content(&bin_words, RAM_SIZE);
244 let mut state = State::initial_with_counters(DelegationsCounters::default());
245 let mut non_determinism_source = QuasiUARTSource::new_with_reads(input_words.to_vec());
246
247 let (reached_end, cycle_markers) = CycleMarkerHooks::with(|| match profiler {
248 Some(profiler) => {
249 VM::<DelegationsCounters, CycleMarkerHooks>::run_basic_unrolled_with_flamegraph::<
250 _,
251 _,
252 _,
253 >(
254 &mut state,
255 &mut ram,
256 &mut (),
257 &instruction_tape,
258 self.cycles,
259 &mut non_determinism_source,
260 profiler,
261 )
262 .map_err(|err| {
263 HostError::Transpiler(format!("failed to generate flamegraph: {err}"))
264 })
265 }
266 None => Ok(
267 VM::<DelegationsCounters, CycleMarkerHooks>::run_basic_unrolled::<_, _, _>(
268 &mut state,
269 &mut ram,
270 &mut (),
271 &instruction_tape,
272 self.cycles,
273 &mut non_determinism_source,
274 ),
275 ),
276 });
277 let reached_end = reached_end?;
278
279 let cycles_executed = ((state.timestamp - INITIAL_TIMESTAMP) / TIMESTAMP_STEP) as usize;
280 let registers = state.registers.map(|register| register.value);
281
282 Ok(ExecutionResult {
283 receipt: Receipt::from_registers(registers),
284 cycles_executed,
285 reached_end,
286 cycle_markers: Some(cycle_markers.into()),
287 })
288 }
289}
290
291fn resolve_app_bin_path(path: &Path) -> Result<PathBuf> {
292 if !path.exists() {
293 return Err(HostError::Transpiler(format!(
294 "binary not found: {}",
295 path.display()
296 )));
297 }
298
299 path.canonicalize().map_err(|err| {
300 HostError::Transpiler(format!(
301 "failed to canonicalize binary path {}: {err}",
302 path.display()
303 ))
304 })
305}
306
307fn resolve_text_path(path: &Path) -> Result<PathBuf> {
308 if !path.exists() {
309 return Err(HostError::Transpiler(format!(
310 "text file not found: {}",
311 path.display()
312 )));
313 }
314
315 path.canonicalize().map_err(|err| {
316 HostError::Transpiler(format!(
317 "failed to canonicalize text path {}: {err}",
318 path.display()
319 ))
320 })
321}
322
323fn derive_text_path(bin_path: &Path) -> PathBuf {
324 let mut text_path = bin_path.to_path_buf();
325 text_path.set_extension("text");
326 text_path
327}
328
329fn derive_elf_path(bin_path: &Path) -> PathBuf {
330 let mut elf_path = bin_path.to_path_buf();
331 elf_path.set_extension("elf");
332 elf_path
333}
334
335fn read_u32_words(path: &Path) -> Result<Vec<u32>> {
336 let mut file = std::fs::File::open(path).map_err(|err| {
337 HostError::Transpiler(format!("failed to open {}: {err}", path.display()))
338 })?;
339 let mut bytes = Vec::new();
340 file.read_to_end(&mut bytes).map_err(|err| {
341 HostError::Transpiler(format!("failed to read {}: {err}", path.display()))
342 })?;
343
344 if bytes.len() % 4 != 0 {
345 return Err(HostError::Transpiler(format!(
346 "file length is not a multiple of 4: {}",
347 path.display()
348 )));
349 }
350
351 let mut words = Vec::with_capacity(bytes.len() / 4);
352 for chunk in bytes.as_chunks::<4>().0 {
353 words.push(u32::from_le_bytes(*chunk));
354 }
355 Ok(words)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::TranspilerRunnerBuilder;
361 use crate::runner::Runner;
362 use crate::MachineProfile;
363 use riscv_transpiler::ir::DebugReducedMachineDecoderConfig;
364 use std::path::Path;
365
366 const MARKER_OPCODE: u32 = 0x7ff01073; const ADDI_OPCODE: u32 = 0x00100093; const LOOP_OPCODE: u32 = 0x0000006f; #[test]
372 fn collects_cycle_markers_for_interpreter_runs() {
373 let dir = tempfile::tempdir().expect("create temp dir");
374 let bin_path = dir.path().join("app.bin");
375 let text_path = dir.path().join("app.text");
376 let program = [MARKER_OPCODE, ADDI_OPCODE, MARKER_OPCODE, LOOP_OPCODE];
377 write_program(&bin_path, &program);
378 write_program(&text_path, &program);
379
380 let runner = TranspilerRunnerBuilder::new(&bin_path)
381 .with_text_path(&text_path)
382 .with_cycles(program.len())
383 .build()
384 .expect("build runner");
385 let execution = runner.run(&[]).expect("run program");
386 let markers = execution.cycle_markers.expect("cycle markers");
387
388 assert!(execution.reached_end);
389 assert_eq!(execution.receipt.registers[1], 1);
390 assert_eq!(markers.markers.len(), 2);
391 assert!(markers.delegation_counter.is_empty());
392 let diff = markers.markers[1].diff(&markers.markers[0]);
393 assert_eq!(diff.cycles, 1);
394 assert!(diff.delegations.is_empty());
395 }
396
397 #[test]
398 fn interpreter_runs_with_reduced_machine_profile() {
399 let dir = tempfile::tempdir().expect("create temp dir");
400 let bin_path = dir.path().join("app.bin");
401 let text_path = dir.path().join("app.text");
402 let program = [ADDI_OPCODE, LOOP_OPCODE];
403 write_program(&bin_path, &program);
404 write_program(&text_path, &program);
405
406 let runner = TranspilerRunnerBuilder::new(&bin_path)
407 .with_text_path(&text_path)
408 .with_machine_profile(MachineProfile::Reduced)
409 .with_cycles(program.len())
410 .build()
411 .expect("build runner");
412 let execution = runner.run(&[]).expect("run program");
413
414 assert_eq!(execution.receipt.registers[1], 1);
415 assert!(execution.cycle_markers.is_some());
416 }
417
418 #[cfg(target_arch = "x86_64")]
419 #[test]
420 fn jit_runs_do_not_collect_cycle_markers() {
421 let dir = tempfile::tempdir().expect("create temp dir");
422 let bin_path = dir.path().join("app.bin");
423 let text_path = dir.path().join("app.text");
424 let program = [ADDI_OPCODE, LOOP_OPCODE];
425 write_program(&bin_path, &program);
426 write_program(&text_path, &program);
427
428 let runner = TranspilerRunnerBuilder::new(&bin_path)
429 .with_text_path(&text_path)
430 .with_cycles(program.len())
431 .with_jit()
432 .build()
433 .expect("build runner");
434 let execution = runner.run(&[]).expect("run program");
435
436 assert_eq!(execution.receipt.registers[1], 1);
437 assert!(execution.cycle_markers.is_none());
438 }
439
440 #[test]
441 fn jit_rejects_non_default_machine_profiles() {
442 let result = TranspilerRunnerBuilder::new("missing.bin")
443 .with_machine_profile(MachineProfile::Reduced)
444 .with_jit()
445 .build();
446 let err = match result {
447 Ok(_) => {
448 panic!("JIT should reject non-default machine profiles before path resolution")
449 }
450 Err(err) => err,
451 };
452
453 assert_eq!(
454 err.to_string(),
455 "transpiler error: JIT execution is currently available only for the full unsigned machine profile; configured decoder is reduced"
456 );
457 }
458
459 #[test]
460 fn jit_reports_raw_decoder_name() {
461 let result = TranspilerRunnerBuilder::new("missing.bin")
462 .with_unstable_raw_decoder::<DebugReducedMachineDecoderConfig>("debug reduced")
463 .with_jit()
464 .build();
465 let err = match result {
466 Ok(_) => panic!("JIT should reject raw decoders before path resolution"),
467 Err(err) => err,
468 };
469
470 assert_eq!(
471 err.to_string(),
472 "transpiler error: JIT execution is currently available only for the full unsigned machine profile; configured decoder is debug reduced"
473 );
474 }
475
476 fn write_program(path: &Path, program: &[u32]) {
477 let bytes: Vec<u8> = program.iter().flat_map(|word| word.to_le_bytes()).collect();
478 std::fs::write(path, bytes).expect("write test program");
479 }
480}