From af039e9a1696e37c5915f5d2d08d64ec256a9e1f Mon Sep 17 00:00:00 2001
From: jonny rhea <5555162+jrhea@users.noreply.github.com>
Date: Mon, 6 Jul 2026 18:04:43 -0500
Subject: [PATCH] core/vm, core/vm/gen: mimic rlpgen's pattern and apply more
cleanup
---
core/vm/gen/gen.go | 948 ++++++++++++++++++++++++++++++++
core/vm/gen/gen_test.go | 42 ++
core/vm/gen/main.go | 925 +------------------------------
core/vm/interpreter.go | 2 +-
core/vm/interpreter_gen.go | 4 +-
core/vm/interpreter_gen_test.go | 40 +-
6 files changed, 1005 insertions(+), 956 deletions(-)
create mode 100644 core/vm/gen/gen.go
create mode 100644 core/vm/gen/gen_test.go
diff --git a/core/vm/gen/gen.go b/core/vm/gen/gen.go
new file mode 100644
index 0000000000..297656669d
--- /dev/null
+++ b/core/vm/gen/gen.go
@@ -0,0 +1,948 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// inlineOps maps an opcode byte to the handler whose body is spliced inline
+// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
+// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
+// factory-built (one shared makePush / makeDup each), so their value is the
+// factory name and emitInlineOp splices the factory body with the per-opcode size.
+// Opcodes not listed here (or in directCallOps) fall through to the default case,
+// which dispatches via the per-fork table.
+var inlineOps = func() map[byte]string {
+ m := map[byte]string{
+ 0x01: "opAdd", 0x02: "opMul", 0x03: "opSub", 0x04: "opDiv", 0x05: "opSdiv",
+ 0x06: "opMod", 0x07: "opSmod", 0x08: "opAddmod", 0x09: "opMulmod", 0x0b: "opSignExtend",
+ 0x10: "opLt", 0x11: "opGt", 0x12: "opSlt", 0x13: "opSgt", 0x14: "opEq", 0x15: "opIszero",
+ 0x16: "opAnd", 0x17: "opOr", 0x18: "opXor", 0x19: "opNot", 0x1a: "opByte",
+ 0x1b: "opSHL", 0x1c: "opSHR", 0x1d: "opSAR", 0x1e: "opCLZ",
+ 0x50: "opPop", 0x56: "opJump", 0x57: "opJumpi", 0x58: "opPc", 0x59: "opMsize", 0x5b: "opJumpdest",
+ 0x5f: "opPush0", 0x60: "opPush1", 0x61: "opPush2",
+ }
+ for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
+ m[byte(code)] = "makePush"
+ }
+ for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
+ m[byte(code)] = "makeDup"
+ }
+ for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
+ m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
+ }
+ return m
+}()
+
+// directCallOps lists the opcodes (dynamic gas, not inlined) whose handler,
+// dynamic-gas, and memory-size functions are the same across every fork
+// (verified: untouched by any enableXxx). They are emitted as direct calls to
+// those functions by name instead of the indirect operation.* pointer calls
+// in the default case.
+var directCallOps = map[byte][3]string{
+ 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
+ 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
+ 0x52: {"opMstore", "gasMStore", "memoryMStore"},
+ 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
+}
+
+// opSpec holds the per-opcode constants the generator emits (gas, stack bounds, intro fork), derived from the per-fork tables.
+type opSpec struct {
+ defined bool
+ name string
+ fork string
+ constGas uint64
+ minStack int
+ maxStack int
+}
+
+type generator struct {
+ fset *token.FileSet
+ opcodeHandlers map[string]*ast.FuncDecl
+ stackHelpers map[string]*ast.FuncDecl
+ gasHelpers map[string]*ast.FuncDecl
+ specs [256]opSpec
+ buf *bytes.Buffer
+}
+
+// p is the writer of the generated file. Every line of output is appended
+// to g.buf through it.
+func (g *generator) p(format string, args ...any) {
+ format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
+ fmt.Fprintf(g.buf, format, args...)
+}
+
+// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
+// interpreter.go. It returns the top-level opXxx handlers by name, the
+// //gen:inline *Stack helper methods, and the gas/memory helper functions
+// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
+// spliced into the generated dispatch (all by name).
+func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
+ fset = token.NewFileSet()
+ opcodeHandlers = map[string]*ast.FuncDecl{}
+ stackHelpers = map[string]*ast.FuncDecl{}
+ gasHelpers = map[string]*ast.FuncDecl{}
+ for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go", "interpreter.go"} {
+ path := filepath.Join(vmDir, name)
+ f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
+ if err != nil {
+ fatalf("parse %s: %v", path, err)
+ }
+ for _, decl := range f.Decls {
+ fn, ok := decl.(*ast.FuncDecl)
+ if !ok || fn.Body == nil {
+ continue
+ }
+ switch {
+ case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
+ gasHelpers[fn.Name.Name] = fn
+ case fn.Recv == nil: // top-level opXxx handler
+ opcodeHandlers[fn.Name.Name] = fn
+ case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
+ stackHelpers[fn.Name.Name] = fn
+ }
+ }
+ }
+ return fset, opcodeHandlers, stackHelpers, gasHelpers
+}
+
+// methodReceiver returns the receiver type name of a pointer-receiver method
+// (e.g. "Stack" for (s *Stack)), or "" if fn is not such a method.
+func methodReceiver(fn *ast.FuncDecl) string {
+ if fn.Recv == nil || len(fn.Recv.List) != 1 {
+ return ""
+ }
+ star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
+ if !ok {
+ return ""
+ }
+ id, ok := star.X.(*ast.Ident)
+ if !ok {
+ return ""
+ }
+ return id.Name
+}
+
+// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
+// helper for splicing into the generated dispatch.
+func hasInlineMarker(fn *ast.FuncDecl) bool {
+ if fn.Doc == nil {
+ return false
+ }
+ for _, c := range fn.Doc.List {
+ if c.Text == "//gen:inline" {
+ return true
+ }
+ }
+ return false
+}
+
+var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
+
+// spliceOpcodeBody returns a named handler's body, rewritten so it can be spliced
+// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
+func (g *generator) spliceOpcodeBody(handler string) string {
+ fn := g.opcodeHandlers[handler]
+ if fn == nil {
+ fatalf("no handler %q to inline", handler)
+ }
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
+}
+
+// spliceOpcodeFactoryBody splices the body of the executionFunc closure that a make*
+// factory returns, substituting the factory's parameters with the per-opcode
+// constants in args (positional, matching the factory signature). This lets
+// closure-built handlers (makePush, makeDup) be derived from their single
+// definition rather than restated in the generator. The caller emits the
+// result with p.
+func (g *generator) spliceOpcodeFactoryBody(factory string, args ...int) string {
+ fn := g.opcodeHandlers[factory]
+ if fn == nil {
+ fatalf("no factory %q to inline", factory)
+ }
+ lit := factoryClosure(factory, fn)
+ // Bind the factory parameters to the per-opcode constants, then inline.
+ names := paramNames(fn)
+ if len(names) != len(args) {
+ fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
+ }
+ params := map[string]int{}
+ for i, nm := range names {
+ params[nm] = args[i]
+ }
+ return g.rewriteOpcodeReturns(g.inlineStackHelpers(lit.Body.List, params))
+}
+
+// factoryClosure returns the executionFunc literal that a make* factory's body
+// is a single `return func(...) {...}` of.
+func factoryClosure(name string, fn *ast.FuncDecl) *ast.FuncLit {
+ if len(fn.Body.List) != 1 {
+ fatalf("factory %q body is not a single return", name)
+ }
+ ret, ok := fn.Body.List[0].(*ast.ReturnStmt)
+ if !ok || len(ret.Results) != 1 {
+ fatalf("factory %q does not return a single value", name)
+ }
+ lit, ok := ret.Results[0].(*ast.FuncLit)
+ if !ok {
+ fatalf("factory %q does not return a func literal", name)
+ }
+ return lit
+}
+
+// renderAst converts AST statements back to formatted Go source text, the
+// inverse of parsing. It uses the generator's fileset and emits nothing itself
+// (the caller passes the result to p).
+func (g *generator) renderAst(stmts []ast.Stmt) string {
+ var raw bytes.Buffer
+ cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
+ for _, stmt := range stmts {
+ if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
+ fatalf("print stmt: %v", err)
+ }
+ raw.WriteByte('\n')
+ }
+ return raw.String()
+}
+
+// rewriteOpcodeReturns rewrites a printed handler body so it runs inside the
+// dispatch loop: the `*pc` dereference becomes the loop's `pc` local, and each
+// `return r0, r1` becomes loop control flow. Success (r1 == nil) advances pc
+// and continues, an error sets err and breaks. (Stack helpers were already
+// inlined by inlineStackHelpers before the body was printed.)
+func (g *generator) rewriteOpcodeReturns(src string) string {
+ src = strings.ReplaceAll(src, "*pc", "pc")
+
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
+ indent, r0, r1 := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
+ if r1 == "nil" {
+ out.WriteString(indent + "pc++\n")
+ out.WriteString(indent + "continue mainLoop\n")
+ } else {
+ out.WriteString(indent + "res, err = " + r0 + ", " + r1 + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ }
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
+var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
+
+// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
+// the dispatch loop: a `return ` becomes the out-of-gas break, and the
+// trailing `return nil` (success) is dropped so the opcode continues.
+func (g *generator) rewriteGasReturns(src string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := gasReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val := m[1], strings.TrimSpace(m[2])
+ if val == "nil" {
+ continue // success: fall through to the rest of the op
+ }
+ out.WriteString(indent + "res, err = nil, " + val + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
+// rewriteStepReturns rewrites a spliced gas-step body's (value, error) returns so
+// it runs inline in the dispatch loop: a non-nil error becomes the out-of-gas
+// break; on success the value is assigned to target (or dropped when target is
+// empty) and the op falls through.
+func (g *generator) rewriteStepReturns(src, target string) string {
+ var out bytes.Buffer
+ for _, line := range strings.Split(src, "\n") {
+ if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
+ indent, val, errVal := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
+ if errVal == "nil" {
+ if target != "" {
+ out.WriteString(indent + target + " = " + val + "\n")
+ }
+ continue
+ }
+ out.WriteString(indent + "res, err = nil, " + errVal + "\n")
+ out.WriteString(indent + "break mainLoop\n")
+ continue
+ }
+ out.WriteString(line + "\n")
+ }
+ return out.String()
+}
+
+// stackCall is a matched call to a tagged helper.
+type stackCall struct {
+ helper string // helper method name
+ lhs []ast.Expr // assignment targets, nil for a void call like dup
+ tok token.Token // the assignment token, := or =
+ args []ast.Expr // call arguments (only dup has one)
+}
+
+// matchStackHelper matches a statement that is a single must-expand helper call,
+// in one of the two normalized forms: an assignment `lhs... := scope.Stack.H(args)`
+// or a bare `scope.Stack.H(args)`.
+func (g *generator) matchStackHelper(stmt ast.Stmt) (stackCall, bool) {
+ switch s := stmt.(type) {
+ case *ast.AssignStmt:
+ if len(s.Rhs) == 1 {
+ if h, args, ok := g.stackHelperCall(s.Rhs[0]); ok {
+ return stackCall{helper: h, lhs: s.Lhs, tok: s.Tok, args: args}, true
+ }
+ }
+ case *ast.ExprStmt:
+ if h, args, ok := g.stackHelperCall(s.X); ok {
+ return stackCall{helper: h, args: args}, true
+ }
+ }
+ return stackCall{}, false
+}
+
+// stackHelperCall unwraps scope.Stack.H(args) where H is a must-expand helper.
+func (g *generator) stackHelperCall(e ast.Expr) (helper string, args []ast.Expr, ok bool) {
+ call, isCall := e.(*ast.CallExpr)
+ if !isCall {
+ return "", nil, false
+ }
+ sel, isSel := call.Fun.(*ast.SelectorExpr) // .H
+ if !isSel || g.stackHelpers[sel.Sel.Name] == nil || !isStackExpr(sel.X) {
+ return "", nil, false
+ }
+ return sel.Sel.Name, call.Args, true
+}
+
+// isStackExpr reports whether e is the stack receiver: the `stack` local or
+// scope.Stack.
+func isStackExpr(e ast.Expr) bool {
+ switch x := e.(type) {
+ case *ast.Ident:
+ return x.Name == "stack"
+ case *ast.SelectorExpr:
+ return x.Sel.Name == "Stack"
+ }
+ return false
+}
+
+// inlineStackHelpers renders a handler body to source, inlining every must-expand
+// helper call and printing other statements unchanged. params maps the factory
+// parameters (makePush/makeDup) to their per-opcode constants.
+func (g *generator) inlineStackHelpers(stmts []ast.Stmt, params map[string]int) string {
+ var out strings.Builder
+ // Walk the handler body one statement at a time. A statement that is a
+ // tagged stack-helper call gets the helper's body spliced in: the generated
+ // dispatch is past Go's big-function inline budget, so the call would not be
+ // inlined otherwise. Every other statement is printed as written.
+ for _, stmt := range stmts {
+ if call, ok := g.matchStackHelper(stmt); ok {
+ // e.g. `x, y := scope.Stack.pop1Peek1()` becomes the body of pop1Peek1.
+ out.WriteString(g.inlineStackHelper(call, params))
+ } else {
+ // A plain statement: print it verbatim, then fill in any makePush or
+ // makeDup factory params with this opcode's constants.
+ out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
+ }
+ }
+ return out.String()
+}
+
+// substParams replaces each factory parameter with its constant. It runs only
+// on printed non-helper statements and on helper arguments, never on a helper
+// expansion, so it cannot touch a field like stack.size. The parameter names do
+// not textually overlap, so map order does not affect the result.
+func substParams(src string, params map[string]int) string {
+ for name, val := range params {
+ src = regexp.MustCompile(`\b`+name+`\b`).ReplaceAllString(src, fmt.Sprint(val))
+ }
+ return src
+}
+
+// inlineStackHelper expands one helper call to its stack.go body. The single
+// rule: the helper is straight-line statements then an optional final return
+// whose result count matches the call's targets. Anything else is not in
+// inlinable form and is a hard error (the shape post-condition). The receiver
+// maps to the loop's `stack` local and each parameter to its call argument.
+func (g *generator) inlineStackHelper(call stackCall, params map[string]int) string {
+ fn := g.stackHelpers[call.helper]
+ if fn == nil {
+ fatalf("no stack helper %q to inline", call.helper)
+ }
+ // Peel an optional trailing return off the body.
+ body := fn.Body.List
+ var ret *ast.ReturnStmt
+ if n := len(body); n > 0 {
+ if r, isRet := body[n-1].(*ast.ReturnStmt); isRet {
+ ret, body = r, body[:n-1]
+ }
+ }
+ results := 0
+ if ret != nil {
+ results = len(ret.Results)
+ }
+ if len(call.lhs) != results {
+ fatalf("stack helper %q returns %d values, call assigns %d", call.helper, results, len(call.lhs))
+ }
+ // Map the receiver to the loop local and each parameter to its argument.
+ names := paramNames(fn)
+ if len(names) != len(call.args) {
+ fatalf("stack helper %q takes %d params, call passes %d", call.helper, len(names), len(call.args))
+ }
+ subst := map[string]string{recvName(fn): "stack"}
+ for i, name := range names {
+ subst[name] = substParams(renderInlineExpr(call.args[i], nil), params)
+ }
+ // The leading bookkeeping statements, then bind each return expression to
+ // its assignment target.
+ var out strings.Builder
+ for _, stmt := range body {
+ out.WriteString(renderInlineStmt(stmt, subst) + "\n")
+ }
+ for i, lhs := range call.lhs {
+ out.WriteString(renderInlineExpr(lhs, nil) + " " + call.tok.String() + " " + renderInlineExpr(ret.Results[i], subst) + "\n")
+ }
+ return out.String()
+}
+
+// recvName returns a method's receiver name (e.g. "s").
+func recvName(fn *ast.FuncDecl) string {
+ if names := fn.Recv.List[0].Names; len(names) > 0 {
+ return names[0].Name
+ }
+ return ""
+}
+
+// paramNames returns a function's parameter names, in order.
+func paramNames(fn *ast.FuncDecl) []string {
+ var names []string
+ for _, f := range fn.Type.Params.List {
+ for _, nm := range f.Names {
+ names = append(names, nm.Name)
+ }
+ }
+ return names
+}
+
+// renderInlineStmt prints one helper-body statement with subst applied. Only the
+// statement shapes the helpers use are handled; any other is not inlinable.
+func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
+ switch s := stmt.(type) {
+ case *ast.IncDecStmt: // s.inner.top++
+ return renderInlineExpr(s.X, subst) + s.Tok.String()
+ case *ast.AssignStmt: // s.size -= 2, data[x] = data[y], or the swap tuple a, b = b, a
+ if len(s.Lhs) == len(s.Rhs) && len(s.Lhs) >= 1 {
+ lhs := make([]string, len(s.Lhs))
+ rhs := make([]string, len(s.Rhs))
+ for i := range s.Lhs {
+ lhs[i] = renderInlineExpr(s.Lhs[i], subst)
+ rhs[i] = renderInlineExpr(s.Rhs[i], subst)
+ }
+ return strings.Join(lhs, ", ") + " " + s.Tok.String() + " " + strings.Join(rhs, ", ")
+ }
+ }
+ fatalf("inline: unsupported statement %T in stack helper", stmt)
+ return ""
+}
+
+// renderInlineExpr prints one helper-body expression, substituting any
+// identifier found in subst. Only the shapes the helpers use are handled.
+func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
+ switch e := expr.(type) {
+ case *ast.Ident:
+ if r, ok := subst[e.Name]; ok {
+ return r
+ }
+ return e.Name
+ case *ast.BasicLit:
+ return e.Value
+ case *ast.SelectorExpr: // x.field
+ return renderInlineExpr(e.X, subst) + "." + e.Sel.Name
+ case *ast.IndexExpr: // x[i]
+ return renderInlineExpr(e.X, subst) + "[" + renderInlineExpr(e.Index, subst) + "]"
+ case *ast.BinaryExpr: // x op y
+ return renderInlineExpr(e.X, subst) + " " + e.Op.String() + " " + renderInlineExpr(e.Y, subst)
+ case *ast.UnaryExpr: // &x
+ return e.Op.String() + renderInlineExpr(e.X, subst)
+ }
+ fatalf("inline: unsupported expression %T in stack helper", expr)
+ return ""
+}
+
+// deriveSpecs records each opcode's constant values (name, intro fork, static
+// gas, stack bounds) from the first fork that defines it, then checks that the
+// opcodes chosen for inlining and direct-calling are safe to emit as constants
+// by verifying they are fork-stable (see checkStable and checkDirectCallStable).
+func (g *generator) deriveSpecs(forks []vm.GenFork) {
+ for code := range 256 {
+ for _, fork := range forks {
+ o := fork.Ops[code]
+ if !o.Defined {
+ continue
+ }
+ g.specs[code] = opSpec{
+ defined: true,
+ name: o.Name,
+ fork: fork.RuleField,
+ constGas: o.ConstantGas,
+ minStack: o.MinStack,
+ maxStack: o.MaxStack,
+ }
+ break // first fork that defines it wins (its intro fork)
+ }
+ }
+
+ // Every inlined opcode must be defined and have fork-stable static
+ // gas / stack bounds across all forks where it appears. Bail loudly otherwise.
+ for code, handler := range inlineOps {
+ g.checkStable(code, handler, forks)
+ }
+
+ // directCallOps opcodes emit their static gas and stack bounds as constants the
+ // same way, so they must be fork-stable too. Dynamic gas is allowed (it is
+ // charged through the named gas function, not a constant).
+ for code := range directCallOps {
+ g.checkDirectCallStable(code, forks)
+ }
+}
+
+// checkStable verifies an opcode selected for inlining is safe to inline: it must
+// be defined, its static gas and stack bounds must be the same across every fork
+// it appears in (they are emitted as constants), and it must have no dynamic gas,
+// since an inlined op charges only its constant static gas. It bails loudly
+// otherwise. `what` names the handler for the error message.
+func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
+ spec := g.specs[code]
+ if !spec.defined {
+ fatalf("opcode %#x (%s) selected for inlining but never defined", code, what)
+ }
+ for _, fork := range forks {
+ o := fork.Ops[code]
+ if !o.Defined {
+ continue
+ }
+ if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
+ fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
+ }
+ }
+}
+
+// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
+// gas and stack bounds must be the same across every fork it appears in (they are
+// emitted as constants), and its handler, gas and memory functions must be the same
+// across those forks too (they are called by name, so a fork that swapped one
+// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
+// directCallOps ops carry by definition. It does not check the directCallOps map's names
+// against the table, which the differential test covers.
+func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
+ spec := g.specs[code]
+ if !spec.defined {
+ fatalf("opcode %#x (directCallOps) is never defined", code)
+ }
+ var exec, dyn, mem string
+ seen := false
+ for _, fork := range forks {
+ o := fork.Ops[code]
+ if !o.Defined {
+ continue
+ }
+ if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
+ fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot emit as constants", code, spec.name, fork.Name)
+ }
+ // Handler, gas and memory functions must match across forks too, or
+ // direct-calling them by name would skip a fork that swapped one. Names
+ // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
+ // func, still stable across forks).
+ if !seen {
+ exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
+ } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
+ fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
+ code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
+ }
+ }
+}
+
+// generateStackChecks returns the underflow/overflow guards, mirroring the legacy
+// loop's order (stack validated before gas). minExpr and maxExpr are the
+// stack-bound expressions (constants on the inlined/direct paths,
+// operation.minStack/maxStack in the table path). under and over select which
+// guards to emit, so those paths can omit a guard whose bound is trivial.
+func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool) string {
+ switch {
+ case under && over:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+} else if sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, minExpr, minExpr, maxExpr, maxExpr)
+ case under:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
+ return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
+}
+`, minExpr, minExpr)
+ case over:
+ return fmt.Sprintf(`if sLen := stack.len(); sLen > %v {
+ return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
+}
+`, maxExpr, maxExpr)
+ }
+ return ""
+}
+
+// generateStaticGas returns the static-gas charge, spliced call-free from the
+// chargeRegularOnly body for amount: a constant on the inlined and
+// direct-call paths, operation.constantGas in the table path. The receiver maps
+// to contract.Gas and the method's single uint64 parameter to amount, substituted
+// textually on word boundaries (which cannot touch fields like RegularGas). Its
+// `return ` becomes the loop's out-of-gas exit and its trailing `return nil`
+// is dropped so the opcode falls through to its remaining steps (see
+// rewriteGasReturns).
+func (g *generator) generateStaticGas(amount any) string {
+ fn := g.gasHelpers["chargeRegularOnly"]
+ if fn == nil {
+ fatalf("no chargeRegularOnly gas helper to inline")
+ }
+ names := paramNames(fn)
+ if len(names) != 1 {
+ fatalf("chargeRegularOnly takes %d params, want 1", len(names))
+ }
+ src := g.renderAst(fn.Body.List)
+ src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
+ src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, fmt.Sprint(amount))
+ return g.rewriteGasReturns(src)
+}
+
+// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
+// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
+// so it runs only when the opcode is active for the current fork, otherwise the
+// case mirrors the legacy loop's undefined-opcode handling.
+func (g *generator) emitInlineOp(code byte) {
+ spec := g.specs[code]
+ g.p("case %s:\n", spec.name)
+ if spec.fork != "" {
+ g.p("if rules.%s {\n", spec.fork)
+ }
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < int(params.StackLimit)))
+
+ // static gas
+ if spec.constGas != 0 {
+ g.p("%s", g.generateStaticGas(spec.constGas))
+ }
+
+ // PUSH1-PUSH32 swap their execute function under EIP-4762 (verkle) to charge
+ // code-chunk gas on the immediate bytes. Defer to the table handler there.
+ // The constant static gas and stack guard above already match.
+ if code >= 0x60 && code <= 0x7f {
+ g.p(`
+ if isEIP4762 {
+ res, err = table[op].execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ pc++
+ continue mainLoop
+ }
+ `)
+ }
+
+ // opcode body
+ switch h := inlineOps[code]; h {
+ case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
+ n := int(code) - 0x5f
+ g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
+ case "makeDup": // DUP1-DUP16: splice makeDup(n)
+ g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
+ default: // the rest: splice the opXxx handler body
+ g.p("%s", g.spliceOpcodeBody(h))
+ }
+
+ // If opcode is inactive for this fork, then close the gate
+ // and fall back to the legacy loop's undefined-opcode handling.
+ if spec.fork != "" {
+ g.p(`
+ }
+ res, err = opUndefined(&pc, evm, scope)
+ break mainLoop
+ `)
+ }
+}
+
+// emitDirectCallOp emits an opcode case identical to the default case, except
+// the handler, dynamic-gas, and memory-size functions are called by name
+// rather than through the indirect operation.* table pointers. Valid only for
+// fork-invariant ops (see directCallOps).
+func (g *generator) emitDirectCallOp(code byte) {
+ spec := g.specs[code]
+ fns := directCallOps[code]
+ g.p("case %s:\n", spec.name)
+
+ // stack bounds check
+ g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < int(params.StackLimit)))
+
+ // static gas
+ if spec.constGas != 0 {
+ g.p("%s", g.generateStaticGas(spec.constGas))
+ }
+
+ // dynamic gas
+ g.p("\nvar memorySize uint64\n")
+
+ // Splice computeMemorySize's body, rewriting its operation.memorySize lookup to
+ // the opcode's memory-size function and its returns for the dispatch loop.
+ memSizeFn := g.gasHelpers["computeMemorySize"]
+ if memSizeFn == nil {
+ fatalf("no computeMemorySize gas helper to inline")
+ }
+ memSizeSrc := g.renderAst(memSizeFn.Body.List)
+ memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
+ g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
+
+ // Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
+ // the opcode's gas function.
+ dynGasFn := g.gasHelpers["chargeDynamicGas"]
+ if dynGasFn == nil {
+ fatalf("no chargeDynamicGas gas helper to inline")
+ }
+ dynGasSrc := g.renderAst(dynGasFn.Body.List)
+ dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
+ g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
+
+ // resize memory
+ g.p(`
+ if memorySize > 0 {
+ mem.Resize(memorySize)
+ }
+ `)
+
+ // call the opcode handler
+ g.p(`
+ res, err = %s(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ `, fns[0])
+
+ // advance to the next opcode
+ g.p(`
+ pc++
+ continue mainLoop
+ `)
+}
+
+// emitDefault emits the switch's default case: every opcode not inlined or
+// direct-called (the fork-varying ops such as CALL, CREATE, SSTORE, SLOAD, LOG
+// and the COPY family) is dispatched through the active per-fork table, exactly
+// as the legacy loop did, so their volatile gas and opcode logic stays shared
+// rather than restated here.
+func (g *generator) emitDefault() {
+ g.p(`
+ default:
+ operation := table[op]
+ `)
+ // stack bounds check
+ g.p("%s", g.generateStackChecks("operation.minStack", "operation.maxStack", true, true))
+
+ // static gas
+ g.p("%s", g.generateStaticGas("operation.constantGas"))
+
+ // dynamic gas
+ g.p(`
+ var memorySize uint64
+ if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
+ return nil, err
+ }
+ `)
+
+ // resize memory
+ g.p(`
+ if memorySize > 0 {
+ mem.Resize(memorySize)
+ }
+ `)
+
+ // call the opcode handler
+ g.p(`
+ res, err = operation.execute(&pc, evm, scope)
+ if err != nil {
+ break mainLoop
+ }
+ `)
+
+ // advance to the next opcode
+ g.p(`
+ pc++
+ continue mainLoop
+ `)
+}
+
+// createFile emits the whole generated file into g.buf: the header, package and
+// imports, then the execUntraced function (its locals and dispatch loop, the
+// verkle code-chunk gas, and a switch with one case per opcode built by the
+// emit* helpers). main formats the buffer and writes it to interpreter_gen.go.
+//
+// The switch has three tiers:
+//
+// - the hot, fork-stable opcodes (arithmetic / comparison / bitwise / PUSH /
+// DUP / SWAP / POP / JUMP / JUMPI / PC / MSIZE / JUMPDEST) are inlined by
+// splicing the existing opXxx handler bodies from instructions.go and
+// eips.go, with their static gas and stack bounds emitted as constants
+// derived from the per-fork instruction tables via vm.GenForks.
+//
+// - the fork-invariant ops (KECCAK256 / MLOAD / MSTORE / MSTORE8, see
+// directCallOps) are called directly by name, skipping the table's function
+// pointers, which Go cannot inline through.
+//
+// - everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG / the COPY
+// family and so on) is dispatched through the active per-fork JumpTable in
+// the default case, exactly as the legacy loop did, so volatile gas and
+// opcode logic stays shared rather than restated.
+func (g *generator) createFile() {
+ // file header, package clause, and imports
+ g.p(`
+ // Code generated by core/vm/gen; DO NOT EDIT.
+
+ package vm
+
+ import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ )
+
+ `)
+
+ // execUntraced: doc comment, loop-local declarations, and the dispatch loop
+ g.p(`
+ // execUntraced is the generated, tracing-free interpreter fast path. Hot,
+ // fork-stable opcodes are inlined with their static gas and stack bounds emitted
+ // as constants. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
+ // handler and gas functions directly by name. Everything fork-varying is
+ // dispatched through the active per-fork table in the default case. EVM.Run
+ // selects this path when no tracer is configured.
+ func (evm *EVM) execUntraced(scope *ScopeContext) (ret []byte, err error) {
+ var (
+ contract = scope.Contract
+ mem = scope.Memory
+ stack = scope.Stack
+ table = evm.table
+ rules = evm.chainRules
+ isEIP4762 = rules.IsEIP4762
+ pc = uint64(0)
+ res []byte
+ )
+ _ = mem
+ _ = rules
+ _ = isEIP4762
+ _ = table
+ mainLoop:
+ for {
+ `)
+
+ // verkle code-chunk gas, spliced from chargeVerkleCodeChunkGas
+ ccgFn := g.gasHelpers["chargeVerkleCodeChunkGas"]
+ if ccgFn == nil {
+ fatalf("no chargeVerkleCodeChunkGas gas helper to inline")
+ }
+ g.p("%s", g.rewriteGasReturns(g.renderAst(ccgFn.Body.List)))
+
+ // fetch the opcode and open the dispatch switch
+ g.p(`
+ op := contract.GetOp(pc)
+ switch op {
+ `)
+
+ // one case per inlined or direct-call opcode, in opcode order
+ for code := range 256 {
+ b := byte(code)
+ if _, named := inlineOps[b]; named {
+ g.emitInlineOp(b)
+ } else if _, dc := directCallOps[b]; dc {
+ g.emitDirectCallOp(b)
+ }
+ }
+
+ // the default case: fork-varying ops via the per-fork table
+ g.emitDefault()
+
+ // close the switch and loop, clear the stop token, and return
+ g.p(`
+ }
+ }
+ if err == errStopToken {
+ err = nil
+ }
+ return res, err
+ }
+ `)
+}
+
+func fatalf(format string, args ...any) {
+ fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
+ os.Exit(1)
+}
+
+// vmDir returns the core/vm directory, the parent of this generator package. It
+// is resolved from this source file's own path so it does not depend on the
+// directory the generator or the test happens to run from.
+func vmDir() string {
+ _, self, _, ok := runtime.Caller(0)
+ if !ok {
+ fatalf("cannot resolve generator source path")
+ }
+ return filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
+}
+
+// generate parses the opcode, gas and fork definitions under core/vm and returns
+// the formatted contents of interpreter_gen.go. It is the shared core of the
+// generator: main writes the result to disk, and the up-to-date test in
+// gen_test.go compares it against the committed file.
+func generate() ([]byte, error) {
+ fset, opcodeHandlers, stackHelpers, gasHelpers := parseHandlers(vmDir())
+ g := &generator{fset: fset, opcodeHandlers: opcodeHandlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
+ g.deriveSpecs(vm.GenForks())
+ g.createFile()
+
+ formatted, err := format.Source(g.buf.Bytes())
+ if err != nil {
+ dbg := filepath.Join(vmDir(), "interpreter_gen.go.broken")
+ os.WriteFile(dbg, g.buf.Bytes(), 0644)
+ return nil, fmt.Errorf("gofmt failed (%v); wrote unformatted output to %s", err, dbg)
+ }
+ return formatted, nil
+}
diff --git a/core/vm/gen/gen_test.go b/core/vm/gen/gen_test.go
new file mode 100644
index 0000000000..f22c3c9db0
--- /dev/null
+++ b/core/vm/gen/gen_test.go
@@ -0,0 +1,42 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package main
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// TestGeneratedDispatchUpToDate asserts that the committed interpreter_gen.go
+// matches what the generator produces from the current opcode, gas and fork
+// definitions. It is the CI guard against hand-edits to the generated file and
+// against the generator drifting from the committed output.
+func TestGeneratedDispatchUpToDate(t *testing.T) {
+ got, err := generate()
+ if err != nil {
+ t.Fatalf("running generator: %v", err)
+ }
+ want, err := os.ReadFile(filepath.Join(vmDir(), "interpreter_gen.go"))
+ if err != nil {
+ t.Fatalf("reading committed interpreter_gen.go: %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatal("interpreter_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
+ }
+}
diff --git a/core/vm/gen/main.go b/core/vm/gen/main.go
index 3f59e6f151..2c31a8c9d7 100644
--- a/core/vm/gen/main.go
+++ b/core/vm/gen/main.go
@@ -15,938 +15,25 @@
// along with the go-ethereum library. If not, see .
// Command gen generates core/vm/interpreter_gen.go, the EVM interpreter's untraced
-// fast-path dispatch. The output is a switch over the opcode byte that:
-//
-// - inlines the hot, fork-stable opcodes (arithmetic / comparison / bitwise /
-// PUSH / DUP / SWAP / POP / JUMP / JUMPI / PC / MSIZE / JUMPDEST) by
-// splicing the existing opXxx handler bodies from instructions.go and
-// eips.go, with their static gas and stack bounds baked in as constants
-// derived from the per-fork instruction tables via vm.GenForks.
-//
-// - calls the fork-invariant ops (KECCAK256 / MLOAD / MSTORE / MSTORE8,
-// see directCallOps) directly by name, skipping the table's function
-// pointers, which Go cannot inline through.
-//
-// - dispatches everything fork-varying (CALL / CREATE / SSTORE / SLOAD / LOG /
-// the COPY family and so on) through the active per-fork JumpTable in the
-// default case, exactly as the legacy loop did, so volatile gas and opcode
-// logic stays shared rather than restated.
-//
-// The generated file is committed and a CI test asserts it matches `go generate`
-// output. Do not hand-edit interpreter_gen.go.
+// fast-path dispatch, a switch over the opcode byte. The generated file is
+// committed and a CI test asserts it matches `go generate` output. Do not
+// hand-edit interpreter_gen.go.
//
// Usage: go generate ./core/vm/...
package main
import (
- "bytes"
"fmt"
- "go/ast"
- "go/format"
- "go/parser"
- "go/printer"
- "go/token"
"os"
"path/filepath"
- "regexp"
- "runtime"
- "strings"
-
- "github.com/ethereum/go-ethereum/core/vm"
)
-const stackLimit = 1024 // params.StackLimit
-
-// inlineOps maps an opcode byte to the handler whose body is spliced inline
-// for that opcode. These are the hot, fork-stable opcodes with no dynamic gas.
-// The value is usually an opXxx handler, but PUSH3-PUSH32 and DUP1-DUP16 are
-// factory-built (one shared makePush / makeDup each), so their value is the
-// factory name and emitInlineOp splices the factory body with the per-opcode size.
-// Opcodes not listed here (or in directCallOps) fall through to the default case,
-// which dispatches via the per-fork table.
-var inlineOps = func() map[byte]string {
- m := map[byte]string{
- 0x01: "opAdd", 0x02: "opMul", 0x03: "opSub", 0x04: "opDiv", 0x05: "opSdiv",
- 0x06: "opMod", 0x07: "opSmod", 0x08: "opAddmod", 0x09: "opMulmod", 0x0b: "opSignExtend",
- 0x10: "opLt", 0x11: "opGt", 0x12: "opSlt", 0x13: "opSgt", 0x14: "opEq", 0x15: "opIszero",
- 0x16: "opAnd", 0x17: "opOr", 0x18: "opXor", 0x19: "opNot", 0x1a: "opByte",
- 0x1b: "opSHL", 0x1c: "opSHR", 0x1d: "opSAR", 0x1e: "opCLZ",
- 0x50: "opPop", 0x56: "opJump", 0x57: "opJumpi", 0x58: "opPc", 0x59: "opMsize", 0x5b: "opJumpdest",
- 0x5f: "opPush0", 0x60: "opPush1", 0x61: "opPush2",
- }
- for code := 0x62; code <= 0x7f; code++ { // PUSH3-PUSH32
- m[byte(code)] = "makePush"
- }
- for code := 0x80; code <= 0x8f; code++ { // DUP1-DUP16
- m[byte(code)] = "makeDup"
- }
- for code := 0x90; code <= 0x9f; code++ { // SWAP1-SWAP16
- m[byte(code)] = fmt.Sprintf("opSwap%d", code-0x8f)
- }
- return m
-}()
-
-// directCallOps lists the opcodes (dynamic gas, not inlined) whose handler,
-// dynamic-gas, and memory-size functions are the same across every fork
-// (verified: untouched by any enableXxx). They are emitted as direct calls to
-// those functions by name instead of the indirect operation.* pointer calls
-// in the default case.
-var directCallOps = map[byte][3]string{
- 0x20: {"opKeccak256", "gasKeccak256", "memoryKeccak256"},
- 0x51: {"opMload", "gasMLoad", "memoryMLoad"},
- 0x52: {"opMstore", "gasMStore", "memoryMStore"},
- 0x53: {"opMstore8", "gasMStore8", "memoryMStore8"},
-}
-
-// opSpec holds the per-opcode constants the generator bakes (gas, stack bounds, intro fork), derived from the per-fork tables.
-type opSpec struct {
- defined bool
- name string
- fork string
- constGas uint64
- minStack int
- maxStack int
-}
-
-type generator struct {
- fset *token.FileSet
- opcodeHandlers map[string]*ast.FuncDecl
- stackHelpers map[string]*ast.FuncDecl
- gasHelpers map[string]*ast.FuncDecl
- specs [256]opSpec
- buf *bytes.Buffer
-}
-
-// p is the writer of the generated file. Every line of output is appended
-// to g.buf through it.
-func (g *generator) p(format string, args ...any) {
- format = strings.TrimRight(strings.TrimPrefix(format, "\n"), " \t")
- fmt.Fprintf(g.buf, format, args...)
-}
-
-// parseHandlers parses instructions.go, eips.go, stack.go, gascosts.go and
-// interpreter.go. It returns the top-level opXxx handlers by name, the
-// //gen:inline *Stack helper methods, and the gas/memory helper functions
-// (chargeRegularOnly, computeMemorySize, chargeDynamicGas) whose bodies are
-// spliced into the generated dispatch (all by name).
-func parseHandlers(vmDir string) (fset *token.FileSet, opcodeHandlers, stackHelpers, gasHelpers map[string]*ast.FuncDecl) {
- fset = token.NewFileSet()
- opcodeHandlers = map[string]*ast.FuncDecl{}
- stackHelpers = map[string]*ast.FuncDecl{}
- gasHelpers = map[string]*ast.FuncDecl{}
- for _, name := range []string{"instructions.go", "eips.go", "stack.go", "gascosts.go", "interpreter.go"} {
- path := filepath.Join(vmDir, name)
- f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
- if err != nil {
- fatalf("parse %s: %v", path, err)
- }
- for _, decl := range f.Decls {
- fn, ok := decl.(*ast.FuncDecl)
- if !ok || fn.Body == nil {
- continue
- }
- switch {
- case fn.Name.Name == "chargeRegularOnly" || fn.Name.Name == "computeMemorySize" || fn.Name.Name == "chargeDynamicGas" || fn.Name.Name == "chargeVerkleCodeChunkGas": // spliced gas/memory helpers
- gasHelpers[fn.Name.Name] = fn
- case fn.Recv == nil: // top-level opXxx handler
- opcodeHandlers[fn.Name.Name] = fn
- case methodReceiver(fn) == "Stack" && hasInlineMarker(fn): // (s *Stack) helper tagged //gen:inline
- stackHelpers[fn.Name.Name] = fn
- }
- }
- }
- return fset, opcodeHandlers, stackHelpers, gasHelpers
-}
-
-// methodReceiver returns the receiver type name of a pointer-receiver method
-// (e.g. "Stack" for (s *Stack)), or "" if fn is not such a method.
-func methodReceiver(fn *ast.FuncDecl) string {
- if fn.Recv == nil || len(fn.Recv.List) != 1 {
- return ""
- }
- star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
- if !ok {
- return ""
- }
- id, ok := star.X.(*ast.Ident)
- if !ok {
- return ""
- }
- return id.Name
-}
-
-// hasInlineMarker reports whether fn is tagged //gen:inline, which marks a stack
-// helper for splicing into the generated dispatch.
-func hasInlineMarker(fn *ast.FuncDecl) bool {
- if fn.Doc == nil {
- return false
- }
- for _, c := range fn.Doc.List {
- if c.Text == "//gen:inline" {
- return true
- }
- }
- return false
-}
-
-var opcodeReturnRe = regexp.MustCompile(`^(\s*)return\s+([^,]+),\s*(.+)$`)
-
-// spliceOpcodeBody returns a named handler's body, rewritten so it can be spliced
-// into the dispatch loop (see rewriteOpcodeReturns). The caller emits it with p.
-func (g *generator) spliceOpcodeBody(handler string) string {
- fn := g.opcodeHandlers[handler]
- if fn == nil {
- fatalf("no handler %q to inline", handler)
- }
- return g.rewriteOpcodeReturns(g.inlineStackHelpers(fn.Body.List, nil))
-}
-
-// spliceOpcodeFactoryBody splices the body of the executionFunc closure that a make*
-// factory returns, substituting the factory's parameters with the per-opcode
-// constants in args (positional, matching the factory signature). This lets
-// closure-built handlers (makePush, makeDup) be derived from their single
-// definition rather than restated in the generator. The caller emits the
-// result with p.
-func (g *generator) spliceOpcodeFactoryBody(factory string, args ...int) string {
- fn := g.opcodeHandlers[factory]
- if fn == nil {
- fatalf("no factory %q to inline", factory)
- }
- lit := factoryClosure(factory, fn)
- // Bind the factory parameters to the per-opcode constants, then inline.
- names := paramNames(fn)
- if len(names) != len(args) {
- fatalf("factory %q takes %d params, got %d args", factory, len(names), len(args))
- }
- params := map[string]int{}
- for i, nm := range names {
- params[nm] = args[i]
- }
- return g.rewriteOpcodeReturns(g.inlineStackHelpers(lit.Body.List, params))
-}
-
-// factoryClosure returns the executionFunc literal that a make* factory's body
-// is a single `return func(...) {...}` of.
-func factoryClosure(name string, fn *ast.FuncDecl) *ast.FuncLit {
- if len(fn.Body.List) != 1 {
- fatalf("factory %q body is not a single return", name)
- }
- ret, ok := fn.Body.List[0].(*ast.ReturnStmt)
- if !ok || len(ret.Results) != 1 {
- fatalf("factory %q does not return a single value", name)
- }
- lit, ok := ret.Results[0].(*ast.FuncLit)
- if !ok {
- fatalf("factory %q does not return a func literal", name)
- }
- return lit
-}
-
-// renderAst converts AST statements back to formatted Go source text, the
-// inverse of parsing. It uses the generator's fileset and emits nothing itself
-// (the caller passes the result to p).
-func (g *generator) renderAst(stmts []ast.Stmt) string {
- var raw bytes.Buffer
- cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
- for _, stmt := range stmts {
- if err := cfg.Fprint(&raw, g.fset, stmt); err != nil {
- fatalf("print stmt: %v", err)
- }
- raw.WriteByte('\n')
- }
- return raw.String()
-}
-
-// rewriteOpcodeReturns rewrites a printed handler body so it runs inside the
-// dispatch loop: the `*pc` dereference becomes the loop's `pc` local, and each
-// `return r0, r1` becomes loop control flow. Success (r1 == nil) advances pc
-// and continues, an error sets err and breaks. (Stack helpers were already
-// inlined by inlineStackHelpers before the body was printed.)
-func (g *generator) rewriteOpcodeReturns(src string) string {
- src = strings.ReplaceAll(src, "*pc", "pc")
-
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
- indent, r0, r1 := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
- if r1 == "nil" {
- out.WriteString(indent + "pc++\n")
- out.WriteString(indent + "continue mainLoop\n")
- } else {
- out.WriteString(indent + "res, err = " + r0 + ", " + r1 + "\n")
- out.WriteString(indent + "break mainLoop\n")
- }
- continue
- }
- out.WriteString(line + "\n")
- }
- return out.String()
-}
-
-var gasReturnRe = regexp.MustCompile(`^(\s*)return\s+(\S.*)$`)
-
-// rewriteGasReturns rewrites a spliced charge body so it runs as a gas step in
-// the dispatch loop: a `return ` becomes the out-of-gas break, and the
-// trailing `return nil` (success) is dropped so the opcode continues.
-func (g *generator) rewriteGasReturns(src string) string {
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := gasReturnRe.FindStringSubmatch(line); m != nil {
- indent, val := m[1], strings.TrimSpace(m[2])
- if val == "nil" {
- continue // success: fall through to the rest of the op
- }
- out.WriteString(indent + "res, err = nil, " + val + "\n")
- out.WriteString(indent + "break mainLoop\n")
- continue
- }
- out.WriteString(line + "\n")
- }
- return out.String()
-}
-
-// rewriteStepReturns rewrites a spliced gas-step body's (value, error) returns so
-// it runs inline in the dispatch loop: a non-nil error becomes the out-of-gas
-// break; on success the value is assigned to target (or dropped when target is
-// empty) and the op falls through.
-func (g *generator) rewriteStepReturns(src, target string) string {
- var out bytes.Buffer
- for _, line := range strings.Split(src, "\n") {
- if m := opcodeReturnRe.FindStringSubmatch(line); m != nil {
- indent, val, errVal := m[1], strings.TrimSpace(m[2]), strings.TrimSpace(m[3])
- if errVal == "nil" {
- if target != "" {
- out.WriteString(indent + target + " = " + val + "\n")
- }
- continue
- }
- out.WriteString(indent + "res, err = nil, " + errVal + "\n")
- out.WriteString(indent + "break mainLoop\n")
- continue
- }
- out.WriteString(line + "\n")
- }
- return out.String()
-}
-
-// stackCall is a matched call to a tagged helper.
-type stackCall struct {
- helper string // helper method name
- lhs []ast.Expr // assignment targets, nil for a void call like dup
- tok token.Token // the assignment token, := or =
- args []ast.Expr // call arguments (only dup has one)
-}
-
-// matchStackHelper matches a statement that is a single must-expand helper call,
-// in one of the two normalized forms: an assignment `lhs... := scope.Stack.H(args)`
-// or a bare `scope.Stack.H(args)`.
-func (g *generator) matchStackHelper(stmt ast.Stmt) (stackCall, bool) {
- switch s := stmt.(type) {
- case *ast.AssignStmt:
- if len(s.Rhs) == 1 {
- if h, args, ok := g.stackHelperCall(s.Rhs[0]); ok {
- return stackCall{helper: h, lhs: s.Lhs, tok: s.Tok, args: args}, true
- }
- }
- case *ast.ExprStmt:
- if h, args, ok := g.stackHelperCall(s.X); ok {
- return stackCall{helper: h, args: args}, true
- }
- }
- return stackCall{}, false
-}
-
-// stackHelperCall unwraps scope.Stack.H(args) where H is a must-expand helper.
-func (g *generator) stackHelperCall(e ast.Expr) (helper string, args []ast.Expr, ok bool) {
- call, isCall := e.(*ast.CallExpr)
- if !isCall {
- return "", nil, false
- }
- sel, isSel := call.Fun.(*ast.SelectorExpr) // .H
- if !isSel || g.stackHelpers[sel.Sel.Name] == nil || !isStackExpr(sel.X) {
- return "", nil, false
- }
- return sel.Sel.Name, call.Args, true
-}
-
-// isStackExpr reports whether e is the stack receiver: the `stack` local or
-// scope.Stack.
-func isStackExpr(e ast.Expr) bool {
- switch x := e.(type) {
- case *ast.Ident:
- return x.Name == "stack"
- case *ast.SelectorExpr:
- return x.Sel.Name == "Stack"
- }
- return false
-}
-
-// inlineStackHelpers renders a handler body to source, inlining every must-expand
-// helper call and printing other statements unchanged. params maps the factory
-// parameters (makePush/makeDup) to their per-opcode constants.
-func (g *generator) inlineStackHelpers(stmts []ast.Stmt, params map[string]int) string {
- var out strings.Builder
- // Walk the handler body one statement at a time. A statement that is a
- // tagged stack-helper call gets the helper's body spliced in: the generated
- // dispatch is past Go's big-function inline budget, so the call would not be
- // inlined otherwise. Every other statement is printed as written.
- for _, stmt := range stmts {
- if call, ok := g.matchStackHelper(stmt); ok {
- // e.g. `x, y := scope.Stack.pop1Peek1()` becomes the body of pop1Peek1.
- out.WriteString(g.inlineStackHelper(call, params))
- } else {
- // A plain statement: print it verbatim, then fill in any makePush or
- // makeDup factory params with this opcode's constants.
- out.WriteString(substParams(g.renderAst([]ast.Stmt{stmt}), params))
- }
- }
- return out.String()
-}
-
-// substParams replaces each factory parameter with its constant. It runs only
-// on printed non-helper statements and on helper arguments, never on a helper
-// expansion, so it cannot touch a field like stack.size. The parameter names do
-// not textually overlap, so map order does not affect the result.
-func substParams(src string, params map[string]int) string {
- for name, val := range params {
- src = regexp.MustCompile(`\b`+name+`\b`).ReplaceAllString(src, fmt.Sprint(val))
- }
- return src
-}
-
-// inlineStackHelper expands one helper call to its stack.go body. The single
-// rule: the helper is straight-line statements then an optional final return
-// whose result count matches the call's targets. Anything else is not in
-// inlinable form and is a hard error (the shape post-condition). The receiver
-// maps to the loop's `stack` local and each parameter to its call argument.
-func (g *generator) inlineStackHelper(call stackCall, params map[string]int) string {
- fn := g.stackHelpers[call.helper]
- if fn == nil {
- fatalf("no stack helper %q to inline", call.helper)
- }
- // Peel an optional trailing return off the body.
- body := fn.Body.List
- var ret *ast.ReturnStmt
- if n := len(body); n > 0 {
- if r, isRet := body[n-1].(*ast.ReturnStmt); isRet {
- ret, body = r, body[:n-1]
- }
- }
- results := 0
- if ret != nil {
- results = len(ret.Results)
- }
- if len(call.lhs) != results {
- fatalf("stack helper %q returns %d values, call assigns %d", call.helper, results, len(call.lhs))
- }
- // Map the receiver to the loop local and each parameter to its argument.
- names := paramNames(fn)
- if len(names) != len(call.args) {
- fatalf("stack helper %q takes %d params, call passes %d", call.helper, len(names), len(call.args))
- }
- subst := map[string]string{recvName(fn): "stack"}
- for i, name := range names {
- subst[name] = substParams(renderInlineExpr(call.args[i], nil), params)
- }
- // The leading bookkeeping statements, then bind each return expression to
- // its assignment target.
- var out strings.Builder
- for _, stmt := range body {
- out.WriteString(renderInlineStmt(stmt, subst) + "\n")
- }
- for i, lhs := range call.lhs {
- out.WriteString(renderInlineExpr(lhs, nil) + " " + call.tok.String() + " " + renderInlineExpr(ret.Results[i], subst) + "\n")
- }
- return out.String()
-}
-
-// recvName returns a method's receiver name (e.g. "s").
-func recvName(fn *ast.FuncDecl) string {
- if names := fn.Recv.List[0].Names; len(names) > 0 {
- return names[0].Name
- }
- return ""
-}
-
-// paramNames returns a function's parameter names, in order.
-func paramNames(fn *ast.FuncDecl) []string {
- var names []string
- for _, f := range fn.Type.Params.List {
- for _, nm := range f.Names {
- names = append(names, nm.Name)
- }
- }
- return names
-}
-
-// renderInlineStmt prints one helper-body statement with subst applied. Only the
-// statement shapes the helpers use are handled; any other is not inlinable.
-func renderInlineStmt(stmt ast.Stmt, subst map[string]string) string {
- switch s := stmt.(type) {
- case *ast.IncDecStmt: // s.inner.top++
- return renderInlineExpr(s.X, subst) + s.Tok.String()
- case *ast.AssignStmt: // s.size -= 2, data[x] = data[y], or the swap tuple a, b = b, a
- if len(s.Lhs) == len(s.Rhs) && len(s.Lhs) >= 1 {
- lhs := make([]string, len(s.Lhs))
- rhs := make([]string, len(s.Rhs))
- for i := range s.Lhs {
- lhs[i] = renderInlineExpr(s.Lhs[i], subst)
- rhs[i] = renderInlineExpr(s.Rhs[i], subst)
- }
- return strings.Join(lhs, ", ") + " " + s.Tok.String() + " " + strings.Join(rhs, ", ")
- }
- }
- fatalf("inline: unsupported statement %T in stack helper", stmt)
- return ""
-}
-
-// renderInlineExpr prints one helper-body expression, substituting any
-// identifier found in subst. Only the shapes the helpers use are handled.
-func renderInlineExpr(expr ast.Expr, subst map[string]string) string {
- switch e := expr.(type) {
- case *ast.Ident:
- if r, ok := subst[e.Name]; ok {
- return r
- }
- return e.Name
- case *ast.BasicLit:
- return e.Value
- case *ast.SelectorExpr: // x.field
- return renderInlineExpr(e.X, subst) + "." + e.Sel.Name
- case *ast.IndexExpr: // x[i]
- return renderInlineExpr(e.X, subst) + "[" + renderInlineExpr(e.Index, subst) + "]"
- case *ast.BinaryExpr: // x op y
- return renderInlineExpr(e.X, subst) + " " + e.Op.String() + " " + renderInlineExpr(e.Y, subst)
- case *ast.UnaryExpr: // &x
- return e.Op.String() + renderInlineExpr(e.X, subst)
- }
- fatalf("inline: unsupported expression %T in stack helper", expr)
- return ""
-}
-
-// deriveSpecs records each opcode's bakeable constants (name, intro fork, static
-// gas, stack bounds) from the first fork that defines it, then checks that the
-// opcodes chosen for inlining and direct-calling are safe to bake by verifying
-// their constants are fork-stable (see checkStable and checkDirectCallStable).
-func (g *generator) deriveSpecs(forks []vm.GenFork) {
- for code := 0; code < 256; code++ {
- for _, fork := range forks {
- o := fork.Ops[code]
- if !o.Defined {
- continue
- }
- g.specs[code] = opSpec{
- defined: true,
- name: o.Name,
- fork: fork.RuleField,
- constGas: o.ConstantGas,
- minStack: o.MinStack,
- maxStack: o.MaxStack,
- }
- break // first fork that defines it wins (its intro fork)
- }
- }
-
- // Every inlined opcode must be defined and have fork-stable static
- // gas / stack bounds across all forks where it appears. Bail loudly otherwise.
- for code, handler := range inlineOps {
- g.checkStable(code, handler, forks)
- }
-
- // directCallOps opcodes bake their static gas and stack bounds the same way, so
- // they must be fork-stable too. Dynamic gas is allowed (it is charged through
- // the named gas function, not baked).
- for code := range directCallOps {
- g.checkDirectCallStable(code, forks)
- }
-}
-
-// checkStable verifies an opcode selected for inlining is safe to inline: it must
-// be defined, its static gas and stack bounds must be the same across every fork
-// it appears in (they are baked as constants), and it must have no dynamic gas,
-// since an inlined op charges only the baked static gas. It bails loudly
-// otherwise. `what` names the handler for the error message.
-func (g *generator) checkStable(code byte, what string, forks []vm.GenFork) {
- spec := g.specs[code]
- if !spec.defined {
- fatalf("opcode %#x (%s) selected for inlining but never defined", code, what)
- }
- for _, fork := range forks {
- o := fork.Ops[code]
- if !o.Defined {
- continue
- }
- if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack || o.DynamicGasFn != "" {
- fatalf("opcode %#x (%s) is not fork-stable (fork %s): cannot inline", code, what, fork.Name)
- }
- }
-}
-
-// checkDirectCallStable verifies a directCallOps opcode is safe to direct-call. Its static
-// gas and stack bounds must be the same across every fork it appears in (they are
-// baked as constants), and its handler, gas and memory functions must be the same
-// across those forks too (they are called by name, so a fork that swapped one
-// would otherwise be missed). Unlike checkStable it allows dynamic gas, which
-// directCallOps ops carry by definition. It does not check the directCallOps map's names
-// against the table, which the differential test covers.
-func (g *generator) checkDirectCallStable(code byte, forks []vm.GenFork) {
- spec := g.specs[code]
- if !spec.defined {
- fatalf("opcode %#x (directCallOps) is never defined", code)
- }
- var exec, dyn, mem string
- seen := false
- for _, fork := range forks {
- o := fork.Ops[code]
- if !o.Defined {
- continue
- }
- if o.ConstantGas != spec.constGas || o.MinStack != spec.minStack || o.MaxStack != spec.maxStack {
- fatalf("opcode %#x (%s) is in directCallOps but not fork-stable (fork %s): static gas or stack bounds vary, cannot bake", code, spec.name, fork.Name)
- }
- // Handler, gas and memory functions must match across forks too, or
- // direct-calling them by name would skip a fork that swapped one. Names
- // come from FuncForPC via vm.GenForks (aliases resolve to the underlying
- // func, still stable across forks).
- if !seen {
- exec, dyn, mem, seen = o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, true
- } else if o.ExecuteFn != exec || o.DynamicGasFn != dyn || o.MemorySizeFn != mem {
- fatalf("opcode %#x (%s) is in directCallOps but its functions vary by fork (fork %s): got %s/%s/%s, want %s/%s/%s, cannot direct-call",
- code, spec.name, fork.Name, o.ExecuteFn, o.DynamicGasFn, o.MemorySizeFn, exec, dyn, mem)
- }
- }
-}
-
-// generateStackChecks returns the underflow/overflow guards, mirroring the legacy
-// loop's order (stack validated before gas). minExpr and maxExpr are the
-// stack-bound expressions (baked constants on the inlined/direct paths,
-// operation.minStack/maxStack in the table path). under and over select which
-// guards to emit, so the baked paths can omit a guard whose bound is trivial.
-func (g *generator) generateStackChecks(minExpr, maxExpr any, under, over bool) string {
- switch {
- case under && over:
- return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
-} else if sLen > %v {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
-}
-`, minExpr, minExpr, maxExpr, maxExpr)
- case under:
- return fmt.Sprintf(`if sLen := stack.len(); sLen < %v {
- return nil, &ErrStackUnderflow{stackLen: sLen, required: %v}
-}
-`, minExpr, minExpr)
- case over:
- return fmt.Sprintf(`if sLen := stack.len(); sLen > %v {
- return nil, &ErrStackOverflow{stackLen: sLen, limit: %v}
-}
-`, maxExpr, maxExpr)
- }
- return ""
-}
-
-// generateStaticGas returns the static-gas charge, spliced call-free from the
-// chargeRegularOnly body for amount: a baked constant on the inlined and
-// direct-call paths, operation.constantGas in the table path. The receiver maps
-// to contract.Gas and the method's single uint64 parameter to amount, substituted
-// textually on word boundaries (which cannot touch fields like RegularGas). Its
-// `return ` becomes the loop's out-of-gas exit and its trailing `return nil`
-// is dropped so the opcode falls through to its remaining steps (see
-// rewriteGasReturns).
-func (g *generator) generateStaticGas(amount any) string {
- fn := g.gasHelpers["chargeRegularOnly"]
- if fn == nil {
- fatalf("no chargeRegularOnly gas helper to inline")
- }
- names := paramNames(fn)
- if len(names) != 1 {
- fatalf("chargeRegularOnly takes %d params, want 1", len(names))
- }
- src := g.renderAst(fn.Body.List)
- src = regexp.MustCompile(`\b`+recvName(fn)+`\b`).ReplaceAllString(src, "contract.Gas")
- src = regexp.MustCompile(`\b`+names[0]+`\b`).ReplaceAllString(src, fmt.Sprint(amount))
- return g.rewriteGasReturns(src)
-}
-
-// emitInlineOp emits an inlined opcode case: the stack and gas guards followed by
-// the spliced opcode body. A fork-introduced opcode wraps that body in a fork gate
-// so it runs only when the opcode is active for the current fork, otherwise the
-// case mirrors the legacy loop's undefined-opcode handling.
-func (g *generator) emitInlineOp(code byte) {
- spec := g.specs[code]
- g.p("case %s:\n", spec.name)
- if spec.fork != "" {
- g.p("if rules.%s {\n", spec.fork)
- }
-
- // stack bounds check
- g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
-
- // static gas
- if spec.constGas != 0 {
- g.p("%s", g.generateStaticGas(spec.constGas))
- }
-
- // PUSH1-PUSH32 swap their execute function under EIP-4762 (verkle) to charge
- // code-chunk gas on the immediate bytes. Defer to the table handler there.
- // The baked static gas and stack guard above already match.
- if code >= 0x60 && code <= 0x7f {
- g.p(`
- if isEIP4762 {
- res, err = table[op].execute(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- pc++
- continue mainLoop
- }
- `)
- }
-
- // opcode body
- switch h := inlineOps[code]; h {
- case "makePush": // PUSH3-PUSH32: splice makePush(size, size)
- n := int(code) - 0x5f
- g.p("%s", g.spliceOpcodeFactoryBody("makePush", n, n))
- case "makeDup": // DUP1-DUP16: splice makeDup(n)
- g.p("%s", g.spliceOpcodeFactoryBody("makeDup", int(code)-0x7f))
- default: // the rest: splice the opXxx handler body
- g.p("%s", g.spliceOpcodeBody(h))
- }
-
- // If opcode is inactive for this fork, then close the gate
- // and fall back to the legacy loop's undefined-opcode handling.
- if spec.fork != "" {
- g.p(`
- }
- res, err = opUndefined(&pc, evm, scope)
- break mainLoop
- `)
- }
-}
-
-// emitDirectCallOp emits an opcode case identical to the default case, except
-// the handler, dynamic-gas, and memory-size functions are called by name
-// rather than through the indirect operation.* table pointers. Valid only for
-// fork-invariant ops (see directCallOps).
-func (g *generator) emitDirectCallOp(code byte) {
- spec := g.specs[code]
- fns := directCallOps[code]
- g.p("case %s:\n", spec.name)
-
- // stack bounds check
- g.p("%s", g.generateStackChecks(spec.minStack, spec.maxStack, spec.minStack > 0, spec.maxStack < stackLimit))
-
- // static gas
- if spec.constGas != 0 {
- g.p("%s", g.generateStaticGas(spec.constGas))
- }
-
- // dynamic gas
- g.p("\nvar memorySize uint64\n")
-
- // Splice computeMemorySize's body, rewriting its operation.memorySize lookup to
- // the opcode's memory-size function and its returns for the dispatch loop.
- memSizeFn := g.gasHelpers["computeMemorySize"]
- if memSizeFn == nil {
- fatalf("no computeMemorySize gas helper to inline")
- }
- memSizeSrc := g.renderAst(memSizeFn.Body.List)
- memSizeSrc = strings.ReplaceAll(memSizeSrc, "operation.memorySize", fns[2])
- g.p("%s", g.rewriteStepReturns(memSizeSrc, "memorySize"))
-
- // Splice chargeDynamicGas's body the same way, rewriting operation.dynamicGas to
- // the opcode's gas function.
- dynGasFn := g.gasHelpers["chargeDynamicGas"]
- if dynGasFn == nil {
- fatalf("no chargeDynamicGas gas helper to inline")
- }
- dynGasSrc := g.renderAst(dynGasFn.Body.List)
- dynGasSrc = strings.ReplaceAll(dynGasSrc, "operation.dynamicGas", fns[1])
- g.p("%s", g.rewriteStepReturns(dynGasSrc, ""))
-
- // resize memory
- g.p(`
- if memorySize > 0 {
- mem.Resize(memorySize)
- }
- `)
-
- // call the opcode handler
- g.p(`
- res, err = %s(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- `, fns[0])
-
- // advance to the next opcode
- g.p(`
- pc++
- continue mainLoop
- `)
-}
-
-// emitDefault emits the switch's default case: every opcode not inlined or
-// direct-called (the fork-varying ops such as CALL, CREATE, SSTORE, SLOAD, LOG
-// and the COPY family) is dispatched through the active per-fork table, exactly
-// as the legacy loop did, so their volatile gas and opcode logic stays shared
-// rather than restated here.
-func (g *generator) emitDefault() {
- g.p(`
- default:
- operation := table[op]
- `)
- // stack bounds check
- g.p("%s", g.generateStackChecks("operation.minStack", "operation.maxStack", true, true))
-
- // static gas
- g.p("%s", g.generateStaticGas("operation.constantGas"))
-
- // dynamic gas
- g.p(`
- var memorySize uint64
- if memorySize, _, err = contract.meterDynamicGas(operation, evm, stack, mem); err != nil {
- return nil, err
- }
- `)
-
- // resize memory
- g.p(`
- if memorySize > 0 {
- mem.Resize(memorySize)
- }
- `)
-
- // call the opcode handler
- g.p(`
- res, err = operation.execute(&pc, evm, scope)
- if err != nil {
- break mainLoop
- }
- `)
-
- // advance to the next opcode
- g.p(`
- pc++
- continue mainLoop
- `)
-}
-
-// createFile emits the whole generated file into g.buf: the header, package and
-// imports, then the execUntraced function (its locals and dispatch loop, the
-// verkle code-chunk gas, and a switch with one case per opcode built by the
-// emit* helpers). main formats the buffer and writes it to interpreter_gen.go.
-func (g *generator) createFile() {
- // file header, package clause, and imports
- g.p(`
- // Code generated by core/vm/gen; DO NOT EDIT.
-
- package vm
-
- import (
- "fmt"
-
- "github.com/ethereum/go-ethereum/common/math"
- "github.com/ethereum/go-ethereum/core/tracing"
- )
-
- `)
-
- // execUntraced: doc comment, loop-local declarations, and the dispatch loop
- g.p(`
- // execUntraced is the generated, tracing-free interpreter fast path. Hot,
- // fork-stable opcodes are inlined with their static gas and stack bounds baked
- // in. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
- // handler and gas functions directly by name. Everything fork-varying is
- // dispatched through the active per-fork table in the default case. EVM.Run
- // selects this path when no tracer is configured.
- func (evm *EVM) execUntraced(scope *ScopeContext) (ret []byte, err error) {
- var (
- contract = scope.Contract
- mem = scope.Memory
- stack = scope.Stack
- table = evm.table
- rules = evm.chainRules
- isEIP4762 = rules.IsEIP4762
- pc = uint64(0)
- res []byte
- )
- _ = mem
- _ = rules
- _ = isEIP4762
- _ = table
- mainLoop:
- for {
- `)
-
- // verkle code-chunk gas, spliced from chargeVerkleCodeChunkGas
- ccgFn := g.gasHelpers["chargeVerkleCodeChunkGas"]
- if ccgFn == nil {
- fatalf("no chargeVerkleCodeChunkGas gas helper to inline")
- }
- g.p("%s", g.rewriteGasReturns(g.renderAst(ccgFn.Body.List)))
-
- // fetch the opcode and open the dispatch switch
- g.p(`
- op := contract.GetOp(pc)
- switch op {
- `)
-
- // one case per inlined or direct-call opcode, in opcode order
- for code := 0; code < 256; code++ {
- b := byte(code)
- if _, named := inlineOps[b]; named {
- g.emitInlineOp(b)
- } else if _, dc := directCallOps[b]; dc {
- g.emitDirectCallOp(b)
- }
- }
-
- // the default case: fork-varying ops via the per-fork table
- g.emitDefault()
-
- // close the switch and loop, clear the stop token, and return
- g.p(`
- }
- }
- if err == errStopToken {
- err = nil
- }
- return res, err
- }
- `)
-}
-
-func fatalf(format string, args ...any) {
- fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
- os.Exit(1)
-}
-
func main() {
- _, self, _, ok := runtime.Caller(0)
- if !ok {
- fatalf("cannot resolve generator source path")
- }
- vmDir := filepath.Dir(filepath.Dir(self)) // .../core/vm/gen -> .../core/vm
-
- fset, opcodeHandlers, stackHelpers, gasHelpers := parseHandlers(vmDir)
- g := &generator{fset: fset, opcodeHandlers: opcodeHandlers, stackHelpers: stackHelpers, gasHelpers: gasHelpers, buf: new(bytes.Buffer)}
- g.deriveSpecs(vm.GenForks())
- g.createFile()
-
- formatted, err := format.Source(g.buf.Bytes())
+ formatted, err := generate()
if err != nil {
- dbg := filepath.Join(vmDir, "interpreter_gen.go.broken")
- os.WriteFile(dbg, g.buf.Bytes(), 0644)
- fatalf("gofmt failed (%v); wrote unformatted output to %s", err, dbg)
- }
- // INTERPRETER_GEN_OUT lets the CI-match test (interpreter_gen_test.go) regenerate to a
- // temporary file and diff it against the committed one, without clobbering it.
- out := filepath.Join(vmDir, "interpreter_gen.go")
- if env := os.Getenv("INTERPRETER_GEN_OUT"); env != "" {
- out = env
+ fatalf("%v", err)
}
+ out := filepath.Join(vmDir(), "interpreter_gen.go")
if err := os.WriteFile(out, formatted, 0644); err != nil {
fatalf("write %s: %v", out, err)
}
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index 86589428a6..166641a183 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -133,7 +133,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
mem.Free()
}()
- // The generated fast path bakes its fork gates at generate time, so it
+ // The generated fast path resolves its fork gates at generate time, so it
// cannot see table changes made by ExtraEips. Those configs run the table
// loop, as do tracing and the differential test via forceTableLoop.
if evm.forceTableLoop || len(evm.Config.ExtraEips) > 0 || evm.Config.Tracer != nil {
diff --git a/core/vm/interpreter_gen.go b/core/vm/interpreter_gen.go
index 252b69784f..f0c14906f1 100644
--- a/core/vm/interpreter_gen.go
+++ b/core/vm/interpreter_gen.go
@@ -10,8 +10,8 @@ import (
)
// execUntraced is the generated, tracing-free interpreter fast path. Hot,
-// fork-stable opcodes are inlined with their static gas and stack bounds baked
-// in. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
+// fork-stable opcodes are inlined with their static gas and stack bounds emitted
+// as constants. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
// handler and gas functions directly by name. Everything fork-varying is
// dispatched through the active per-fork table in the default case. EVM.Run
// selects this path when no tracer is configured.
diff --git a/core/vm/interpreter_gen_test.go b/core/vm/interpreter_gen_test.go
index 786dd14338..b01bba1cd1 100644
--- a/core/vm/interpreter_gen_test.go
+++ b/core/vm/interpreter_gen_test.go
@@ -16,9 +16,10 @@
package vm
-// Tests for the generated interpreter dispatch (interpreter_gen.go): that the
-// committed file is up to date, that it behaves identically to the table loop,
-// and that the fast path keeps its cheap stack helpers inlined.
+// Tests for the generated interpreter dispatch (interpreter_gen.go): that it
+// behaves identically to the table loop, and that the fast path keeps its cheap
+// stack helpers inlined. The check that the committed file matches the generator
+// output lives with the generator, in core/vm/gen.
import (
"bytes"
@@ -26,9 +27,7 @@ import (
"go/parser"
"go/token"
"math/big"
- "os"
"os/exec"
- "path/filepath"
"regexp"
"strings"
"testing"
@@ -41,33 +40,6 @@ import (
"github.com/holiman/uint256"
)
-// TestGeneratedDispatchUpToDate asserts that the committed interpreter_gen.go matches
-// what `go generate` (core/vm/gen) produces from the current opcode/gas/fork
-// definitions. It is the CI guard against hand-edits to the generated file and
-// against the generator drifting from the committed output.
-func TestGeneratedDispatchUpToDate(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping generator round-trip in -short mode")
- }
- tmp := filepath.Join(t.TempDir(), "interpreter_gen.go")
- cmd := exec.Command("go", "run", "./gen")
- cmd.Env = append(os.Environ(), "INTERPRETER_GEN_OUT="+tmp)
- if out, err := cmd.CombinedOutput(); err != nil {
- t.Fatalf("running generator: %v\n%s", err, out)
- }
- got, err := os.ReadFile(tmp)
- if err != nil {
- t.Fatalf("reading regenerated output: %v", err)
- }
- want, err := os.ReadFile("interpreter_gen.go")
- if err != nil {
- t.Fatalf("reading committed interpreter_gen.go: %v", err)
- }
- if string(got) != string(want) {
- t.Fatalf("interpreter_gen.go is out of date; run `go generate ./core/vm/...` and commit the result")
- }
-}
-
// Differential test comparing the table loop against the generated dispatch.
//
// These tests prove that the generated dispatch (execUntraced) is bit-identical
@@ -84,7 +56,7 @@ func TestGeneratedDispatchUpToDate(t *testing.T) {
// the tracer test suites instead.
// diffForks is the set of fork configurations the diff test runs every program
-// under. Spanning forks exercises the baked runtime fork gates (e.g. SHL from
+// under. Spanning forks exercises the generated fork gates (e.g. SHL from
// Constantinople, PUSH0 from Shanghai, CLZ from Osaka) in both the active and
// the not-yet-activated states.
var diffForks = func() []struct {
@@ -318,7 +290,7 @@ func diffBlockCtx(merged bool) BlockContext {
// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even
// when they touch opcodes the generated dispatch inlines. PUSH0 (EIP-3855) on
// a pre-Shanghai config is the canary: the runtime table has it enabled but
-// the baked fork gate does not, so execution must route through the table loop.
+// the generated fork gate does not, so execution must route through the table loop.
func TestExtraEIPs(t *testing.T) {
code := asm(PUSH0, STOP)
statedb := newDiffState(t)