mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 14:46:42 +00:00
cmd/easm: basic compiler
[ci skip]
This commit is contained in:
parent
119228a9ec
commit
c995328881
5 changed files with 376 additions and 35 deletions
8
cmd/easm/examples/jump.asm
Normal file
8
cmd/easm/examples/jump.asm
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
jump @main
|
||||||
|
push 1
|
||||||
|
other:
|
||||||
|
push 0
|
||||||
|
push 0
|
||||||
|
mstore
|
||||||
|
main:
|
||||||
|
push 1
|
||||||
17
cmd/easm/examples/prog.asm
Normal file
17
cmd/easm/examples/prog.asm
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
jump @main
|
||||||
|
my_label:
|
||||||
|
push 0
|
||||||
|
push 1
|
||||||
|
mstore
|
||||||
|
|
||||||
|
push 0
|
||||||
|
push 5
|
||||||
|
push 10
|
||||||
|
log1
|
||||||
|
stop
|
||||||
|
main:
|
||||||
|
push 1
|
||||||
|
push 1
|
||||||
|
eq
|
||||||
|
|
||||||
|
jumpi @my_label
|
||||||
3
cmd/easm/examples/push.asm
Normal file
3
cmd/easm/examples/push.asm
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
push 1
|
||||||
|
push 2
|
||||||
|
add
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
@ -31,6 +32,7 @@ const (
|
||||||
invalidStatement // any invalid statement
|
invalidStatement // any invalid statement
|
||||||
element // any element during element parsing
|
element // any element during element parsing
|
||||||
label // label is emitted when a labal is found
|
label // label is emitted when a labal is found
|
||||||
|
labelDef // label definition is emitted when a new label is found
|
||||||
number // number is emitted when a number is found
|
number // number is emitted when a number is found
|
||||||
stringValue // stringValue is emitted when a string has been found
|
stringValue // stringValue is emitted when a string has been found
|
||||||
|
|
||||||
|
|
@ -48,12 +50,13 @@ func (it itemType) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringItemTypes = []string{
|
var stringItemTypes = []string{
|
||||||
eof: "eof",
|
eof: "EOF",
|
||||||
invalidStatement: "invalid statement",
|
invalidStatement: "invalid statement",
|
||||||
element: "element",
|
element: "element",
|
||||||
lineEnd: "bol",
|
lineEnd: "end of line",
|
||||||
lineStart: "eol",
|
lineStart: "new line",
|
||||||
label: "label",
|
label: "label",
|
||||||
|
labelDef: "label definition",
|
||||||
number: "number",
|
number: "number",
|
||||||
stringValue: "string",
|
stringValue: "string",
|
||||||
}
|
}
|
||||||
|
|
@ -69,21 +72,26 @@ type Lexer struct {
|
||||||
|
|
||||||
lineno int // current line number in the source file
|
lineno int // current line number in the source file
|
||||||
start, pos, width int // positions for lexing and returning value
|
start, pos, width int // positions for lexing and returning value
|
||||||
|
|
||||||
|
debug bool // flag for triggering debug output
|
||||||
}
|
}
|
||||||
|
|
||||||
// lex lexes the program by name with the given source. It returns a
|
// lex lexes the program by name with the given source. It returns a
|
||||||
// channel on which the items are delivered.
|
// channel on which the items are delivered.
|
||||||
func lex(name, source string) chan item {
|
func lex(name string, source []byte, debug bool) <-chan item {
|
||||||
ch := make(chan item)
|
ch := make(chan item)
|
||||||
l := &Lexer{
|
l := &Lexer{
|
||||||
input: source,
|
input: string(source),
|
||||||
items: ch,
|
items: ch,
|
||||||
state: lexLine,
|
state: lexLine,
|
||||||
|
debug: debug,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
l.emit(lineStart)
|
||||||
for l.state != nil {
|
for l.state != nil {
|
||||||
l.state = l.state(l)
|
l.state = l.state(l)
|
||||||
}
|
}
|
||||||
|
l.emit(eof)
|
||||||
close(l.items)
|
close(l.items)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -157,19 +165,26 @@ func (l *Lexer) blob() string {
|
||||||
|
|
||||||
// Emits a new item on to item channel for processing
|
// Emits a new item on to item channel for processing
|
||||||
func (l *Lexer) emit(t itemType) {
|
func (l *Lexer) emit(t itemType) {
|
||||||
l.items <- item{t, l.lineno, l.blob()}
|
item := item{t, l.lineno, l.blob()}
|
||||||
|
|
||||||
|
if l.debug {
|
||||||
|
fmt.Printf("%04d: (%-20v) %s\n", item.lineno, item.typ, item.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.items <- item
|
||||||
l.start = l.pos
|
l.start = l.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexLine is state function for lexing lines
|
// lexLine is state function for lexing lines
|
||||||
func lexLine(l *Lexer) stateFn {
|
func lexLine(l *Lexer) stateFn {
|
||||||
l.emit(lineStart)
|
|
||||||
for {
|
for {
|
||||||
switch r := l.next(); {
|
switch r := l.next(); {
|
||||||
case r == '\n':
|
case r == '\n':
|
||||||
l.lineno++
|
|
||||||
l.ignore()
|
|
||||||
l.emit(lineEnd)
|
l.emit(lineEnd)
|
||||||
|
l.ignore()
|
||||||
|
l.lineno++
|
||||||
|
|
||||||
|
l.emit(lineStart)
|
||||||
case isSpace(r):
|
case isSpace(r):
|
||||||
l.ignore()
|
l.ignore()
|
||||||
case isAlphaNumeric(r) || r == '_':
|
case isAlphaNumeric(r) || r == '_':
|
||||||
|
|
@ -177,6 +192,7 @@ func lexLine(l *Lexer) stateFn {
|
||||||
case isNumber(r):
|
case isNumber(r):
|
||||||
return lexNumber
|
return lexNumber
|
||||||
case r == '@':
|
case r == '@':
|
||||||
|
l.ignore()
|
||||||
return lexLabel
|
return lexLabel
|
||||||
case r == '"':
|
case r == '"':
|
||||||
return lexInsideString
|
return lexInsideString
|
||||||
|
|
@ -221,10 +237,13 @@ func lexNumber(l *Lexer) stateFn {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lexElement(l *Lexer) stateFn {
|
func lexElement(l *Lexer) stateFn {
|
||||||
l.acceptRun(Alpha + "_")
|
l.acceptRun(Alpha + "_" + Numbers)
|
||||||
|
|
||||||
if l.accept(":") {
|
if l.peek() == ':' {
|
||||||
l.emit(label)
|
l.emit(labelDef)
|
||||||
|
|
||||||
|
l.accept(":")
|
||||||
|
l.ignore()
|
||||||
} else {
|
} else {
|
||||||
l.emit(element)
|
l.emit(element)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
332
cmd/easm/main.go
332
cmd/easm/main.go
|
|
@ -1,29 +1,323 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cli "gopkg.in/urfave/cli.v1"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
app *cli.App
|
||||||
|
|
||||||
|
DebugFlag = cli.BoolFlag{
|
||||||
|
Name: "debug",
|
||||||
|
Usage: "outputs lexer and compiler debug output",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
app = cli.NewApp()
|
||||||
|
app.Name = filepath.Base(os.Args[0])
|
||||||
|
app.Author = ""
|
||||||
|
app.Email = ""
|
||||||
|
app.Version = "0.1"
|
||||||
|
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
DebugFlag,
|
||||||
|
}
|
||||||
|
app.Action = run
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx *cli.Context) {
|
||||||
|
debug := ctx.GlobalBool(DebugFlag.Name)
|
||||||
|
|
||||||
|
if len(ctx.Args()) == 0 {
|
||||||
|
glog.Exitln("err: <filename> required")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := ctx.Args().First()
|
||||||
|
src, err := ioutil.ReadFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
glog.Exitln("err:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
compiler := newCompiler(debug)
|
||||||
|
compiler.feed(lex(fn, src, debug))
|
||||||
|
|
||||||
|
bin, errors := compiler.compile()
|
||||||
|
if len(errors) > 0 {
|
||||||
|
// report errors
|
||||||
|
for _, err := range errors {
|
||||||
|
got, want := err.Error()
|
||||||
|
fmt.Printf("%s:%d: syntax error: unexpected %v, expected %v\n", fn, err.Lineno(), got, want)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(bin)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
program := `
|
if err := app.Run(os.Args); err != nil {
|
||||||
jump @main
|
fmt.Fprintln(os.Stderr, err)
|
||||||
my_label:
|
os.Exit(1)
|
||||||
push 0
|
}
|
||||||
push "hello"
|
}
|
||||||
mstore
|
|
||||||
|
|
||||||
push 0
|
// Compiler contains information about the parsed source
|
||||||
push 5
|
// and holds the tokens for the program.
|
||||||
push 10
|
type Compiler struct {
|
||||||
log
|
tokens []item
|
||||||
|
binary []interface{}
|
||||||
|
|
||||||
main:
|
labels map[string]int
|
||||||
push 1
|
|
||||||
push 1
|
|
||||||
eq
|
|
||||||
|
|
||||||
jumpif @my_label
|
pc, pos int
|
||||||
`
|
|
||||||
ch := lex("program.asm", program)
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCompiler returns a new allocated compiler.
|
||||||
|
func newCompiler(debug bool) *Compiler {
|
||||||
|
return &Compiler{
|
||||||
|
labels: make(map[string]int),
|
||||||
|
debug: debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// feed feeds tokens in to ch and are interpreted by
|
||||||
|
// the compiler.
|
||||||
|
//
|
||||||
|
// feed is the first pass in the compile stage as it
|
||||||
|
// collect the used labels in the program and keeps a
|
||||||
|
// program counter which is used to determine the locations
|
||||||
|
// of the jump dests. The labels can than be used in the
|
||||||
|
// second stage to push labels and determine the right
|
||||||
|
// position.
|
||||||
|
func (c *Compiler) feed(ch <-chan item) {
|
||||||
for i := range ch {
|
for i := range ch {
|
||||||
fmt.Printf("%04d: (%-20v) %s\n", i.lineno, i.typ, i.text)
|
switch i.typ {
|
||||||
|
case number:
|
||||||
|
num := common.String2Big(i.text).Bytes()
|
||||||
|
if len(num) == 0 {
|
||||||
|
num = []byte{0}
|
||||||
|
}
|
||||||
|
c.pc += len(num)
|
||||||
|
case stringValue:
|
||||||
|
c.pc += len(i.text) - 2
|
||||||
|
case element:
|
||||||
|
c.pc++
|
||||||
|
case labelDef:
|
||||||
|
c.labels[i.text] = c.pc
|
||||||
|
c.pc++
|
||||||
|
case label:
|
||||||
|
c.pc += 5
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tokens = append(c.tokens, i)
|
||||||
|
}
|
||||||
|
if c.debug {
|
||||||
|
fmt.Println("found", len(c.labels), "labels")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile compiles the current tokens and returns a
|
||||||
|
// binary string that can be interpreted by the EVM
|
||||||
|
// and an error if it failed.
|
||||||
|
//
|
||||||
|
// compile is the second stage in the compile phase
|
||||||
|
// which compiles the tokens to EVM instructions.
|
||||||
|
func (c *Compiler) compile() (string, []cerror) {
|
||||||
|
var errors []cerror
|
||||||
|
// continue looping over the tokens until
|
||||||
|
// the stack has been exhausted.
|
||||||
|
for c.pos < len(c.tokens) {
|
||||||
|
if err := c.compileLine(); err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn the binary to hex
|
||||||
|
var bin string
|
||||||
|
for _, v := range c.binary {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case vm.OpCode:
|
||||||
|
bin += fmt.Sprintf("%x", []byte{byte(v)})
|
||||||
|
case []byte:
|
||||||
|
bin += fmt.Sprintf("%x", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bin, errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns the next token and increments the
|
||||||
|
// posititon.
|
||||||
|
func (c *Compiler) next() item {
|
||||||
|
token := c.tokens[c.pos]
|
||||||
|
c.pos++
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile line compiles a single line instruction e.g.
|
||||||
|
// "push 1", "jump @labal".
|
||||||
|
func (c *Compiler) compileLine() cerror {
|
||||||
|
n := c.next()
|
||||||
|
if n.typ != lineStart {
|
||||||
|
return compileErr(n, n.typ.String(), lineStart.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
lvalue := c.next()
|
||||||
|
switch lvalue.typ {
|
||||||
|
case eof:
|
||||||
|
return nil
|
||||||
|
case element:
|
||||||
|
if err := c.compileElement(lvalue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case labelDef:
|
||||||
|
c.compileLabel()
|
||||||
|
case lineEnd:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return compileErr(lvalue, lvalue.text, fmt.Sprintf("%v or %v", labelDef, element))
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := c.next(); n.typ != lineEnd {
|
||||||
|
return compileErr(n, n.text, lineEnd.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileNumber compiles the number to bytes
|
||||||
|
func (c *Compiler) compileNumber(element item) (int, error) {
|
||||||
|
num := common.String2Big(element.text).Bytes()
|
||||||
|
if len(num) == 0 {
|
||||||
|
num = []byte{0}
|
||||||
|
}
|
||||||
|
c.pushBin(num)
|
||||||
|
return len(num), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileElement compiles the element (push & label or both)
|
||||||
|
// to a binary representation and may error if incorrect statements
|
||||||
|
// where fed.
|
||||||
|
func (c *Compiler) compileElement(element item) cerror {
|
||||||
|
// check for a jump. jumps must be read and compiled
|
||||||
|
// from right to left.
|
||||||
|
if isJump(element.text) {
|
||||||
|
rvalue := c.next()
|
||||||
|
switch rvalue.typ {
|
||||||
|
case number:
|
||||||
|
// TODO figure out how to return the error properly
|
||||||
|
c.compileNumber(rvalue)
|
||||||
|
case stringValue:
|
||||||
|
// strings are quoted, remove them.
|
||||||
|
c.pushBin(rvalue.text[1 : len(rvalue.text)-2])
|
||||||
|
case label:
|
||||||
|
c.pushBin(vm.PUSH4)
|
||||||
|
pos := big.NewInt(int64(c.labels[rvalue.text])).Bytes()
|
||||||
|
pos = append(make([]byte, 4-len(pos)), pos...)
|
||||||
|
c.pushBin(pos)
|
||||||
|
default:
|
||||||
|
return compileErr(rvalue, rvalue.text, "number, string or label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// push the operation
|
||||||
|
c.pushBin(toBinary(element.text))
|
||||||
|
|
||||||
|
// handle pushes. pushes are read from left to right.
|
||||||
|
if isPush(element.text) {
|
||||||
|
rvalue := c.next()
|
||||||
|
switch rvalue.typ {
|
||||||
|
case number:
|
||||||
|
// TODO figure out how to return the error properly
|
||||||
|
c.compileNumber(rvalue)
|
||||||
|
case stringValue:
|
||||||
|
// strings are quoted, remove them.
|
||||||
|
c.pushBin(rvalue.text[1 : len(rvalue.text)-1])
|
||||||
|
case label:
|
||||||
|
c.pushBin(vm.PUSH4)
|
||||||
|
pos := make([]byte, 4)
|
||||||
|
copy(pos, big.NewInt(int64(c.labels[rvalue.text])).Bytes())
|
||||||
|
c.pushBin(pos)
|
||||||
|
default:
|
||||||
|
return compileErr(rvalue, rvalue.text, "number, string or label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileLabel pushes a jumpdest to the binary slice.
|
||||||
|
func (c *Compiler) compileLabel() cerror {
|
||||||
|
c.pushBin(vm.JUMPDEST)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushBin pushes the value v to the binary stack.
|
||||||
|
func (c *Compiler) pushBin(v interface{}) {
|
||||||
|
if c.debug {
|
||||||
|
fmt.Printf("%d: %v\n", len(c.binary), v)
|
||||||
|
}
|
||||||
|
c.binary = append(c.binary, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPush returns whether the string op is either any of
|
||||||
|
// push(N).
|
||||||
|
func isPush(op string) bool {
|
||||||
|
if op == "push" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isJump returns whether the string op is jump(i)
|
||||||
|
func isJump(op string) bool {
|
||||||
|
return op == "jumpi" || op == "jump"
|
||||||
|
}
|
||||||
|
|
||||||
|
// toBinary converts text to a vm.OpCode
|
||||||
|
func toBinary(text string) vm.OpCode {
|
||||||
|
if isPush(text) {
|
||||||
|
text = "push1"
|
||||||
|
}
|
||||||
|
return vm.StringToOp(strings.ToUpper(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
type cerror interface {
|
||||||
|
Error() (string, string)
|
||||||
|
Lineno() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type compileError struct {
|
||||||
|
got string
|
||||||
|
want string
|
||||||
|
|
||||||
|
lineno int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c compileError) Error() (string, string) { return c.got, c.want }
|
||||||
|
func (c compileError) Lineno() int { return c.lineno }
|
||||||
|
|
||||||
|
var (
|
||||||
|
errExpBol = errors.New("expected beginning of line")
|
||||||
|
errExpElementOrLabel = errors.New("expected beginning of line")
|
||||||
|
)
|
||||||
|
|
||||||
|
func compileErr(c item, got, want string) cerror {
|
||||||
|
return compileError{
|
||||||
|
got: got,
|
||||||
|
want: want,
|
||||||
|
lineno: c.lineno,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue