Skip to main content

revmc_codegen/compiler/
mod.rs

1//! EVM bytecode compiler implementation.
2
3use crate::{
4    Backend, Builder, Bytecode, EvmCompilerFn, EvmContext, EvmStack, FxHashMap, GasParams, Result,
5    bytecode::AnalysisConfig,
6};
7use revm_primitives::{Bytes, hardfork::SpecId};
8use revmc_backend::{
9    Attribute, BackendConfig, FunctionAttributeLocation, Linkage, OptimizationLevel, eyre::ensure,
10    format_bytes,
11};
12use revmc_builtins::Builtins;
13use revmc_context::RawEvmCompilerFn;
14use std::{
15    cell::Cell,
16    fs,
17    io::{self, Write},
18    mem,
19    path::{Path, PathBuf},
20    time::{Duration, Instant},
21};
22
23mod translate;
24use translate::{FcxConfig, FunctionCx};
25
26/// Collected timing remarks for the compiler dump.
27#[derive(Default)]
28struct Remarks {
29    parse: Cell<Duration>,
30    translate: Cell<Duration>,
31    verify: Cell<Duration>,
32    optimize: Cell<Duration>,
33    codegen: Cell<Duration>,
34    finalize_total: Cell<Duration>,
35}
36
37/// Per-phase timing breakdown from a compilation.
38#[derive(Clone, Copy, Debug, Default)]
39pub struct CompileTimings {
40    /// Time spent parsing and analyzing EVM bytecode.
41    pub parse: Duration,
42    /// Time spent translating analyzed bytecode to IR.
43    pub translate: Duration,
44    /// Time spent running optimization passes.
45    pub optimize: Duration,
46    /// Time spent emitting machine code (JIT lookup or object write).
47    pub codegen: Duration,
48}
49
50impl Remarks {
51    fn clear(&mut self) {
52        *self = Self::default();
53    }
54
55    fn time(&self, field: impl FnOnce(&Self) -> &Cell<Duration>) -> TimingGuard<'_> {
56        TimingGuard { target: field(self), start: Instant::now() }
57    }
58}
59
60struct TimingGuard<'a> {
61    target: &'a Cell<Duration>,
62    start: Instant,
63}
64
65impl Drop for TimingGuard<'_> {
66    fn drop(&mut self) {
67        self.target.set(self.target.get() + self.start.elapsed());
68    }
69}
70
71/// EVM bytecode compiler.
72///
73/// This currently represents one single-threaded IR context and module, which can be used to
74/// compile multiple functions as JIT or AOT.
75///
76/// Functions can be incrementally added with [`translate`], and then either written to an object
77/// file with [`write_object`] when in AOT mode, or JIT-compiled with [`jit_function`].
78///
79/// Performing either of these operations finalizes the module, and no more functions can be added
80/// afterwards until [`clear`] is called, which will reset the module to its initial state.
81///
82/// [`translate`]: EvmCompiler::translate
83/// [`write_object`]: EvmCompiler::write_object
84/// [`jit_function`]: EvmCompiler::jit_function
85/// [`clear`]: EvmCompiler::clear
86#[derive(derive_more::Debug)]
87pub struct EvmCompiler<B: Backend> {
88    name: Option<String>,
89    #[debug(skip)]
90    backend: B,
91    out_dir: Option<PathBuf>,
92    config: FcxConfig,
93    #[debug(skip)]
94    builtins: Builtins<B>,
95    #[debug(skip)]
96    gas_params: Option<GasParams>,
97
98    dedup: bool,
99    dse: bool,
100
101    dump_assembly: bool,
102    dump_unopt_assembly: bool,
103
104    compiler_gas_limit: u64,
105
106    #[debug(skip)]
107    remarks: Remarks,
108    /// Function sizes accumulated across all `jit_function`/`write_object` calls
109    /// in the current finalize cycle. Reset by `clear` / `clear_ir`.
110    compiled_sizes: Vec<(String, usize)>,
111    finalized: bool,
112}
113
114#[cfg(feature = "llvm")]
115impl EvmCompiler<revmc_llvm::EvmLlvmBackend> {
116    /// Creates a new instance of the compiler with the LLVM backend.
117    pub fn new_llvm(aot: bool) -> Result<Self> {
118        revmc_llvm::EvmLlvmBackend::new(aot).map(Self::new)
119    }
120}
121
122impl<B: Backend> EvmCompiler<B> {
123    /// Creates a new instance of the compiler with the given backend.
124    pub fn new(backend: B) -> Self {
125        Self {
126            name: None,
127            backend,
128            out_dir: None,
129            config: FcxConfig::default(),
130            builtins: Builtins::new(),
131            gas_params: None,
132            dedup: true,
133            dse: true,
134            dump_assembly: true,
135            dump_unopt_assembly: false,
136            compiler_gas_limit: crate::bytecode::DEFAULT_COMPILER_GAS_LIMIT,
137            remarks: Remarks::default(),
138            compiled_sizes: Vec::new(),
139            finalized: false,
140        }
141    }
142
143    /// Sets the name of the module.
144    pub fn set_module_name(&mut self, name: impl Into<String>) {
145        let name = name.into();
146        self.backend.set_module_name(&name);
147        self.name = Some(name);
148    }
149
150    fn is_aot(&self) -> bool {
151        self.backend.is_aot()
152    }
153
154    fn is_jit(&self) -> bool {
155        !self.is_aot()
156    }
157
158    /// Returns a reference to the underlying compiler backend.
159    #[doc(hidden)]
160    #[inline]
161    pub fn backend(&self) -> &B {
162        &self.backend
163    }
164
165    /// Returns a mutable reference to the underlying compiler backend.
166    #[doc(hidden)]
167    #[inline]
168    pub fn backend_mut(&mut self) -> &mut B {
169        &mut self.backend
170    }
171
172    /// Returns the per-phase timing breakdown from the last compilation and resets the counters.
173    pub fn take_timings(&self) -> CompileTimings {
174        let r = &self.remarks;
175        let timings = CompileTimings {
176            parse: r.parse.get(),
177            translate: r.translate.get(),
178            optimize: r.optimize.get(),
179            codegen: r.codegen.get(),
180        };
181        r.parse.set(Duration::ZERO);
182        r.translate.set(Duration::ZERO);
183        r.optimize.set(Duration::ZERO);
184        r.codegen.set(Duration::ZERO);
185        timings
186    }
187
188    /// Clones the current backend config, applies `f`, and sends the updated snapshot.
189    fn update_backend_config(&mut self, f: impl FnOnce(&mut BackendConfig)) {
190        let mut config = self.backend.config().clone();
191        f(&mut config);
192        self.backend.apply_config(config);
193    }
194
195    /// Returns the output directory.
196    pub fn out_dir(&self) -> Option<&Path> {
197        self.out_dir.as_deref()
198    }
199
200    /// Dumps intermediate outputs and other debug info to the given directory after compilation.
201    ///
202    /// Disables dumping if `output_dir` is `None`.
203    pub fn set_dump_to(&mut self, output_dir: Option<PathBuf>) {
204        self.update_backend_config(|c| c.is_dumping = output_dir.is_some());
205        self.config.comments = output_dir.is_some();
206        self.config.debug = output_dir.is_some();
207        if output_dir.is_some() {
208            self.config.frame_pointers = true;
209        }
210        self.out_dir = output_dir;
211    }
212
213    /// Dumps assembly to the output directory.
214    ///
215    /// This can be quite slow.
216    ///
217    /// Defaults to `true`.
218    pub fn dump_assembly(&mut self, yes: bool) {
219        self.dump_assembly = yes;
220    }
221
222    /// Dumps the unoptimized assembly to the output directory.
223    ///
224    /// This can be quite slow.
225    ///
226    /// Defaults to `false`.
227    pub fn dump_unopt_assembly(&mut self, yes: bool) {
228        self.dump_unopt_assembly = yes;
229    }
230
231    /// Returns the optimization level.
232    pub fn opt_level(&self) -> OptimizationLevel {
233        self.backend.config().opt_level
234    }
235
236    /// Sets the optimization level.
237    ///
238    /// Note that some backends may not support setting the optimization level after initialization.
239    ///
240    /// Defaults to the backend's initial optimization level.
241    pub fn set_opt_level(&mut self, level: OptimizationLevel) {
242        self.update_backend_config(|c| c.opt_level = level);
243    }
244
245    /// Sets whether to enable debug assertions.
246    ///
247    /// These are useful for debugging, but they do a moderate performance penalty due to the
248    /// insertion of extra checks and removal of certain assumptions.
249    ///
250    /// Defaults to `cfg!(debug_assertions)`.
251    pub fn debug_assertions(&mut self, yes: bool) {
252        self.update_backend_config(|c| c.debug_assertions = yes);
253        self.config.debug_assertions = yes;
254    }
255
256    /// Enables the block deduplication pass.
257    ///
258    /// Defaults to `true`.
259    pub fn set_dedup(&mut self, yes: bool) {
260        self.dedup = yes;
261    }
262
263    /// Enables the dead store elimination pass.
264    ///
265    /// Defaults to `true`.
266    pub fn set_dse(&mut self, yes: bool) {
267        self.dse = yes;
268    }
269
270    /// Returns whether JIT debug support is enabled.
271    ///
272    /// Registers JIT objects with debuggers via `__jit_debug_register_code`,
273    /// allowing GDB/LLDB to resolve JIT-compiled function names and set breakpoints.
274    ///
275    /// This setting is applied once per process on first JIT compilation.
276    /// Subsequent compilers inherit the value set by the first.
277    ///
278    /// Defaults to `true`.
279    pub fn debug_support(&self) -> bool {
280        self.backend.config().debug_support
281    }
282
283    /// Sets whether to enable JIT debug support.
284    ///
285    /// This setting is applied once per process on first JIT compilation.
286    /// Subsequent compilers inherit the value set by the first.
287    pub fn set_debug_support(&mut self, yes: bool) {
288        self.update_backend_config(|c| c.debug_support = yes);
289    }
290
291    /// Returns whether JIT profiling support is enabled.
292    ///
293    /// Installs the LLVM `PerfSupportPlugin` which writes jitdump records,
294    /// allowing profilers to resolve JIT-compiled symbols with debug and unwind info.
295    ///
296    /// This setting is applied once per process on first JIT compilation.
297    /// Subsequent compilers inherit the value set by the first.
298    ///
299    /// Defaults to `false`.
300    pub fn profiling_support(&self) -> bool {
301        self.backend.config().profiling_support
302    }
303
304    /// Sets whether to enable JIT profiling support.
305    ///
306    /// This setting is applied once per process on first JIT compilation.
307    /// Subsequent compilers inherit the value set by the first.
308    pub fn set_profiling_support(&mut self, yes: bool) {
309        self.update_backend_config(|c| c.profiling_support = yes);
310    }
311
312    /// Returns whether the simple perf map plugin is enabled.
313    ///
314    /// Writes `/tmp/perf-<pid>.map` in the perf map format so that profilers
315    /// can resolve JIT-compiled symbols without the jitdump machinery.
316    ///
317    /// Not suitable for long-running programs. The map file is append-only
318    /// and never cleaned up, so entries for freed JIT code accumulate
319    /// indefinitely. Prefer [`set_profiling_support`](Self::set_profiling_support)
320    /// (jitdump) for long-lived processes.
321    ///
322    /// This setting is applied once per process on first JIT compilation.
323    /// Subsequent compilers inherit the value set by the first.
324    ///
325    /// Defaults to `true`.
326    pub fn simple_perf(&self) -> bool {
327        self.backend.config().simple_perf
328    }
329
330    /// Sets whether to enable the simple perf map plugin.
331    ///
332    /// This setting is applied once per process on first JIT compilation.
333    /// Subsequent compilers inherit the value set by the first.
334    pub fn set_simple_perf(&mut self, yes: bool) {
335        self.update_backend_config(|c| c.simple_perf = yes);
336    }
337
338    /// Sets whether to enable debug info emission.
339    ///
340    /// Debug info embeds DWARF metadata and annotates IR/assembly dumps with source locations
341    /// from the parsed bytecode.
342    ///
343    /// Automatically enabled by [`set_dump_to`](Self::set_dump_to).
344    ///
345    /// Defaults to `false`.
346    pub fn set_debug_info(&mut self, yes: bool) {
347        self.config.debug = yes;
348    }
349
350    /// Sets whether to enable frame pointers.
351    ///
352    /// This is useful for profiling and debugging, but it incurs a very slight performance penalty.
353    ///
354    /// Enabled by default in debug builds, when `-Cforce-frame-pointers` is set, or when
355    /// [`set_dump_to`](Self::set_dump_to) is called with a directory.
356    pub fn frame_pointers(&mut self, yes: bool) {
357        self.config.frame_pointers = yes;
358    }
359
360    /// Sets whether to treat the stack as observable outside the function.
361    ///
362    /// If this is set to `true`, the stack length must be passed in the arguments.
363    ///
364    /// This is useful to inspect the stack after the function has been executed, but it does
365    /// incur a performance penalty as the stack will be stored at all return sites.
366    ///
367    /// Defaults to `false`.
368    pub fn inspect_stack(&mut self, yes: bool) {
369        self.config.inspect_stack = yes;
370    }
371
372    /// Sets whether to enable stack bound checks.
373    ///
374    /// Defaults to `true`.
375    ///
376    /// # Safety
377    ///
378    /// Removing stack length checks may improve compilation speed and performance, but will result
379    /// in **undefined behavior** if the stack length overflows at runtime, rather than a
380    /// [`StackUnderflow`]/[`StackOverflow`] result.
381    ///
382    /// [`StackUnderflow`]: revm_interpreter::InstructionResult::StackUnderflow
383    /// [`StackOverflow`]: revm_interpreter::InstructionResult::StackOverflow
384    pub unsafe fn stack_bound_checks(&mut self, yes: bool) {
385        self.config.stack_bound_checks = yes;
386    }
387
388    /// Sets the gas budget for compile-time evaluation of user-supplied bytecode.
389    ///
390    /// The compiler evaluates EVM operations at compile time during analysis passes. Without a
391    /// budget, adversarial bytecode (e.g. many `EXP`) can make compilation
392    /// arbitrarily slow. This limit uses the EVM gas schedule to bound work.
393    ///
394    /// When the budget is exhausted, further evaluation is skipped and values remain dynamic.
395    ///
396    /// Defaults to 100k gas. Set to `0` to disable compile-time evaluation entirely, or
397    /// `u64::MAX` to disable the limit.
398    pub fn set_compiler_gas_limit(&mut self, limit: u64) {
399        self.compiler_gas_limit = limit;
400    }
401
402    /// Sets whether to track gas costs.
403    ///
404    /// Disabling this will greatly improves compilation speed and performance, at the cost of not
405    /// being able to check for gas exhaustion.
406    ///
407    /// Note that this does not disable gas usage in certain instructions, mainly the ones that
408    /// are implemented as builtins.
409    ///
410    /// Use with care, as executing a function with gas disabled may result in an infinite loop.
411    ///
412    /// Defaults to `true`.
413    pub fn gas_metering(&mut self, yes: bool) {
414        self.config.gas_metering = yes;
415    }
416
417    /// Sets whether to collapse every JIT failure path to a single
418    /// [`OutOfGas`](revm_interpreter::InstructionResult::OutOfGas) constant.
419    ///
420    /// Failures (stack under/overflow, invalid jump, real OOG, invalid opcode, etc.) are
421    /// semantically interchangeable for callers that only branch on success vs failure, so
422    /// this lets LLVM DCE the per-failure-site materialization and the failure-block phi.
423    /// Successful exits (`STOP`/`RETURN`/`REVERT`) keep their original codes.
424    ///
425    /// Useful for benchmarking the cost of failure-result materialization.
426    ///
427    /// Defaults to `true`.
428    pub fn single_error(&mut self, yes: bool) {
429        self.config.single_error = yes;
430    }
431
432    /// Sets custom gas parameters.
433    ///
434    /// Overrides the default gas schedule derived from the spec_id.
435    /// Useful for custom chains or hardforks with non-standard gas costs.
436    ///
437    /// Defaults to `GasParams::new_spec(spec_id)`.
438    pub fn set_gas_params(&mut self, gas_params: GasParams) {
439        self.gas_params = Some(gas_params);
440    }
441
442    /// Translates the given EVM bytecode into an internal function.
443    ///
444    /// NOTE: `name` must be unique for each function, as it is used as the name of the final
445    /// symbol.
446    pub fn translate<'a>(
447        &mut self,
448        name: &str,
449        input: impl Into<EvmCompilerInput<'a>>,
450        spec_id: SpecId,
451    ) -> Result<B::FuncId> {
452        ensure!(!self.finalized, "cannot compile more functions after finalizing the module");
453        let bytecode = self.parse(input.into(), spec_id)?;
454        self.translate_inner(name, &bytecode)
455    }
456
457    /// (JIT) Compiles the given EVM bytecode into a JIT function.
458    ///
459    /// See [`translate`](Self::translate) for more information.
460    ///
461    /// # Safety
462    ///
463    /// The returned function pointer is owned by the module, and must not be called after the
464    /// module is cleared or the function is freed.
465    pub unsafe fn jit<'a>(
466        &mut self,
467        name: &str,
468        bytecode: impl Into<EvmCompilerInput<'a>>,
469        spec_id: SpecId,
470    ) -> Result<EvmCompilerFn> {
471        let id = self.translate(name, bytecode.into(), spec_id)?;
472        unsafe { self.jit_function(id) }
473    }
474
475    /// (JIT) Finalizes the module and JITs the given function.
476    ///
477    /// # Safety
478    ///
479    /// The returned function pointer is owned by the module, and must not be called after the
480    /// module is cleared or the function is freed.
481    pub unsafe fn jit_function(&mut self, id: B::FuncId) -> Result<EvmCompilerFn> {
482        ensure!(self.is_jit(), "cannot JIT functions during AOT compilation");
483        self.finalize()?;
484        let addr = {
485            let _t = self.remarks.time(|r| &r.codegen);
486            self.backend.jit_function(id)?
487        };
488        debug_assert!(addr != 0);
489        self.record_compiled_sizes();
490        if let Some(dump_dir) = &self.dump_dir() {
491            self.dump_remarks(dump_dir)?;
492        }
493        Ok(EvmCompilerFn::new(unsafe { std::mem::transmute::<usize, RawEvmCompilerFn>(addr) }))
494    }
495
496    /// (AOT) Writes the compiled object to the given file.
497    pub fn write_object_to_file(&mut self, path: &Path) -> Result<()> {
498        let file = fs::File::create(path)?;
499        let mut writer = io::BufWriter::new(file);
500        self.write_object(&mut writer)?;
501        writer.flush()?;
502        Ok(())
503    }
504
505    /// (AOT) Finalizes the module and writes the compiled object to the given writer.
506    pub fn write_object<W: io::Write>(&mut self, w: W) -> Result<()> {
507        ensure!(self.is_aot(), "cannot write AOT object during JIT compilation");
508        self.finalize()?;
509        {
510            let _t = self.remarks.time(|r| &r.codegen);
511            self.backend.write_object(w)?;
512        }
513        if let Some(dump_dir) = &self.dump_dir() {
514            self.dump_remarks(dump_dir)?;
515        }
516        Ok(())
517    }
518
519    /// (JIT) Frees the memory associated with a single function.
520    ///
521    /// Note that this will not reset the state of the internal module even if all functions are
522    /// freed with this function. Use [`clear`] to reset the module.
523    ///
524    /// [`clear`]: EvmCompiler::clear
525    ///
526    /// # Safety
527    ///
528    /// Because this function invalidates any pointers retrieved from the corresponding module, it
529    /// should only be used when none of the functions from that module are currently executing and
530    /// none of the `fn` pointers are called afterwards.
531    pub unsafe fn free_function(&mut self, id: B::FuncId) -> Result<()> {
532        unsafe { self.backend.free_function(id) }
533    }
534
535    /// Clears the IR module, freeing memory used by IR representations.
536    ///
537    /// This does **not** free JIT-compiled machine code, so previously obtained function pointers
538    /// remain valid. The module is left in a state where new functions can be translated.
539    pub fn clear_ir(&mut self) -> Result<()> {
540        self.builtins.clear();
541        self.remarks.clear();
542        self.compiled_sizes.clear();
543        self.finalized = false;
544        self.backend.clear_ir()
545    }
546
547    /// Frees all functions and resets the state of the internal module, allowing for new functions
548    /// to be compiled.
549    ///
550    /// # Safety
551    ///
552    /// Because this function invalidates any pointers retrieved from the corresponding module, it
553    /// should only be used when none of the functions from that module are currently executing and
554    /// none of the `fn` pointers are called afterwards.
555    pub unsafe fn clear(&mut self) -> Result<()> {
556        self.builtins.clear();
557        self.remarks.clear();
558        self.compiled_sizes.clear();
559        self.finalized = false;
560        unsafe { self.backend.free_all_functions() }
561    }
562
563    /// Parses the given EVM bytecode. Not public API.
564    #[doc(hidden)] // Not public API.
565    pub fn parse<'a>(
566        &mut self,
567        input: EvmCompilerInput<'a>,
568        spec_id: SpecId,
569    ) -> Result<Bytecode<'a>> {
570        let _t = self.remarks.time(|r| &r.parse);
571        let EvmCompilerInput::Code(bytecode) = input;
572
573        let mut bytecode = Bytecode::new(bytecode, spec_id, self.gas_params.clone());
574        bytecode.compiler_gas_limit = self.compiler_gas_limit;
575        bytecode.config.set(AnalysisConfig::INSPECT_STACK, self.config.inspect_stack);
576        bytecode.config.set(AnalysisConfig::DEDUP, self.dedup);
577        bytecode.config.set(AnalysisConfig::DSE, self.dse);
578        bytecode.analyze()?;
579        if let Some(dump_dir) = &self.dump_dir() {
580            Self::dump_bytecode(dump_dir, &bytecode)?;
581        }
582        Ok(bytecode)
583    }
584
585    #[instrument(name = "translate", level = "debug", skip_all)]
586    #[doc(hidden)] // Not public API.
587    pub fn translate_inner(&mut self, name: &str, bytecode: &Bytecode<'_>) -> Result<B::FuncId> {
588        ensure!(cfg!(target_endian = "little"), "only little-endian is supported");
589        let _t = self.remarks.time(|r| &r.translate);
590        ensure!(self.backend.function_name_is_unique(name), "function name `{name}` is not unique");
591
592        // Use bytecode.txt as the debug info source file.
593        if self.config.debug
594            && let Some(dump_dir) = &self.dump_dir()
595        {
596            let path = dump_dir.join("bytecode.txt");
597            let mut config = self.backend.config().clone();
598            config.debug_file = Some(path);
599            self.backend.apply_config(config);
600        }
601
602        let linkage = Linkage::Public;
603        let (bcx, id) =
604            Self::make_builder(&mut self.backend, &self.config, bytecode, name, linkage)?;
605        FunctionCx::translate(bcx, self.config, &mut self.builtins, bytecode)?;
606        Ok(id)
607    }
608
609    #[instrument(level = "debug", skip_all)]
610    fn finalize(&mut self) -> Result<()> {
611        if self.finalized {
612            return Ok(());
613        }
614
615        let finalize_start = Instant::now();
616
617        // Finalize debug info before any verification or code generation.
618        self.backend.finalize_debug_info()?;
619
620        let dump_dir = self.dump_dir();
621
622        if let Some(dump_dir) = &dump_dir {
623            let path = dump_dir.join("unopt").with_extension(self.backend.ir_extension());
624            self.dump_ir(&path)?;
625
626            // Dump IR before verifying for better debugging.
627            self.verify_module()?;
628
629            if self.dump_assembly && self.dump_unopt_assembly {
630                let path = dump_dir.join("unopt.s");
631                self.dump_disasm(&path)?;
632                if self.config.debug {
633                    let src_path = dump_dir.join("bytecode.txt");
634                    if src_path.exists() {
635                        Self::annotate_asm(&path, &src_path)?;
636                    }
637                }
638            }
639        } else {
640            self.verify_module()?;
641        }
642
643        self.optimize_module()?;
644
645        if let Some(dump_dir) = &dump_dir {
646            let path = dump_dir.join("opt").with_extension(self.backend.ir_extension());
647            self.dump_ir(&path)?;
648
649            if self.dump_assembly {
650                let path = dump_dir.join("opt.s");
651                self.dump_disasm(&path)?;
652                if self.config.debug {
653                    let src_path = dump_dir.join("bytecode.txt");
654                    if src_path.exists() {
655                        Self::annotate_asm(&path, &src_path)?;
656                    }
657                }
658            }
659        }
660        self.finalized = true;
661
662        let finalize_total = &self.remarks.finalize_total;
663        finalize_total.set(finalize_total.get() + finalize_start.elapsed());
664
665        Ok(())
666    }
667
668    #[instrument(level = "debug", skip_all)]
669    fn make_builder<'a>(
670        backend: &'a mut B,
671        config: &FcxConfig,
672        bytecode: &Bytecode<'_>,
673        name: &str,
674        linkage: Linkage,
675    ) -> Result<(B::Builder<'a>, B::FuncId)> {
676        fn size_align<T>(i: usize) -> (usize, usize, usize) {
677            (i, mem::size_of::<T>(), mem::align_of::<T>())
678        }
679
680        let i8 = backend.type_int(8);
681        let ptr = backend.type_ptr();
682        let (ret, params, param_names, ptr_attrs) = (
683            Some(i8),
684            &[ptr, ptr, ptr],
685            &["arg.ecx.addr", "arg.stack.addr", "arg.stack_len.addr"],
686            &[size_align::<EvmContext<'_>>(0), size_align::<EvmStack>(1), size_align::<usize>(2)],
687        );
688        debug_assert_eq!(params.len(), param_names.len());
689        let (mut bcx, id) = backend.build_function(name, ret, params, param_names, linkage)?;
690
691        // Function attributes.
692        let function_attributes = default_attrs::for_fn()
693            .chain(config.frame_pointers.then_some(Attribute::AllFramePointers));
694        for attr in function_attributes {
695            bcx.add_function_attribute(None, attr, FunctionAttributeLocation::Function);
696        }
697
698        // Pointer argument attributes.
699        for &(i, size, align) in ptr_attrs {
700            let attrs = default_attrs::for_sized_ref((size, align));
701            for attr in attrs {
702                let loc = FunctionAttributeLocation::Param(i as _);
703                bcx.add_function_attribute(None, attr, loc);
704            }
705        }
706
707        // The stack argument's contents are dead after return when the stack is not observed,
708        // so the caller can elide any stores to the buffer.
709        if !bytecode.stack_observed() {
710            for param in 1..=2 {
711                bcx.add_function_attribute(
712                    None,
713                    Attribute::DeadOnReturn,
714                    FunctionAttributeLocation::Param(param),
715                );
716            }
717        }
718
719        Ok((bcx, id))
720    }
721
722    #[instrument(level = "debug", skip_all)]
723    fn dump_ir(&mut self, path: &Path) -> Result<()> {
724        self.backend.dump_ir(path)?;
725        if self.config.debug
726            && let Some(dump_dir) = &self.dump_dir()
727        {
728            let src_path = dump_dir.join("bytecode.txt");
729            if src_path.exists() {
730                Self::annotate_ir(path, &src_path)?;
731            }
732        }
733        Ok(())
734    }
735
736    #[instrument(level = "debug", skip_all)]
737    fn dump_disasm(&mut self, path: &Path) -> Result<()> {
738        self.backend.dump_disasm(path)
739    }
740
741    #[instrument(level = "debug", skip_all)]
742    fn verify_module(&mut self) -> Result<()> {
743        if !self.config.debug_assertions {
744            return Ok(());
745        }
746        let _t = self.remarks.time(|r| &r.verify);
747        self.backend.verify_module()
748    }
749
750    #[instrument(level = "debug", skip_all)]
751    fn optimize_module(&mut self) -> Result<()> {
752        let _t = self.remarks.time(|r| &r.optimize);
753        self.backend.optimize_module()
754    }
755
756    fn dump_remarks(&self, dump_dir: &Path) -> Result<()> {
757        let r = &self.remarks;
758        let parse = r.parse.get();
759        let translate = r.translate.get();
760        let finalize = r.finalize_total.get();
761        let verify = r.verify.get();
762        let optimize = r.optimize.get();
763        let codegen = r.codegen.get();
764        let total = parse + translate + finalize + codegen;
765        let file = fs::File::create(dump_dir.join("remarks.txt"))?;
766        let mut w = io::BufWriter::new(file);
767        write!(
768            w,
769            "\
770Compilation remarks
771===================
772
773parse:      {parse:>11.3?}
774translate:  {translate:>11.3?}
775finalize:   {finalize:>11.3?}
776- verify:   {verify:>11.3?}
777- optimize: {optimize:>11.3?}
778codegen:    {codegen:>11.3?}
779
780total:      {total:>11.3?}
781"
782        )?;
783
784        // Display sizes of generated files.
785        let mut files: Vec<_> = fs::read_dir(dump_dir)?
786            .filter_map(|e| e.ok())
787            .filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
788            .filter(|e| e.file_name() != "remarks.txt")
789            .collect();
790        if !files.is_empty() {
791            files.sort_by_key(|e| e.file_name());
792            writeln!(w)?;
793            writeln!(w, "Generated files")?;
794            writeln!(w, "===============")?;
795            for entry in &files {
796                let name = entry.file_name();
797                let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
798                writeln!(w, "{}: {}", name.to_string_lossy(), format_bytes(size as usize))?;
799            }
800        }
801
802        // Cumulative JIT code sizes (across all jit_function calls in this finalize cycle).
803        if !self.compiled_sizes.is_empty() {
804            let total: usize = self.compiled_sizes.iter().map(|(_, s)| *s).sum();
805            writeln!(w)?;
806            writeln!(w, "JIT code sizes (estimated)")?;
807            writeln!(w, "==========================")?;
808            for (name, size) in &self.compiled_sizes {
809                writeln!(w, "{name}: {}", format_bytes(*size))?;
810            }
811            if self.compiled_sizes.len() > 1 {
812                writeln!(w, "total: {}", format_bytes(total))?;
813            }
814        }
815
816        w.flush()?;
817        Ok(())
818    }
819
820    /// Pull the latest compiled function sizes from the backend and merge them into
821    /// `compiled_sizes`, deduplicating by name (later entries overwrite earlier ones).
822    fn record_compiled_sizes(&mut self) {
823        for (name, size) in self.backend.function_sizes() {
824            if let Some(slot) = self.compiled_sizes.iter_mut().find(|(n, _)| *n == name) {
825                slot.1 = size;
826            } else {
827                self.compiled_sizes.push((name, size));
828            }
829        }
830    }
831
832    #[instrument(level = "debug", skip_all)]
833    fn dump_bytecode(dump_dir: &Path, bytecode: &Bytecode<'_>) -> Result<()> {
834        {
835            let file = fs::File::create(dump_dir.join("bytecode.txt"))?;
836            let mut writer = io::BufWriter::new(file);
837            write!(writer, "{bytecode}")?;
838            writer.flush()?;
839        }
840
841        {
842            let file = fs::File::create(dump_dir.join("bytecode.dbg.txt"))?;
843            let mut writer = io::BufWriter::new(file);
844            writeln!(writer, "{bytecode:#?}")?;
845            writer.flush()?;
846        }
847
848        fs::write(dump_dir.join("bytecode.bin"), &*bytecode.code)?;
849
850        {
851            let file = fs::File::create(dump_dir.join("bytecode.dot"))?;
852            let mut writer = io::BufWriter::new(file);
853            let mut dot = String::new();
854            bytecode.write_dot(&mut dot).map_err(|e| revmc_backend::eyre::eyre!("{e}"))?;
855            writer.write_all(dot.as_bytes())?;
856            writer.flush()?;
857        }
858
859        Ok(())
860    }
861
862    /// Rewrites an IR dump file in-place, appending bytecode source lines as comments.
863    ///
864    /// For each IR instruction with `!dbg !N`, resolves the `!DILocation(line: L, ...)`
865    /// metadata and appends `; >> <source line>`.
866    fn annotate_ir(ir_path: &Path, src_path: &Path) -> Result<()> {
867        let src = fs::read_to_string(src_path)?;
868        let src_lines: Vec<&str> = src.lines().collect();
869        let ir = fs::read_to_string(ir_path)?;
870
871        // Parse `!N = !DILocation(line: L, ...)` metadata.
872        let mut di_locs = FxHashMap::default();
873        for line in ir.lines() {
874            let line = line.trim_start();
875            if !line.starts_with('!') {
876                continue;
877            }
878            // Match: !N = !DILocation(line: L, ...)
879            let Some(rest) = line.strip_prefix('!') else { continue };
880            let Some((id_str, rest)) = rest.split_once(" = !DILocation(line: ") else { continue };
881            let Ok(id) = id_str.parse::<u32>() else { continue };
882            let Some((line_str, _)) = rest.split_once(',') else { continue };
883            let Ok(line_no) = line_str.parse::<u32>() else { continue };
884            di_locs.insert(id, line_no);
885        }
886
887        if di_locs.is_empty() {
888            return Ok(());
889        }
890
891        // Collect lines and find max width for comment alignment.
892        let annotated: Vec<_> =
893            ir.lines().map(|line| (line, resolve_dbg_line(line, &di_locs, &src_lines))).collect();
894        let comment_col = annotated
895            .iter()
896            .filter_map(|(line, src)| src.is_some().then_some(line.len()))
897            .max()
898            .unwrap_or(0)
899            .min(100);
900
901        // Rewrite the file with aligned annotations.
902        let file = fs::File::create(ir_path)?;
903        let mut w = io::BufWriter::new(file);
904        for (line, src_line) in &annotated {
905            if let Some(src_line) = src_line {
906                writeln!(w, "{line:<comment_col$} ; >> {src_line}")?;
907            } else {
908                writeln!(w, "{line}")?;
909            }
910        }
911        w.flush()?;
912        Ok(())
913    }
914
915    /// Rewrites an assembly dump file in-place, replacing `# bytecode.txt:LL:CC` comments
916    /// with the corresponding source line from `bytecode.txt`.
917    fn annotate_asm(asm_path: &Path, src_path: &Path) -> Result<()> {
918        let src = fs::read_to_string(src_path)?;
919        let src_lines: Vec<&str> = src.lines().collect();
920        let asm = fs::read_to_string(asm_path)?;
921
922        let annotated: Vec<_> = asm
923            .lines()
924            .map(|line| {
925                let resolved = resolve_asm_source_line(line, &src_lines);
926                // Strip the original `# bytecode.txt:...` comment when we have a resolved line.
927                let stripped = if resolved.is_some() {
928                    line.find("# bytecode.txt:").map(|pos| line[..pos].trim_end()).unwrap_or(line)
929                } else {
930                    line
931                };
932                (stripped, resolved)
933            })
934            .collect();
935        let comment_col = 40;
936
937        let file = fs::File::create(asm_path)?;
938        let mut w = io::BufWriter::new(file);
939        for (line, src_line) in &annotated {
940            if let Some(src_line) = src_line {
941                writeln!(w, "{line:<comment_col$} # {src_line}")?;
942            } else {
943                writeln!(w, "{line}")?;
944            }
945        }
946        w.flush()?;
947        Ok(())
948    }
949
950    /// Returns the dump directory, if set.
951    #[doc(hidden)]
952    pub fn dump_dir(&self) -> Option<PathBuf> {
953        let mut dump_dir = self.out_dir.clone()?;
954        if let Some(name) = &self.name {
955            dump_dir.push(name.replace(char::is_whitespace, "_"));
956        }
957        if !dump_dir.exists() {
958            let _ = fs::create_dir_all(&dump_dir);
959        }
960        Some(dump_dir)
961    }
962}
963
964/// [`EvmCompiler`] input.
965#[allow(missing_debug_implementations)]
966pub enum EvmCompilerInput<'a> {
967    /// EVM bytecode.
968    Code(&'a [u8]),
969}
970
971impl<'a> From<&'a [u8]> for EvmCompilerInput<'a> {
972    fn from(code: &'a [u8]) -> Self {
973        EvmCompilerInput::Code(code)
974    }
975}
976
977impl<'a> From<&'a Vec<u8>> for EvmCompilerInput<'a> {
978    fn from(code: &'a Vec<u8>) -> Self {
979        EvmCompilerInput::Code(code)
980    }
981}
982
983impl<'a> From<&'a Bytes> for EvmCompilerInput<'a> {
984    fn from(code: &'a Bytes) -> Self {
985        EvmCompilerInput::Code(code)
986    }
987}
988
989#[allow(dead_code)]
990mod default_attrs {
991    use revmc_backend::Attribute;
992
993    pub(crate) fn for_fn() -> impl Iterator<Item = Attribute> {
994        [
995            Attribute::WillReturn,      // Always returns.
996            Attribute::NoSync,          // No thread synchronization.
997            Attribute::NativeTargetCpu, // Optimization.
998            Attribute::NoRecurse,       // Revm is not recursive.
999            Attribute::NonLazyBind,     // Skip PLT indirection.
1000            Attribute::UWTable,         // Unwind tables for profilers/debuggers.
1001        ]
1002        .into_iter()
1003    }
1004
1005    pub(crate) fn for_param() -> impl Iterator<Item = Attribute> {
1006        [Attribute::NoUndef].into_iter()
1007    }
1008
1009    pub(crate) fn for_ptr() -> impl Iterator<Item = Attribute> {
1010        for_param().chain([Attribute::NoCapture])
1011    }
1012
1013    pub(crate) fn for_sized_ptr((size, align): (usize, usize)) -> impl Iterator<Item = Attribute> {
1014        for_ptr().chain([Attribute::Dereferenceable(size as u64), Attribute::Align(align as u64)])
1015    }
1016
1017    pub(crate) fn for_ptr_t<T>() -> impl Iterator<Item = Attribute> {
1018        for_sized_ptr(size_align::<T>())
1019    }
1020
1021    pub(crate) fn for_ref() -> impl Iterator<Item = Attribute> {
1022        for_ptr().chain([Attribute::NonNull, Attribute::NoAlias])
1023    }
1024
1025    pub(crate) fn for_sized_ref((size, align): (usize, usize)) -> impl Iterator<Item = Attribute> {
1026        for_ref().chain([Attribute::Dereferenceable(size as u64), Attribute::Align(align as u64)])
1027    }
1028
1029    pub(crate) fn for_ref_t<T>() -> impl Iterator<Item = Attribute> {
1030        for_sized_ref(size_align::<T>())
1031    }
1032
1033    pub(crate) fn size_align<T>() -> (usize, usize) {
1034        (std::mem::size_of::<T>(), std::mem::align_of::<T>())
1035    }
1036}
1037
1038/// Resolves a `# bytecode.txt:LL:CC` or `# bytecode.txt:LL` comment in an assembly line
1039/// to the corresponding source line.
1040fn resolve_asm_source_line<'a>(line: &str, src_lines: &[&'a str]) -> Option<&'a str> {
1041    let pos = line.find("# bytecode.txt:")?;
1042    let after = &line[pos + "# bytecode.txt:".len()..];
1043    let num_len = after.find(|c: char| !c.is_ascii_digit()).unwrap_or(after.len());
1044    let line_no: u32 = after[..num_len].parse().ok()?;
1045    let idx = line_no.checked_sub(1)? as usize;
1046    let src_line = src_lines.get(idx)?.trim();
1047    (!src_line.is_empty()).then_some(src_line)
1048}
1049
1050/// Resolves a `!dbg !N` reference in an IR line to the corresponding source line.
1051fn resolve_dbg_line<'a>(
1052    ir_line: &str,
1053    di_locs: &FxHashMap<u32, u32>,
1054    src_lines: &[&'a str],
1055) -> Option<&'a str> {
1056    let pos = ir_line.find("!dbg !")?;
1057    let after = &ir_line[pos + 6..];
1058    let id_len = after.find(|c: char| !c.is_ascii_digit()).unwrap_or(after.len());
1059    let id: u32 = after[..id_len].parse().ok()?;
1060    let line_no = *di_locs.get(&id)?;
1061    let idx = line_no.checked_sub(1)? as usize;
1062    let src_line = src_lines.get(idx)?.trim();
1063    (!src_line.is_empty()).then_some(src_line)
1064}