From e4af29508033ecd5945d6b5bffac86898ddfd909 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 12:24:03 +0300 Subject: [PATCH 1/8] compiler: process constant first in BinaryExpr handling --- pkg/compiler/codegen.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 8efdb57e4..71baf838b 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -689,6 +689,19 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil case *ast.BinaryExpr: + // The AST package will try to resolve all basic literals for us. + // If the typeinfo.Value is not nil we know that the expr is resolved + // and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6. + // NOTE: Constants will also be automatically resolved be the AST parser. + // example: + // const x = 10 + // x + 2 will results into 12 + tinfo := c.typeAndValueOf(n) + if tinfo.Value != nil { + c.emitLoadConst(tinfo) + return nil + } + switch n.Op { case token.LAND: end := c.newLabel() @@ -711,19 +724,6 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil default: - // The AST package will try to resolve all basic literals for us. - // If the typeinfo.Value is not nil we know that the expr is resolved - // and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6. - // NOTE: Constants will also be automatically resolved be the AST parser. - // example: - // const x = 10 - // x + 2 will results into 12 - tinfo := c.typeAndValueOf(n) - if tinfo.Value != nil { - c.emitLoadConst(tinfo) - return nil - } - var checkForNull bool if isExprNil(n.X) { From ae88c77a8a89aa6701a4f0ad6948ffa3991947ea Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 12:33:21 +0300 Subject: [PATCH 2/8] compiler: process `nil` comparisons separately --- pkg/compiler/codegen.go | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 71baf838b..4de352221 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -702,6 +702,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil } + if arg := c.getCompareWithNilArg(n); arg != nil { + ast.Walk(c, arg) + emit.Opcode(c.prog.BinWriter, opcode.ISNULL) + if n.Op == token.NEQ { + emit.Opcode(c.prog.BinWriter, opcode.NOT) + } + return nil + } + switch n.Op { case token.LAND: end := c.newLabel() @@ -724,27 +733,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil default: - var checkForNull bool - - if isExprNil(n.X) { - checkForNull = true - } else { - ast.Walk(c, n.X) - } - if isExprNil(n.Y) { - checkForNull = true - } else { - ast.Walk(c, n.Y) - } - if checkForNull { - emit.Opcode(c.prog.BinWriter, opcode.ISNULL) - if n.Op == token.NEQ { - emit.Opcode(c.prog.BinWriter, opcode.NOT) - } - - return nil - } - + ast.Walk(c, n.X) + ast.Walk(c, n.Y) switch { case n.Op == token.ADD: // VM has separate opcodes for number and string concatenation @@ -1149,6 +1139,15 @@ func isFallthroughStmt(c ast.Node) bool { return ok && s.Tok == token.FALLTHROUGH } +func (c *codegen) getCompareWithNilArg(n *ast.BinaryExpr) ast.Expr { + if isExprNil(n.X) { + return n.Y + } else if isExprNil(n.Y) { + return n.X + } + return nil +} + func (c *codegen) pushStackLabel(name string, size int) { c.labelList = append(c.labelList, labelWithStackSize{ name: name, From fd7af77895a3102c14ff678f6e5dab9e9d071de6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 12:39:58 +0300 Subject: [PATCH 3/8] compiler: refactor `convertToken` func Move `getEqualityOpcode` into `convertToken`. `convertToken` does not need codegen. --- pkg/compiler/codegen.go | 112 ++++++++++++++++------------------- pkg/compiler/codegen_test.go | 31 +++++++--- pkg/compiler/types.go | 5 ++ 3 files changed, 79 insertions(+), 69 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 4de352221..089660638 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -446,7 +446,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { // RHS can contain exactly one expression, thus there is no need to iterate. ast.Walk(c, n.Lhs[0]) ast.Walk(c, n.Rhs[0]) - c.convertToken(n.Tok) + c.emitToken(n.Tok, c.typeOf(n.Rhs[0])) } for i := 0; i < len(n.Lhs); i++ { switch t := n.Lhs[i].(type) { @@ -581,7 +581,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { case *ast.SwitchStmt: ast.Walk(c, n.Tag) - eqOpcode := c.getEqualityOpcode(n.Tag) + eqOpcode, _ := convertToken(token.EQL, c.typeOf(n.Tag)) switchEnd, label := c.generateLabel(labelEnd) lastSwitch := c.currentSwitch @@ -735,28 +735,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { default: ast.Walk(c, n.X) ast.Walk(c, n.Y) - switch { - case n.Op == token.ADD: - // VM has separate opcodes for number and string concatenation - if isString(tinfo.Type) { - emit.Opcode(c.prog.BinWriter, opcode.CAT) - } else { - emit.Opcode(c.prog.BinWriter, opcode.ADD) - } - case n.Op == token.EQL: - // VM has separate opcodes for number and string equality - op := c.getEqualityOpcode(n.X) - emit.Opcode(c.prog.BinWriter, op) - case n.Op == token.NEQ: - // VM has separate opcodes for number and string equality - if isString(c.typeOf(n.X)) { - emit.Opcode(c.prog.BinWriter, opcode.NOTEQUAL) - } else { - emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL) - } - default: - c.convertToken(n.Op) - } + c.emitToken(n.Op, c.typeOf(n.X)) return nil } @@ -925,7 +904,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { case *ast.IncDecStmt: ast.Walk(c, n.X) - c.convertToken(n.Tok) + c.emitToken(n.Tok, c.typeOf(n.X)) // For now only identifiers are supported for (post) for stmts. // for i := 0; i < 10; i++ {} @@ -1205,15 +1184,6 @@ func (c *codegen) getLabelOffset(typ labelOffsetType, name string) uint16 { return c.labels[labelWithType{name: name, typ: typ}] } -func (c *codegen) getEqualityOpcode(expr ast.Expr) opcode.Opcode { - t, ok := c.typeOf(expr).Underlying().(*types.Basic) - if ok && t.Info()&types.IsNumeric != 0 { - return opcode.NUMEQUAL - } - - return opcode.EQUAL -} - // getByteArray returns byte array value from constant expr. // Only literals are supported. func (c *codegen) getByteArray(expr ast.Expr) []byte { @@ -1459,59 +1429,79 @@ func (c *codegen) convertStruct(lit *ast.CompositeLit, ptr bool) { } } -func (c *codegen) convertToken(tok token.Token) { +func (c *codegen) emitToken(tok token.Token, typ types.Type) { + op, err := convertToken(tok, typ) + if err != nil { + c.prog.Err = err + return + } + emit.Opcode(c.prog.BinWriter, op) +} + +func convertToken(tok token.Token, typ types.Type) (opcode.Opcode, error) { switch tok { case token.ADD_ASSIGN: - emit.Opcode(c.prog.BinWriter, opcode.ADD) + return opcode.ADD, nil case token.SUB_ASSIGN: - emit.Opcode(c.prog.BinWriter, opcode.SUB) + return opcode.SUB, nil case token.MUL_ASSIGN: - emit.Opcode(c.prog.BinWriter, opcode.MUL) + return opcode.MUL, nil case token.QUO_ASSIGN: - emit.Opcode(c.prog.BinWriter, opcode.DIV) + return opcode.DIV, nil case token.REM_ASSIGN: - emit.Opcode(c.prog.BinWriter, opcode.MOD) + return opcode.MOD, nil case token.ADD: - emit.Opcode(c.prog.BinWriter, opcode.ADD) + // VM has separate opcodes for number and string concatenation + if isString(typ) { + return opcode.CAT, nil + } + return opcode.ADD, nil case token.SUB: - emit.Opcode(c.prog.BinWriter, opcode.SUB) + return opcode.SUB, nil case token.MUL: - emit.Opcode(c.prog.BinWriter, opcode.MUL) + return opcode.MUL, nil case token.QUO: - emit.Opcode(c.prog.BinWriter, opcode.DIV) + return opcode.DIV, nil case token.REM: - emit.Opcode(c.prog.BinWriter, opcode.MOD) + return opcode.MOD, nil case token.LSS: - emit.Opcode(c.prog.BinWriter, opcode.LT) + return opcode.LT, nil case token.LEQ: - emit.Opcode(c.prog.BinWriter, opcode.LTE) + return opcode.LTE, nil case token.GTR: - emit.Opcode(c.prog.BinWriter, opcode.GT) + return opcode.GT, nil case token.GEQ: - emit.Opcode(c.prog.BinWriter, opcode.GTE) + return opcode.GTE, nil case token.EQL: - emit.Opcode(c.prog.BinWriter, opcode.NUMEQUAL) + // VM has separate opcodes for number and string equality + if isNumber(typ) { + return opcode.NUMEQUAL, nil + } + return opcode.EQUAL, nil case token.NEQ: - emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL) + // VM has separate opcodes for number and string equality + if isNumber(typ) { + return opcode.NUMNOTEQUAL, nil + } + return opcode.NOTEQUAL, nil case token.DEC: - emit.Opcode(c.prog.BinWriter, opcode.DEC) + return opcode.DEC, nil case token.INC: - emit.Opcode(c.prog.BinWriter, opcode.INC) + return opcode.INC, nil case token.NOT: - emit.Opcode(c.prog.BinWriter, opcode.NOT) + return opcode.NOT, nil case token.AND: - emit.Opcode(c.prog.BinWriter, opcode.AND) + return opcode.AND, nil case token.OR: - emit.Opcode(c.prog.BinWriter, opcode.OR) + return opcode.OR, nil case token.SHL: - emit.Opcode(c.prog.BinWriter, opcode.SHL) + return opcode.SHL, nil case token.SHR: - emit.Opcode(c.prog.BinWriter, opcode.SHR) + return opcode.SHR, nil case token.XOR: - emit.Opcode(c.prog.BinWriter, opcode.XOR) + return opcode.XOR, nil default: - c.prog.Err = fmt.Errorf("compiler could not convert token: %s", tok) - return + return 0, fmt.Errorf("compiler could not convert token: %s", tok) } } diff --git a/pkg/compiler/codegen_test.go b/pkg/compiler/codegen_test.go index b499771d8..9245edef9 100644 --- a/pkg/compiler/codegen_test.go +++ b/pkg/compiler/codegen_test.go @@ -2,9 +2,9 @@ package compiler import ( "go/token" + "go/types" "testing" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/assert" ) @@ -14,59 +14,74 @@ func TestConvertToken(t *testing.T) { name string token token.Token opcode opcode.Opcode + typ types.Type } testCases := []testCase{ - {"ADD", + {"ADD (string)", token.ADD, opcode.ADD, + types.Typ[types.Int], + }, + {"ADD (number)", + token.ADD, + opcode.CAT, + types.Typ[types.String], }, {"SUB", token.SUB, opcode.SUB, + nil, }, {"MUL", token.MUL, opcode.MUL, + nil, }, {"QUO", token.QUO, opcode.DIV, + nil, }, {"REM", token.REM, opcode.MOD, + nil, }, {"ADD_ASSIGN", token.ADD_ASSIGN, opcode.ADD, + nil, }, {"SUB_ASSIGN", token.SUB_ASSIGN, opcode.SUB, + nil, }, {"MUL_ASSIGN", token.MUL_ASSIGN, opcode.MUL, + nil, }, {"QUO_ASSIGN", token.QUO_ASSIGN, opcode.DIV, + nil, }, {"REM_ASSIGN", token.REM_ASSIGN, opcode.MOD, + nil, }, } for _, tcase := range testCases { - t.Run(tcase.name, func(t *testing.T) { eval(t, tcase.token, tcase.opcode) }) + t.Run(tcase.name, func(t *testing.T) { eval(t, tcase.token, tcase.opcode, tcase.typ) }) } } -func eval(t *testing.T, token token.Token, opcode opcode.Opcode) { - codegen := &codegen{prog: io.NewBufBinWriter()} - codegen.convertToken(token) - readOpcode := codegen.prog.Bytes() - assert.Equal(t, []byte{byte(opcode)}, readOpcode) +func eval(t *testing.T, token token.Token, opcode opcode.Opcode, typ types.Type) { + op, err := convertToken(token, typ) + assert.NoError(t, err) + assert.Equal(t, opcode, op) } diff --git a/pkg/compiler/types.go b/pkg/compiler/types.go index 77bbcfc1c..e03dcf286 100644 --- a/pkg/compiler/types.go +++ b/pkg/compiler/types.go @@ -31,6 +31,11 @@ func isByte(typ types.Type) bool { return isBasicTypeOfKind(typ, types.Uint8, types.Int8) } +func isNumber(typ types.Type) bool { + t, ok := typ.Underlying().(*types.Basic) + return ok && t.Info()&types.IsNumeric != 0 +} + func isString(typ types.Type) bool { return isBasicTypeOfKind(typ, types.String) } From 59367c96d1b674a76c6e898c99dd8af4b2801d58 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 13:12:10 +0300 Subject: [PATCH 4/8] compiler: allow to use `+=` on strings --- pkg/compiler/assign_test.go | 10 ++++++++++ pkg/compiler/codegen.go | 12 +++++------- pkg/compiler/codegen_test.go | 13 +++++++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/pkg/compiler/assign_test.go b/pkg/compiler/assign_test.go index f521d3a4a..4c7007a0f 100644 --- a/pkg/compiler/assign_test.go +++ b/pkg/compiler/assign_test.go @@ -108,6 +108,16 @@ var assignTestCases = []testCase{ `, big.NewInt(15), }, + { + "add assign for string", + `package foo + func Main() string { + s := "Hello, " + s += "world!" + return s + }`, + []byte("Hello, world!"), + }, { "decl assign", ` diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 089660638..24669b72d 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1440,7 +1440,11 @@ func (c *codegen) emitToken(tok token.Token, typ types.Type) { func convertToken(tok token.Token, typ types.Type) (opcode.Opcode, error) { switch tok { - case token.ADD_ASSIGN: + case token.ADD_ASSIGN, token.ADD: + // VM has separate opcodes for number and string concatenation + if isString(typ) { + return opcode.CAT, nil + } return opcode.ADD, nil case token.SUB_ASSIGN: return opcode.SUB, nil @@ -1450,12 +1454,6 @@ func convertToken(tok token.Token, typ types.Type) (opcode.Opcode, error) { return opcode.DIV, nil case token.REM_ASSIGN: return opcode.MOD, nil - case token.ADD: - // VM has separate opcodes for number and string concatenation - if isString(typ) { - return opcode.CAT, nil - } - return opcode.ADD, nil case token.SUB: return opcode.SUB, nil case token.MUL: diff --git a/pkg/compiler/codegen_test.go b/pkg/compiler/codegen_test.go index 9245edef9..3de3dea7f 100644 --- a/pkg/compiler/codegen_test.go +++ b/pkg/compiler/codegen_test.go @@ -18,12 +18,12 @@ func TestConvertToken(t *testing.T) { } testCases := []testCase{ - {"ADD (string)", + {"ADD (number)", token.ADD, opcode.ADD, types.Typ[types.Int], }, - {"ADD (number)", + {"ADD (string)", token.ADD, opcode.CAT, types.Typ[types.String], @@ -48,10 +48,15 @@ func TestConvertToken(t *testing.T) { opcode.MOD, nil, }, - {"ADD_ASSIGN", + {"ADD_ASSIGN (number)", token.ADD_ASSIGN, opcode.ADD, - nil, + types.Typ[types.Int], + }, + {"ADD_ASSIGN (string)", + token.ADD_ASSIGN, + opcode.CAT, + types.Typ[types.String], }, {"SUB_ASSIGN", token.SUB_ASSIGN, From 1be1b8de9e4abe1725a149a776db623019ae7f64 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 13:29:48 +0300 Subject: [PATCH 5/8] compiler: merge `&&` and `||` processing --- pkg/compiler/codegen.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 24669b72d..44cee3b30 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -712,21 +712,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { } switch n.Op { - case token.LAND: + case token.LAND, token.LOR: end := c.newLabel() ast.Walk(c, n.X) - emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 1 + 5}) - emit.Opcode(c.prog.BinWriter, opcode.PUSHF) - emit.Jmp(c.prog.BinWriter, opcode.JMPL, end) - ast.Walk(c, n.Y) - c.setLabel(end) - return nil - - case token.LOR: - end := c.newLabel() - ast.Walk(c, n.X) - emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 1 + 5}) - emit.Opcode(c.prog.BinWriter, opcode.PUSHT) + if n.Op == token.LAND { + emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 1 + 5}) + emit.Opcode(c.prog.BinWriter, opcode.PUSHF) + } else { + emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 1 + 5}) + emit.Opcode(c.prog.BinWriter, opcode.PUSHT) + } emit.Jmp(c.prog.BinWriter, opcode.JMPL, end) ast.Walk(c, n.Y) c.setLabel(end) From 4d04c56efbdfc9b33d6148c96ba2b0dcd6f93742 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 13:56:55 +0300 Subject: [PATCH 6/8] compiler: make `toShortForm` accept an opcode --- pkg/compiler/codegen.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 44cee3b30..3b7771ddf 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1699,13 +1699,13 @@ func shortenJumps(b []byte, offsets []int) []byte { // 2. Convert instructions. copyOffset := 0 l := len(offsets) - b[offsets[0]] = toShortForm(b[offsets[0]]) + b[offsets[0]] = byte(toShortForm(opcode.Opcode(b[offsets[0]]))) for i := 0; i < l; i++ { start := offsets[i] + 2 end := len(b) if i != l-1 { end = offsets[i+1] + 2 - b[offsets[i+1]] = toShortForm(b[offsets[i+1]]) + b[offsets[i+1]] = byte(toShortForm(opcode.Opcode(b[offsets[i+1]]))) } copy(b[start-copyOffset:], b[start+3:end]) copyOffset += longToShortRemoveCount @@ -1731,28 +1731,28 @@ func calcOffsetCorrection(ip, target int, offsets []int) int { return cnt } -func toShortForm(b byte) byte { - switch op := opcode.Opcode(b); op { +func toShortForm(op opcode.Opcode) opcode.Opcode { + switch op { case opcode.JMPL: - return byte(opcode.JMP) + return opcode.JMP case opcode.JMPIFL: - return byte(opcode.JMPIF) + return opcode.JMPIF case opcode.JMPIFNOTL: - return byte(opcode.JMPIFNOT) + return opcode.JMPIFNOT case opcode.JMPEQL: - return byte(opcode.JMPEQ) + return opcode.JMPEQ case opcode.JMPNEL: - return byte(opcode.JMPNE) + return opcode.JMPNE case opcode.JMPGTL: - return byte(opcode.JMPGT) + return opcode.JMPGT case opcode.JMPGEL: - return byte(opcode.JMPGE) + return opcode.JMPGE case opcode.JMPLEL: - return byte(opcode.JMPLE) + return opcode.JMPLE case opcode.JMPLTL: - return byte(opcode.JMPLT) + return opcode.JMPLT case opcode.CALLL: - return byte(opcode.CALL) + return opcode.CALL default: panic(fmt.Errorf("invalid opcode: %s", op)) } From 51f3baf68e57a29ba6df1e0f83f3be528445d7bb Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 14:52:37 +0300 Subject: [PATCH 7/8] compiler: refactor BinaryExpr handling Reuse code between if conditions and expression context. --- pkg/compiler/codegen.go | 127 +++++++++++++++++++++++++--------------- pkg/compiler/types.go | 4 ++ 2 files changed, 85 insertions(+), 46 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 3b7771ddf..fcae45fbf 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -561,8 +561,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { lElseEnd := c.newLabel() if n.Cond != nil { - ast.Walk(c, n.Cond) - emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, lElse) + c.emitBoolExpr(n.Cond, true, false, lElse) } c.setLabel(lIf) @@ -689,50 +688,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil case *ast.BinaryExpr: - // The AST package will try to resolve all basic literals for us. - // If the typeinfo.Value is not nil we know that the expr is resolved - // and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6. - // NOTE: Constants will also be automatically resolved be the AST parser. - // example: - // const x = 10 - // x + 2 will results into 12 - tinfo := c.typeAndValueOf(n) - if tinfo.Value != nil { - c.emitLoadConst(tinfo) - return nil - } - - if arg := c.getCompareWithNilArg(n); arg != nil { - ast.Walk(c, arg) - emit.Opcode(c.prog.BinWriter, opcode.ISNULL) - if n.Op == token.NEQ { - emit.Opcode(c.prog.BinWriter, opcode.NOT) - } - return nil - } - - switch n.Op { - case token.LAND, token.LOR: - end := c.newLabel() - ast.Walk(c, n.X) - if n.Op == token.LAND { - emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 1 + 5}) - emit.Opcode(c.prog.BinWriter, opcode.PUSHF) - } else { - emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 1 + 5}) - emit.Opcode(c.prog.BinWriter, opcode.PUSHT) - } - emit.Jmp(c.prog.BinWriter, opcode.JMPL, end) - ast.Walk(c, n.Y) - c.setLabel(end) - return nil - - default: - ast.Walk(c, n.X) - ast.Walk(c, n.Y) - c.emitToken(n.Op, c.typeOf(n.X)) - return nil - } + c.emitBinaryExpr(n, false, false, 0) + return nil case *ast.CallExpr: var ( @@ -1122,6 +1079,84 @@ func (c *codegen) getCompareWithNilArg(n *ast.BinaryExpr) ast.Expr { return nil } +func (c *codegen) emitJumpOnCondition(cond bool, jmpLabel uint16) { + if cond { + emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, jmpLabel) + } else { + emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, jmpLabel) + } +} + +func (c *codegen) emitBoolExpr(n ast.Expr, needJump bool, cond bool, jmpLabel uint16) { + if be, ok := n.(*ast.BinaryExpr); ok { + c.emitBinaryExpr(be, needJump, cond, jmpLabel) + } else { + ast.Walk(c, n) + if needJump { + c.emitJumpOnCondition(cond, jmpLabel) + } + } +} + +func (c *codegen) emitBinaryExpr(n *ast.BinaryExpr, needJump bool, cond bool, jmpLabel uint16) { + // The AST package will try to resolve all basic literals for us. + // If the typeinfo.Value is not nil we know that the expr is resolved + // and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6. + // NOTE: Constants will also be automatically resolved be the AST parser. + // example: + // const x = 10 + // x + 2 will results into 12 + tinfo := c.typeAndValueOf(n) + if tinfo.Value != nil { + c.emitLoadConst(tinfo) + if needJump && isBool(tinfo.Type) { + c.emitJumpOnCondition(cond, jmpLabel) + } + return + } else if arg := c.getCompareWithNilArg(n); arg != nil { + ast.Walk(c, arg) + emit.Opcode(c.prog.BinWriter, opcode.ISNULL) + if needJump { + c.emitJumpOnCondition(cond == (n.Op == token.EQL), jmpLabel) + } else if n.Op == token.NEQ { + emit.Opcode(c.prog.BinWriter, opcode.NOT) + } + return + } + + switch n.Op { + case token.LAND, token.LOR: + end := c.newLabel() + + // true || .. == true, false && .. == false + condShort := n.Op == token.LOR + if needJump { + l := end + if cond == condShort { + l = jmpLabel + } + c.emitBoolExpr(n.X, true, condShort, l) + c.emitBoolExpr(n.Y, true, cond, jmpLabel) + } else { + push := c.newLabel() + c.emitBoolExpr(n.X, true, condShort, push) + c.emitBoolExpr(n.Y, false, false, 0) + emit.Jmp(c.prog.BinWriter, opcode.JMPL, end) + c.setLabel(push) + emit.Bool(c.prog.BinWriter, condShort) + } + c.setLabel(end) + + default: + ast.Walk(c, n.X) + ast.Walk(c, n.Y) + c.emitToken(n.Op, c.typeOf(n.X)) + if needJump { + c.emitJumpOnCondition(cond, jmpLabel) + } + } +} + func (c *codegen) pushStackLabel(name string, size int) { c.labelList = append(c.labelList, labelWithStackSize{ name: name, diff --git a/pkg/compiler/types.go b/pkg/compiler/types.go index e03dcf286..9b17de395 100644 --- a/pkg/compiler/types.go +++ b/pkg/compiler/types.go @@ -31,6 +31,10 @@ func isByte(typ types.Type) bool { return isBasicTypeOfKind(typ, types.Uint8, types.Int8) } +func isBool(typ types.Type) bool { + return isBasicTypeOfKind(typ, types.Bool, types.UntypedBool) +} + func isNumber(typ types.Type) bool { t, ok := typ.Underlying().(*types.Basic) return ok && t.Info()&types.IsNumeric != 0 From 9dc3edf3517fc9d457b824606b66b3d5c04a1dd1 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sun, 23 Aug 2020 18:00:23 +0300 Subject: [PATCH 8/8] compiler: make use of extended JMP* opcodes --- pkg/compiler/binary_expr_test.go | 99 ++++++++++++++++++++++++++++++++ pkg/compiler/codegen.go | 67 ++++++++++++++++++++- 2 files changed, 163 insertions(+), 3 deletions(-) diff --git a/pkg/compiler/binary_expr_test.go b/pkg/compiler/binary_expr_test.go index bf3f94c62..22bafd8a7 100644 --- a/pkg/compiler/binary_expr_test.go +++ b/pkg/compiler/binary_expr_test.go @@ -1,6 +1,7 @@ package compiler_test import ( + "fmt" "math/big" "testing" ) @@ -262,3 +263,101 @@ var binaryExprTestCases = []testCase{ func TestBinaryExprs(t *testing.T) { runTestCases(t, binaryExprTestCases) } + +func getBoolExprTestFunc(val bool, cond string) func(t *testing.T) { + srcTmpl := `package foo + var s = "str" + var v = 9 + var cond = %s + func Main() int { + if %s { + return 42 + } %s + return 17 + %s + }` + res := big.NewInt(42) + if !val { + res.SetInt64(17) + } + return func(t *testing.T) { + t.Run("AsExpression", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, cond, "cond", "", "") + eval(t, src, res) + }) + t.Run("InCondition", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, "true", cond, "", "") + eval(t, src, res) + }) + t.Run("InConditionWithElse", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, "true", cond, " else {", "}") + eval(t, src, res) + }) + } +} + +// TestBooleanExprs enumerates a lot of possible combinations of boolean expressions +// and tests if the result matches to that of Go. +func TestBooleanExprs(t *testing.T) { + trueExpr := []string{"true", "v < 10", "v <= 9", "v > 8", "v >= 9", "v == 9", "v != 8", `s == "str"`} + falseExpr := []string{"false", "v > 9", "v >= 10", "v < 9", "v <= 8", "v == 8", "v != 9", `s == "a"`} + t.Run("Single", func(t *testing.T) { + for _, s := range trueExpr { + t.Run(s, getBoolExprTestFunc(true, s)) + } + for _, s := range falseExpr { + t.Run(s, getBoolExprTestFunc(false, s)) + } + }) + + type arg struct { + val bool + s string + } + t.Run("Combine", func(t *testing.T) { + var double []arg + for _, e := range trueExpr { + double = append(double, arg{true, e + " || false"}) + double = append(double, arg{true, e + " && true"}) + } + for _, e := range falseExpr { + double = append(double, arg{false, e + " && true"}) + double = append(double, arg{false, e + " || false"}) + } + for i := range double { + t.Run(double[i].s, getBoolExprTestFunc(double[i].val, double[i].s)) + } + + var triple []arg + for _, a1 := range double { + for _, a2 := range double { + triple = append(triple, arg{a1.val || a2.val, fmt.Sprintf("(%s) || (%s)", a1.s, a2.s)}) + triple = append(triple, arg{a1.val && a2.val, fmt.Sprintf("(%s) && (%s)", a1.s, a2.s)}) + } + } + for i := range triple { + t.Run(triple[i].s, getBoolExprTestFunc(triple[i].val, triple[i].s)) + } + }) + return +} + +func TestShortCircuit(t *testing.T) { + srcTmpl := `package foo + var a = 1 + func inc() bool { a += 1; return %s } + func Main() int { + if %s { + return 41 + a + } + return 16 + a + }` + t.Run("||", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, "true", "a == 1 || inc()") + eval(t, src, big.NewInt(42)) + }) + t.Run("&&", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, "false", "a == 2 && inc()") + eval(t, src, big.NewInt(17)) + }) +} diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index fcae45fbf..a9cac98d8 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1087,6 +1087,8 @@ func (c *codegen) emitJumpOnCondition(cond bool, jmpLabel uint16) { } } +// emitBoolExpr emits boolean expression. If needJump is true and expression evaluates to `cond`, +// jump to jmpLabel is performed and no item is left on stack. func (c *codegen) emitBoolExpr(n ast.Expr, needJump bool, cond bool, jmpLabel uint16) { if be, ok := n.(*ast.BinaryExpr); ok { c.emitBinaryExpr(be, needJump, cond, jmpLabel) @@ -1098,6 +1100,8 @@ func (c *codegen) emitBoolExpr(n ast.Expr, needJump bool, cond bool, jmpLabel ui } } +// emitBinaryExpr emits binary expression. If needJump is true and expression evaluates to `cond`, +// jump to jmpLabel is performed and no item is left on stack. func (c *codegen) emitBinaryExpr(n *ast.BinaryExpr, needJump bool, cond bool, jmpLabel uint16) { // The AST package will try to resolve all basic literals for us. // If the typeinfo.Value is not nil we know that the expr is resolved @@ -1150,10 +1154,21 @@ func (c *codegen) emitBinaryExpr(n *ast.BinaryExpr, needJump bool, cond bool, jm default: ast.Walk(c, n.X) ast.Walk(c, n.Y) - c.emitToken(n.Op, c.typeOf(n.X)) - if needJump { - c.emitJumpOnCondition(cond, jmpLabel) + typ := c.typeOf(n.X) + if !needJump { + c.emitToken(n.Op, typ) + return } + op, ok := getJumpForToken(n.Op, typ) + if !ok { + c.emitToken(n.Op, typ) + c.emitJumpOnCondition(cond, jmpLabel) + return + } + if !cond { + op = negateJmp(op) + } + emit.Jmp(c.prog.BinWriter, op, jmpLabel) } } @@ -1214,6 +1229,29 @@ func (c *codegen) getLabelOffset(typ labelOffsetType, name string) uint16 { return c.labels[labelWithType{name: name, typ: typ}] } +// For `&&` and `||` it return an opcode which jumps only if result is known: +// false && .. == false, true || .. = true +func getJumpForToken(tok token.Token, typ types.Type) (opcode.Opcode, bool) { + switch tok { + case token.GTR: + return opcode.JMPGTL, true + case token.GEQ: + return opcode.JMPGEL, true + case token.LSS: + return opcode.JMPLTL, true + case token.LEQ: + return opcode.JMPLEL, true + case token.EQL, token.NEQ: + if isNumber(typ) { + if tok == token.EQL { + return opcode.JMPEQL, true + } + return opcode.JMPNEL, true + } + } + return 0, false +} + // getByteArray returns byte array value from constant expr. // Only literals are supported. func (c *codegen) getByteArray(expr ast.Expr) []byte { @@ -1766,6 +1804,29 @@ func calcOffsetCorrection(ip, target int, offsets []int) int { return cnt } +func negateJmp(op opcode.Opcode) opcode.Opcode { + switch op { + case opcode.JMPIFL: + return opcode.JMPIFNOTL + case opcode.JMPIFNOTL: + return opcode.JMPIFL + case opcode.JMPEQL: + return opcode.JMPNEL + case opcode.JMPNEL: + return opcode.JMPEQL + case opcode.JMPGTL: + return opcode.JMPLEL + case opcode.JMPGEL: + return opcode.JMPLTL + case opcode.JMPLEL: + return opcode.JMPGTL + case opcode.JMPLTL: + return opcode.JMPGEL + default: + panic(fmt.Errorf("invalid opcode in negateJmp: %s", op)) + } +} + func toShortForm(op opcode.Opcode) opcode.Opcode { switch op { case opcode.JMPL: