Skip to content

Commit e3146cc

Browse files
authored
all: implement EIP-8024: backward compatible SWAPN, DUPN, EXCHANGE (#24)
This implements [EIP-8024](https://eips.ethereum.org/EIPS/eip-8024), adding three new stack manipulation instructions. The EIP is currently scheduled for the "Amsterdam" execution-layer fork of Ethereum. These instructions are followed by an immediate argument, which is a new concept in geas. I have chosen the `[x]` syntax for immediates, i.e. ``` swapn[17] exchange[1, 22] ``` I'm not sure if there is a need for being able to write the immediate argument in its encoded form. The encoding function is not straightforward, but it is of course reversible (for valid inputs), and the disassembler currently also shows the decoded form.
1 parent 43900cc commit e3146cc

File tree

13 files changed

+473
-29
lines changed

13 files changed

+473
-29
lines changed

asm/compiler.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,16 +396,20 @@ loop:
396396
c.errorAt(inst.ast, fmt.Errorf("%w %s", ecUnknownOpcode, inst.op))
397397
continue loop
398398
}
399+
output = append(output, op.Code)
400+
if len(inst.data) > 0 && !op.HasImmediate {
401+
panic(fmt.Sprintf("BUG: instruction at pc=%d has unexpected data", inst.pc))
402+
}
403+
output = append(output, inst.data...)
399404
// Unreachable code check.
400405
if !c.errors.hasError() {
401406
unreachable.check(c, inst.ast, op)
402407
}
403-
output = append(output, op.Code)
404-
fallthrough
405408

406409
default:
410+
// Empty op.
407411
if len(inst.data) > 0 {
408-
panic(fmt.Sprintf("BUG: instruction at pc=%d has unexpected data", inst.pc))
412+
panic(fmt.Sprintf("BUG: empty instruction at pc=%d has unexpected data", inst.pc))
409413
}
410414
}
411415
}

asm/compiler_expand.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,31 @@ func (op opcodeStatement) expand(c *Compiler, doc *ast.Document, prog *compilerP
8181
}
8282

8383
default:
84-
if _, err := prog.resolveOp(opcode); err != nil {
84+
evmOp, err := prog.resolveOp(opcode)
85+
if err != nil {
8586
return err
8687
}
88+
// Arg is not expected.
8789
if op.Arg != nil {
8890
if opcode == "PUSH0" {
8991
return ecPushzeroWithArgument
9092
}
9193
return ecUnexpectedArgument
9294
}
95+
// Handle immediates.
96+
if evmOp.HasImmediate {
97+
if len(op.Immediates) == 0 {
98+
return ecMissingImmediate
99+
}
100+
imm, err := evmOp.EncodeImmediateArgs(op.Immediates)
101+
if err != nil {
102+
return fmt.Errorf("%s %v", evmOp.Name, err)
103+
}
104+
inst.data = []byte{imm}
105+
inst.dataSize = 1
106+
} else if len(op.Immediates) > 0 {
107+
return ecUnexpectedImmediate
108+
}
93109
}
94110

95111
prog.addInstruction(inst)

asm/error.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ const (
6161
ecPragmaTargetInIncludeFile
6262
ecPragmaTargetConflict
6363
ecPragmaTargetUnknown
64+
ecMissingImmediate
65+
ecUnexpectedImmediate
6466
)
6567

6668
func (e compilerError) Error() string {
@@ -115,6 +117,10 @@ func (e compilerError) Error() string {
115117
return "duplicate '#pragma target ...' directive"
116118
case ecPragmaTargetUnknown:
117119
return "unknown #pragma target"
120+
case ecMissingImmediate:
121+
return "missing immediate for opcode"
122+
case ecUnexpectedImmediate:
123+
return "unexpected immediate"
118124
default:
119125
return fmt.Sprintf("invalid error %d", e)
120126
}

asm/testdata/compiler-tests.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,56 @@ instr-macro-variable-shadow:
847847
output:
848848
bytecode: "6001 6002"
849849

850+
immediates:
851+
input:
852+
code: |
853+
dupn[17]
854+
dupn[18]
855+
add
856+
exchange[2, 3]
857+
exchange[1, 19]
858+
sub
859+
swapn[19]
860+
swapn[108]
861+
#pragma target "amsterdam"
862+
output:
863+
bytecode: "e600 e601 01 e812 e8d0 03 e702 e780"
864+
865+
immediate-swapn-range-errors:
866+
input:
867+
code: |
868+
swapn[2]
869+
swapn[245]
870+
#pragma target "amsterdam"
871+
output:
872+
errors:
873+
- ':1: SWAPN stack depth 2 out of range (17-235)'
874+
- ':2: SWAPN stack depth 245 out of range (17-235)'
875+
876+
immediate-dupn-range-errors:
877+
input:
878+
code: |
879+
dupn[2]
880+
dupn[245]
881+
#pragma target "amsterdam"
882+
output:
883+
errors:
884+
- ':1: DUPN stack depth 2 out of range (17-235)'
885+
- ':2: DUPN stack depth 245 out of range (17-235)'
886+
887+
immediate-exchange-range-errors:
888+
input:
889+
code: |
890+
exchange[14, 15]
891+
exchange[1, 30]
892+
exchange[13, 29]
893+
#pragma target "amsterdam"
894+
output:
895+
errors:
896+
- ':1: EXCHANGE first position 14 out of range (1-13)'
897+
- ':2: EXCHANGE second position 30 out of range (1-29)'
898+
- ':3: EXCHANGE invalid positions 13, 29 (n+m > 30)'
899+
850900
opcode-unknown-in-fork:
851901
input:
852902
code: |

disasm/disassembler.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ func (d *Disassembler) Disassemble(bytecode []byte, outW io.Writer) error {
9393
if op.Push {
9494
size := d.printPush(out, op, bytecode[pc:])
9595
pc += size
96+
} else if op.HasImmediate {
97+
size := d.printImmediate(out, op, bytecode[pc:])
98+
pc += size
9699
} else {
97100
d.printOp(out, op)
98101
}
@@ -143,6 +146,30 @@ func (d *Disassembler) printPush(out io.Writer, op *evm.Op, code []byte) (dataSi
143146
return len(data)
144147
}
145148

149+
func (d *Disassembler) printImmediate(out io.Writer, op *evm.Op, code []byte) (dataSize int) {
150+
if len(code) < 2 {
151+
// Truncated instruction
152+
fmt.Fprintf(out, "#bytes %#x", code)
153+
return len(code) - 1
154+
}
155+
imm := code[1]
156+
if !op.ValidateImmediate(imm) {
157+
fmt.Fprintf(out, "#bytes %#x ; invalid %s", code[0], op.Name)
158+
return 0
159+
}
160+
d.printOp(out, op)
161+
args := op.DecodeImmediate(imm)
162+
fmt.Fprint(out, "[")
163+
for i, arg := range args {
164+
if i > 0 {
165+
fmt.Fprint(out, ", ")
166+
}
167+
fmt.Fprintf(out, "%d", arg)
168+
}
169+
fmt.Fprint(out, "]")
170+
return 1
171+
}
172+
146173
func (d *Disassembler) newline(out io.Writer, prevOp *evm.Op, nextOp *evm.Op) {
147174
if prevOp == nil {
148175
return

disasm/disassembler_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,77 @@ import (
2525
"github.com/fjl/geas/asm"
2626
)
2727

28+
func TestImmediateOpcodes(t *testing.T) {
29+
// Test EIP-8024 opcodes: DUPN (0xe6), SWAPN (0xe7), EXCHANGE (0xe8)
30+
bytecode, _ := hex.DecodeString("e600e780e812e8d0")
31+
expectedOutput := strings.TrimSpace(`
32+
dupn[17]
33+
swapn[108]
34+
exchange[2, 3]
35+
exchange[1, 19]
36+
`)
37+
38+
var buf strings.Builder
39+
d := New()
40+
d.SetShowBlocks(false)
41+
d.SetTarget("amsterdam")
42+
d.Disassemble(bytecode, &buf)
43+
output := strings.TrimSpace(buf.String())
44+
if output != expectedOutput {
45+
t.Fatalf("wrong output:\ngot:\n%s\n\nwant:\n%s", output, expectedOutput)
46+
}
47+
48+
// try round trip
49+
a := asm.New(nil)
50+
a.SetDefaultFork("amsterdam")
51+
rtcode := a.CompileString(output)
52+
if !bytes.Equal(rtcode, bytecode) {
53+
t.Error("disassembly did not round-trip")
54+
}
55+
}
56+
57+
func TestImmediateOpcodeTruncated(t *testing.T) {
58+
bytecode, _ := hex.DecodeString("e6")
59+
expectedOutput := "#bytes 0xe6"
60+
61+
var buf strings.Builder
62+
d := New()
63+
d.SetShowBlocks(false)
64+
d.SetTarget("amsterdam")
65+
d.Disassemble(bytecode, &buf)
66+
output := strings.TrimSpace(buf.String())
67+
if output != expectedOutput {
68+
t.Fatalf("wrong output:\ngot: %s\nwant: %s", output, expectedOutput)
69+
}
70+
}
71+
72+
// This checks that the disassembler can handle immediate opcodes which are not working.
73+
func TestImmediateOpcodeInvalid(t *testing.T) {
74+
bytecode, _ := hex.DecodeString("e75be6605be7610000e65fe850")
75+
expectedOutput := strings.TrimSpace(`
76+
#bytes 0xe7 ; invalid SWAPN
77+
jumpdest
78+
#bytes 0xe6 ; invalid DUPN
79+
push1 0x5b
80+
#bytes 0xe7 ; invalid SWAPN
81+
push2 0x0000
82+
#bytes 0xe6 ; invalid DUPN
83+
push0
84+
#bytes 0xe8 ; invalid EXCHANGE
85+
pop
86+
`)
87+
88+
var buf strings.Builder
89+
d := New()
90+
d.SetShowBlocks(false)
91+
d.SetTarget("amsterdam")
92+
d.Disassemble(bytecode, &buf)
93+
output := strings.TrimSpace(buf.String())
94+
if output != expectedOutput {
95+
t.Fatalf("wrong output:\ngot: %s\nwant: %s", output, expectedOutput)
96+
}
97+
}
98+
2899
func TestIncompletePush(t *testing.T) {
29100
bytecode, _ := hex.DecodeString("6080604052348015600e575f80fd5b50603e80601a5f395ff3fe60806040525f80fdfea2646970667358221220ba4339602dd535d09d71fae3164f7aa7f6e098ec879fc9e8f36bd912d4877c5264736f6c63430008190033")
30101
expectedOutput := strings.TrimSpace(`

internal/ast/ast.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,10 @@ func (st *stbase) StartsBlock() bool {
178178
type (
179179
Opcode struct {
180180
stbase
181-
Op string
182-
Arg Expr // Immediate argument for PUSH* / JUMP*.
183-
PushSize byte // For PUSH<n>, this is n+1.
181+
Op string
182+
Arg Expr // Immediate argument for PUSH* / JUMP*.
183+
Immediates []int // Immediate arguments in brackets, e.g. dupn[x], exchange[x, y].
184+
PushSize byte // For PUSH<n>, this is n+1.
184185
}
185186

186187
LabelDef struct {

internal/ast/lexer.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const (
7070
instMacroIdent // macro identifier
7171
openBrace // open brace
7272
closeBrace // closing brace
73+
openBracket // open bracket
74+
closeBracket // close bracket
7375
equals // equals sign
7476
arith // arithmetic operation
7577
comment // comment
@@ -213,6 +215,14 @@ func lexNext(l *lexer) stateFn {
213215
l.emit(closeBrace)
214216
return lexNext
215217

218+
case r == '[':
219+
l.emit(openBracket)
220+
return lexNext
221+
222+
case r == ']':
223+
l.emit(closeBracket)
224+
return lexNext
225+
216226
case r == ',':
217227
l.emit(comma)
218228
return lexNext

internal/ast/parse.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,13 @@ func parseOpcode(p *Parser, tok token) *Opcode {
469469
st.PushSize = byte(size + 1)
470470
}
471471

472-
// Parse optional argument.
472+
// Parse optional immediates in brackets.
473473
argToken := p.next()
474+
if argToken.typ == openBracket {
475+
st.Immediates = parseImmediates(p)
476+
argToken = p.next()
477+
}
478+
// Parse optional argument.
474479
switch argToken.typ {
475480
case lineEnd, eof, comment:
476481
p.unread(argToken)
@@ -480,6 +485,38 @@ func parseOpcode(p *Parser, tok token) *Opcode {
480485
return st
481486
}
482487

488+
func parseImmediates(p *Parser) []int {
489+
const limit = 2 // how many immediates allowed
490+
491+
var args []int
492+
for {
493+
switch tok := p.next(); tok.typ {
494+
case numberLiteral:
495+
n, _ := lzint.ParseNumberLiteral(tok.text)
496+
if n.IntegerBitLen() > 8 {
497+
p.throwError(tok, "immediate value > 8 bits")
498+
}
499+
args = append(args, int(n.Int().Int64()))
500+
case lineEnd, eof, comment:
501+
p.throwError(tok, "unexpected end of immediates")
502+
default:
503+
p.throwError(tok, "expected number in immediates list")
504+
}
505+
switch tok := p.next(); tok.typ {
506+
case closeBracket:
507+
return args
508+
case comma:
509+
if len(args) >= limit {
510+
p.throwError(tok, "too many immediates")
511+
}
512+
case lineEnd, eof, comment:
513+
p.throwError(tok, "unexpected end of immediates")
514+
default:
515+
p.throwError(tok, "expected ',' or ']'")
516+
}
517+
}
518+
}
519+
483520
var sizedPushRE = regexp.MustCompile("(?i)^PUSH([0-9]*)$")
484521

485522
func parsePushSize(name string) (int, bool) {

internal/ast/tokentype_string.go

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)