Skip to main content

airbender_host/runner/
transpiler_runner.rs

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
21/// Builder for creating a configured transpiler runner.
22pub 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    /// Selects one of the platform-supported machine profiles for bytecode preprocessing.
73    ///
74    /// The default is [`MachineProfile::FullUnsigned`], which preserves the historical
75    /// `airbender-host` transpiler behavior. Non-default profiles currently require
76    /// interpreter execution; combining them with [`Self::with_jit`] returns an error.
77    pub fn with_machine_profile(mut self, profile: MachineProfile) -> Self {
78        self.decoder = TranspilerDecoderConfig::from_profile(profile);
79        self
80    }
81
82    /// Selects a raw upstream decoder configuration.
83    ///
84    /// This method intentionally exposes `riscv_transpiler` internals and is not
85    /// covered by Airbender Platform's stability guarantees. It is intended for
86    /// advanced users who already accept that upstream Airbender APIs can change
87    /// without a platform-level compatibility layer. Raw decoders currently require
88    /// interpreter execution; combining them with [`Self::with_jit`] returns an error.
89    /// If the decoder emits instructions that the host runner does not implement,
90    /// execution may panic instead of returning a structured error.
91    ///
92    /// `name` is used only for diagnostics, so callers should pass a short domain
93    /// name that will make sense in error messages.
94    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        // Airbender's standalone JIT entry point accepts raw bytecode and is currently
109        // tailored for the full unsigned configuration. Enabling JIT for other profiles
110        // requires profile-aware support in Airbender itself so execution cannot bypass
111        // the decoder semantics that the interpreter path applies here.
112        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
144/// Transpiler based execution runner.
145pub 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; // csrrw x0, 2047, x0
367    const ADDI_OPCODE: u32 = 0x00100093; // addi x1, x0, 1
368    const LOOP_OPCODE: u32 = 0x0000006f; // jal x0, 0
369
370    // TODO: Evaluate how low-level do we want tests to be
371    #[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}