cmd/easm: basic compiler

[ci skip]
This commit is contained in:
Jeffrey Wilcke 2017-02-16 23:48:22 +01:00
parent 119228a9ec
commit c995328881
No known key found for this signature in database
GPG key ID: 63FF149CD6945F9E
5 changed files with 376 additions and 35 deletions

View file

@ -0,0 +1,8 @@
jump @main
push 1
other:
push 0
push 0
mstore
main:
push 1

View 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

View file

@ -0,0 +1,3 @@
push 1
push 2
add

View file

@ -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)
} }

View file

@ -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,
} }
} }