ethrex_levm/opcode_handlers/push.rs
1//! # Stack push operations
2//!
3//! Includes the following opcodes:
4//! - `PUSH0`
5//! - `PUSH1` to `PUSH32`
6
7use crate::{
8 errors::{InternalError, OpcodeResult, VMError},
9 gas_cost,
10 opcode_handlers::OpcodeHandler,
11 vm::VM,
12};
13use ethrex_common::{types::BYTECODE_PADDING, utils::u256_from_big_endian_const};
14
15/// Implementation for the `PUSH0` opcode.
16pub struct OpPush0Handler;
17impl OpcodeHandler for OpPush0Handler {
18 #[inline(always)]
19 fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
20 vm.current_call_frame
21 .increase_consumed_gas(gas_cost::PUSH0)?;
22
23 vm.current_call_frame.stack.push_zero()?;
24
25 Ok(OpcodeResult::Continue)
26 }
27}
28
29/// Implementation for the `PUSHn` opcode.
30pub struct OpPushHandler<const N: usize>;
31impl<const N: usize> OpcodeHandler for OpPushHandler<N> {
32 #[inline(always)]
33 fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
34 // PUSHn reads up to 32 immediate bytes without a bounds check, relying on
35 // the trailing zero padding appended to every bytecode. After the read the
36 // dispatch loop fetches the next opcode at `pc + N`, which needs one byte
37 // beyond the immediates, so the padding must exceed 32 (i.e. be >= 33).
38 // Keep the unchecked read below sound if the padding ever shrinks.
39 const { assert!(BYTECODE_PADDING > 32) };
40
41 let literal_offset = vm.current_call_frame.pc;
42 // Use a *checked* add for the pc advance, not unchecked `+= N`. Both
43 // compute the same value (pc never overflows in practice), but the
44 // checked form is required for good codegen here: with unchecked/wrapping
45 // arithmetic LLVM can no longer prove the immediate slice length is the
46 // constant `N`, so the `get_unchecked` read below degrades to a
47 // runtime-length memcpy and the PUSH hot loop runs ~2x slower (IPC
48 // collapses 3.4 -> 1.2). The overflow branch is free (perfectly
49 // predicted) and never taken.
50 vm.current_call_frame.pc = literal_offset
51 .checked_add(N)
52 .ok_or(InternalError::Overflow)?;
53 // `pc` is now exactly `literal_offset + N`; reuse it as the immediate end.
54 let literal_end = vm.current_call_frame.pc;
55
56 vm.current_call_frame
57 .increase_consumed_gas(gas_cost::PUSHN)?;
58
59 let bytecode = vm.current_call_frame.bytecode.dispatch_buf();
60 // SAFETY: PUSH only dispatches on a real opcode byte, so
61 // `literal_offset <= bytecode_len`; the buffer is padded with
62 // BYTECODE_PADDING (>= N) trailing zeros, so N bytes are always in bounds.
63 // Immediate bytes past the real code end read as zero, matching EVM
64 // PUSH-past-end semantics (the padding is zeroed).
65 let mut buf = [0u8; N];
66 #[expect(unsafe_code, reason = "read bounded by padded bytecode len")]
67 buf.copy_from_slice(unsafe { bytecode.get_unchecked(literal_offset..literal_end) });
68 let value = u256_from_big_endian_const(buf);
69 vm.current_call_frame.stack.push(value)?;
70
71 Ok(OpcodeResult::Continue)
72 }
73}