mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 03:10:48 +00:00
core/vm, core/vm/gen: mimic rlpgen's pattern and apply more cleanup
This commit is contained in:
parent
2756eedd27
commit
af039e9a16
6 changed files with 1005 additions and 956 deletions
948
core/vm/gen/gen.go
Normal file
948
core/vm/gen/gen.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 <err>` 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) // <recv>.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 <err>` 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
|
||||||
|
}
|
||||||
42
core/vm/gen/gen_test.go
Normal file
42
core/vm/gen/gen_test.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,938 +15,25 @@
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Command gen generates core/vm/interpreter_gen.go, the EVM interpreter's untraced
|
// 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:
|
// 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
|
||||||
// - inlines the hot, fork-stable opcodes (arithmetic / comparison / bitwise /
|
// hand-edit interpreter_gen.go.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Usage: go generate ./core/vm/...
|
// Usage: go generate ./core/vm/...
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
|
||||||
"go/format"
|
|
||||||
"go/parser"
|
|
||||||
"go/printer"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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 <err>` 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) // <recv>.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 <err>` 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() {
|
func main() {
|
||||||
_, self, _, ok := runtime.Caller(0)
|
formatted, err := generate()
|
||||||
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())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dbg := filepath.Join(vmDir, "interpreter_gen.go.broken")
|
fatalf("%v", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
out := filepath.Join(vmDir(), "interpreter_gen.go")
|
||||||
if err := os.WriteFile(out, formatted, 0644); err != nil {
|
if err := os.WriteFile(out, formatted, 0644); err != nil {
|
||||||
fatalf("write %s: %v", out, err)
|
fatalf("write %s: %v", out, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
|
||||||
mem.Free()
|
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
|
// cannot see table changes made by ExtraEips. Those configs run the table
|
||||||
// loop, as do tracing and the differential test via forceTableLoop.
|
// loop, as do tracing and the differential test via forceTableLoop.
|
||||||
if evm.forceTableLoop || len(evm.Config.ExtraEips) > 0 || evm.Config.Tracer != nil {
|
if evm.forceTableLoop || len(evm.Config.ExtraEips) > 0 || evm.Config.Tracer != nil {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// execUntraced is the generated, tracing-free interpreter fast path. Hot,
|
// execUntraced is the generated, tracing-free interpreter fast path. Hot,
|
||||||
// fork-stable opcodes are inlined with their static gas and stack bounds baked
|
// fork-stable opcodes are inlined with their static gas and stack bounds emitted
|
||||||
// in. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
|
// as constants. Fork-invariant ops (KECCAK256/MLOAD/MSTORE/MSTORE8) call their
|
||||||
// handler and gas functions directly by name. Everything fork-varying is
|
// handler and gas functions directly by name. Everything fork-varying is
|
||||||
// dispatched through the active per-fork table in the default case. EVM.Run
|
// dispatched through the active per-fork table in the default case. EVM.Run
|
||||||
// selects this path when no tracer is configured.
|
// selects this path when no tracer is configured.
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@
|
||||||
|
|
||||||
package vm
|
package vm
|
||||||
|
|
||||||
// Tests for the generated interpreter dispatch (interpreter_gen.go): that the
|
// Tests for the generated interpreter dispatch (interpreter_gen.go): that it
|
||||||
// committed file is up to date, that it behaves identically to the table loop,
|
// behaves identically to the table loop, and that the fast path keeps its cheap
|
||||||
// and that the fast path keeps its cheap stack helpers inlined.
|
// stack helpers inlined. The check that the committed file matches the generator
|
||||||
|
// output lives with the generator, in core/vm/gen.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -26,9 +27,7 @@ import (
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -41,33 +40,6 @@ import (
|
||||||
"github.com/holiman/uint256"
|
"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.
|
// Differential test comparing the table loop against the generated dispatch.
|
||||||
//
|
//
|
||||||
// These tests prove that the generated dispatch (execUntraced) is bit-identical
|
// 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.
|
// the tracer test suites instead.
|
||||||
|
|
||||||
// diffForks is the set of fork configurations the diff test runs every program
|
// 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
|
// Constantinople, PUSH0 from Shanghai, CLZ from Osaka) in both the active and
|
||||||
// the not-yet-activated states.
|
// the not-yet-activated states.
|
||||||
var diffForks = func() []struct {
|
var diffForks = func() []struct {
|
||||||
|
|
@ -318,7 +290,7 @@ func diffBlockCtx(merged bool) BlockContext {
|
||||||
// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even
|
// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even
|
||||||
// when they touch opcodes the generated dispatch inlines. PUSH0 (EIP-3855) on
|
// 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
|
// 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) {
|
func TestExtraEIPs(t *testing.T) {
|
||||||
code := asm(PUSH0, STOP)
|
code := asm(PUSH0, STOP)
|
||||||
statedb := newDiffState(t)
|
statedb := newDiffState(t)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue