Skip to main content

revmc_codegen/bytecode/passes/
sections.rs

1use crate::bytecode::{Bytecode, Inst, InstFlags};
2use core::fmt;
3use revm_bytecode::opcode as op;
4use revm_primitives::U256;
5
6/// A gas section tracks the total base gas cost of a sequence of instructions.
7///
8/// Gas sections end at instructions that require `gasleft` (e.g. `GAS`, `SSTORE`),
9/// branching instructions, or suspending instructions.
10#[derive(Clone, Copy, Default, PartialEq, Eq)]
11pub(crate) struct GasSection {
12    /// The total base gas cost of all instructions in the section.
13    pub(crate) gas_cost: u32,
14}
15
16impl fmt::Debug for GasSection {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        if self.is_empty() {
19            f.write_str("GasSection::EMPTY")
20        } else {
21            f.debug_struct("GasSection").field("gas_cost", &self.gas_cost).finish()
22        }
23    }
24}
25
26impl GasSection {
27    /// Returns `true` if the section is empty.
28    #[inline]
29    pub(crate) fn is_empty(self) -> bool {
30        self == Self::default()
31    }
32}
33
34/// A stack section tracks stack height requirements for a sequence of instructions.
35///
36/// Stack sections end at instructions that require `gasleft`, branching instructions, or
37/// suspending instructions. This keeps stack failures after dynamic-gas and host-effect
38/// instructions from being hoisted ahead of those instructions.
39#[derive(Clone, Copy, Default, PartialEq, Eq)]
40pub(crate) struct StackSection {
41    /// The stack height required to execute the section.
42    pub(crate) inputs: u16,
43    /// The maximum stack height growth relative to the stack height at section start.
44    pub(crate) max_growth: i16,
45}
46
47impl fmt::Debug for StackSection {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        if self.is_empty() {
50            f.write_str("StackSection::EMPTY")
51        } else {
52            f.debug_struct("StackSection")
53                .field("inputs", &self.inputs)
54                .field("max_growth", &self.max_growth)
55                .finish()
56        }
57    }
58}
59
60impl StackSection {
61    /// Returns `true` if the section is empty.
62    #[inline]
63    pub(crate) fn is_empty(self) -> bool {
64        self == Self::default()
65    }
66
67    /// Compute a stack section from an iterator of `(inputs, outputs)` pairs.
68    pub(crate) fn from_stack_io(iter: impl IntoIterator<Item = (u8, u8)>) -> Self {
69        let mut analysis = StackSectionAnalysis::default();
70        for (inp, out) in iter {
71            analysis.process(inp, out);
72        }
73        analysis.section()
74    }
75}
76
77/// Gas section analysis state.
78pub(crate) struct GasSectionAnalysis {
79    gas_cost: u64,
80    start_inst: Inst,
81}
82
83impl Default for GasSectionAnalysis {
84    fn default() -> Self {
85        Self { gas_cost: 0, start_inst: Inst::from_usize(0) }
86    }
87}
88
89impl GasSectionAnalysis {
90    /// Accumulates a base gas cost.
91    #[inline]
92    pub(crate) fn process(&mut self, base_gas: u16) {
93        self.gas_cost += base_gas as u64;
94    }
95
96    /// Accumulates extra gas (e.g. pre-computed dynamic gas for folded instructions).
97    #[inline]
98    pub(crate) fn process_extra(&mut self, gas: u64) {
99        self.gas_cost += gas;
100    }
101
102    fn save_to_reset(&mut self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
103        self.save_to(bytecode, next_section_inst);
104        self.reset(next_section_inst);
105    }
106
107    /// Saves the current gas section to the bytecode.
108    fn save_to(&self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
109        if self.start_inst >= bytecode.insts.len_idx() {
110            return;
111        }
112        let section = self.section();
113        if !section.is_empty() {
114            let _ = next_section_inst;
115            // trace!(
116            //     inst = %self.start_inst,
117            //     len = %(next_section_inst - self.start_inst).0,
118            //     ?section,
119            //     "saving gas"
120            // );
121            let mut insts = bytecode.insts[self.start_inst..].iter_mut();
122            if let Some(inst) = insts.find(|inst| !inst.is_dead_code()) {
123                inst.gas_section = section;
124            }
125        }
126    }
127
128    /// Resets the analysis for a new section.
129    pub(crate) fn reset(&mut self, inst: Inst) {
130        *self = Self { start_inst: inst, ..Default::default() };
131    }
132
133    /// Returns the current gas section.
134    pub(crate) fn section(&self) -> GasSection {
135        GasSection { gas_cost: self.gas_cost.try_into().unwrap_or(u32::MAX) }
136    }
137}
138
139/// Stack section analysis state.
140pub(crate) struct StackSectionAnalysis {
141    inputs: i32,
142    diff: i32,
143    max_growth: i32,
144    start_inst: Inst,
145}
146
147impl Default for StackSectionAnalysis {
148    fn default() -> Self {
149        Self { inputs: 0, diff: 0, max_growth: 0, start_inst: Inst::from_usize(0) }
150    }
151}
152
153impl StackSectionAnalysis {
154    /// Returns the cumulative stack delta so far.
155    #[inline]
156    pub(crate) fn diff(&self) -> i32 {
157        self.diff
158    }
159
160    /// Accumulates a single instruction's stack I/O.
161    #[inline]
162    pub(crate) fn process(&mut self, inputs: u8, outputs: u8) {
163        self.inputs = self.inputs.max(inputs as i32 - self.diff);
164        self.diff += outputs as i32 - inputs as i32;
165        self.max_growth = self.max_growth.max(self.diff);
166    }
167
168    fn save_to_reset(&mut self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
169        self.save_to(bytecode, next_section_inst);
170        self.reset(next_section_inst);
171    }
172
173    /// Saves the current stack section to the bytecode.
174    fn save_to(&self, bytecode: &mut Bytecode<'_>, next_section_inst: Inst) {
175        if self.start_inst >= bytecode.insts.len_idx() {
176            return;
177        }
178        let section = self.section();
179        let _ = next_section_inst;
180        // trace!(
181        //     inst = %self.start_inst,
182        //     len = %(next_section_inst - self.start_inst).0,
183        //     ?section,
184        //     "saving stack"
185        // );
186        let mut insts = bytecode.insts[self.start_inst..].iter_mut();
187        if let Some(inst) = insts.find(|inst| !inst.is_dead_code()) {
188            inst.flags |= InstFlags::STACK_SECTION_HEAD;
189            inst.stack_section = section;
190        }
191    }
192
193    /// Resets the analysis for a new section.
194    pub(crate) fn reset(&mut self, inst: Inst) {
195        *self = Self { start_inst: inst, ..Default::default() };
196    }
197
198    /// Returns the current stack section.
199    pub(crate) fn section(&self) -> StackSection {
200        StackSection {
201            inputs: self.inputs.try_into().unwrap_or(u16::MAX),
202            max_growth: self.max_growth.try_into().unwrap_or(i16::MAX),
203        }
204    }
205}
206
207/// Instruction section analysis, tracking gas and stack sections separately.
208#[derive(Default)]
209pub(crate) struct SectionsAnalysis {
210    gas: GasSectionAnalysis,
211    stack: StackSectionAnalysis,
212}
213
214impl SectionsAnalysis {
215    /// Process a single instruction.
216    pub(crate) fn process(&mut self, bytecode: &mut Bytecode<'_>, inst: Inst) {
217        // JUMPDEST starts both gas and stack sections.
218        if bytecode.inst(inst).is_reachable_jumpdest(bytecode.has_dynamic_jumps()) {
219            self.stack.save_to_reset(bytecode, inst);
220            self.gas.save_to_reset(bytecode, inst);
221        }
222
223        let data = bytecode.inst(inst);
224        let (inp, out) = data.stack_io();
225        self.stack.process(inp, out);
226        self.gas.process(data.base_gas);
227
228        // When EXP's builtin will be skipped — either because both operands are known
229        // (const_output) or because the exponent is known and handled by a peephole (≤ 2) —
230        // fold the dynamic gas into the section statically.
231        if data.opcode == op::EXP
232            && let Some(exponent) = bytecode.const_operand(inst, 1)
233            && (bytecode.const_output(inst).is_some() || exponent <= U256::from(2))
234        {
235            self.gas.process_extra(bytecode.gas_params.exp_cost(exponent));
236        }
237
238        // Dynamic-gas, branching, and suspending instructions end both sections.
239        let next = inst + 1;
240        if data.may_suspend() || data.is_branching() || data.requires_gasleft(bytecode.spec_id) {
241            self.stack.save_to_reset(bytecode, next);
242            self.gas.save_to_reset(bytecode, next);
243        }
244    }
245
246    /// Finishes the analysis.
247    pub(crate) fn finish(self, bytecode: &mut Bytecode<'_>) {
248        let last = bytecode.insts.len_idx() - 1;
249        self.gas.save_to(bytecode, last);
250        self.stack.save_to(bytecode, last);
251        if enabled!(tracing::Level::DEBUG) {
252            let mut max_len = 0usize;
253            let mut count = 0usize;
254            let mut section_len = 0usize;
255            for (_inst, data) in bytecode.iter_insts() {
256                section_len += 1;
257                if !data.is_stack_section_head() {
258                    continue;
259                }
260                // `section_len` includes this head; the previous section had `section_len - 1`.
261                if count > 0 {
262                    max_len = max_len.max(section_len - 1);
263                }
264                section_len = 1;
265                count += 1;
266            }
267            // Account for the last section.
268            max_len = max_len.max(section_len);
269            debug!(count, max_len, "sections");
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use crate::bytecode::{
277        Inst,
278        passes::block_analysis::tests::{analyze_asm, analyze_asm_spec, analyze_code_spec},
279    };
280    use revm_bytecode::opcode as op;
281    use revm_primitives::hardfork::SpecId;
282
283    /// Returns the gas section cost for the first non-dead instruction (the section head).
284    fn section_gas(src: &str) -> u32 {
285        let bytecode = analyze_asm(src);
286        bytecode.inst(Inst::from_usize(0)).gas_section.gas_cost
287    }
288
289    /// EXP with an asymmetric base/exponent: base is 1 byte (2), exponent is 2 bytes (256).
290    /// If the wrong operand were used for gas, the dynamic cost would be 50*1=50 instead of
291    /// 50*2=100, and the total would differ.
292    #[test]
293    fn exp_folded_gas_uses_exponent_not_base() {
294        // PUSH 256, PUSH 2, EXP, PUSH0, MSTORE, STOP
295        // Gas: PUSH(3) + PUSH(3) + EXP_base(10) + EXP_dynamic(50*2=100) + PUSH0(2) + MSTORE(3) +
296        // STOP(0)    = 3 + 3 + 10 + 100 + 2 + 3 + 0 = 121
297        let gas = section_gas("PUSH 256 PUSH 2 EXP PUSH0 MSTORE STOP");
298        assert_eq!(gas, 121, "dynamic gas must use exponent (256=2 bytes), not base (2=1 byte)");
299    }
300
301    /// EXP with zero exponent has no dynamic gas.
302    #[test]
303    fn exp_folded_gas_zero_exponent() {
304        // PUSH0(2) + PUSH(3) + EXP_base(10) + EXP_dynamic(0) + PUSH0(2) + MSTORE(3) + STOP(0) = 20
305        let gas = section_gas("PUSH 0 PUSH 2 EXP PUSH0 MSTORE STOP");
306        assert_eq!(gas, 20, "zero exponent should have no dynamic gas");
307    }
308
309    /// EXP with max exponent (32 bytes).
310    #[test]
311    fn exp_folded_gas_max_exponent() {
312        // PUSH U256::MAX (32 bytes), PUSH 2, EXP, PUSH0, MSTORE, STOP
313        // Gas: PUSH(3) + PUSH(3) + EXP_base(10) + EXP_dynamic(50*32=1600) + PUSH0(2) + MSTORE(3) +
314        // STOP(0)    = 3 + 3 + 10 + 1600 + 2 + 3 + 0 = 1621
315        let gas = section_gas(
316            "PUSH 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
317             PUSH 2 EXP PUSH0 MSTORE STOP",
318        );
319        assert_eq!(gas, 1621, "max exponent should cost 50*32=1600 dynamic gas");
320    }
321
322    /// Disabled opcodes must not contribute stack I/O or gas to the preceding section.
323    /// Otherwise the section-head underflow check fires before the disabled opcode's
324    /// `NotActivated` guard, producing a divergent `StackUnderflow`.
325    #[test]
326    fn disabled_opcode_does_not_poison_section() {
327        // CALLDATASIZE(0→1) ; TSTORE(2→0, disabled before Cancun)
328        let bytecode = analyze_asm_spec("CALLDATASIZE TSTORE", SpecId::SHANGHAI);
329        let head = bytecode.inst(Inst::from_usize(0));
330        assert_eq!(
331            head.stack_section.inputs, 0,
332            "CALLDATASIZE needs 0 inputs; disabled TSTORE must not inflate the section"
333        );
334        assert_eq!(head.gas_section.gas_cost, 2, "only CALLDATASIZE gas (2) should be charged");
335    }
336
337    /// DUPN with an invalid immediate must have stack_io = (0, 0) so that section analysis
338    /// matches translate (which emits `InvalidImmediateEncoding` and diverges).
339    /// Previously, the fallback to OPCODE_INFO's placeholder `(0, 1)` inflated the section's
340    /// cumulative diff, causing translate to underflow the virtual stack on a subsequent SSTORE.
341    #[test]
342    fn invalid_dupn_imm_does_not_inflate_section() {
343        // PUSH0, DUPN 0x5b (invalid immediate), PUSH0, SSTORE, STOP
344        // Without the fix, DUPN had stack_io=(0,1), inflating the section diff by 1.
345        // Translate would skip the +1 (goto_return!(fail)), then SSTORE's diff=-2 would
346        // underflow the virtual stack.
347        let bytecode = analyze_code_spec(
348            vec![op::PUSH0, op::DUPN, 0x5b, op::PUSH0, op::SSTORE, op::STOP],
349            SpecId::AMSTERDAM,
350        );
351        let dupn = bytecode.inst(Inst::from_usize(1));
352        assert_eq!(dupn.stack_io(), (0, 0), "DUPN with invalid immediate must have zero stack I/O");
353    }
354}