compiler: do not short-circuit in complex conditions

Current implementation of short-circuting is just plain wrong
as it uses `last` or `before-last` labels which meaning depend
on context. It doesn't even handle simple assignements like
`a := x == 1 && y == 2`.

This commit makes all jumps in such conditions local
and adds tests.

Closes #699, #700.
This commit is contained in:
Evgenii Stratonikov 2020-02-28 17:18:42 +03:00
parent 26c4e83ddf
commit b461a6ab63
2 changed files with 64 additions and 2 deletions

View file

@ -505,15 +505,27 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.BinaryExpr: case *ast.BinaryExpr:
switch n.Op { switch n.Op {
case token.LAND: case token.LAND:
next := c.newLabel()
end := c.newLabel()
ast.Walk(c, n.X) ast.Walk(c, n.X)
emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOT, uint16(len(c.l)-1)) emit.Jmp(c.prog.BinWriter, opcode.JMPIF, next)
emit.Opcode(c.prog.BinWriter, opcode.PUSHF)
emit.Jmp(c.prog.BinWriter, opcode.JMP, end)
c.setLabel(next)
ast.Walk(c, n.Y) ast.Walk(c, n.Y)
c.setLabel(end)
return nil return nil
case token.LOR: case token.LOR:
next := c.newLabel()
end := c.newLabel()
ast.Walk(c, n.X) ast.Walk(c, n.X)
emit.Jmp(c.prog.BinWriter, opcode.JMPIF, uint16(len(c.l)-3)) emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOT, next)
emit.Opcode(c.prog.BinWriter, opcode.PUSHT)
emit.Jmp(c.prog.BinWriter, opcode.JMP, end)
c.setLabel(next)
ast.Walk(c, n.Y) ast.Walk(c, n.Y)
c.setLabel(end)
return nil return nil
default: default:

View file

@ -1,6 +1,7 @@
package compiler_test package compiler_test
import ( import (
"fmt"
"math/big" "math/big"
"strings" "strings"
"testing" "testing"
@ -704,3 +705,52 @@ func TestForLoopRangeCompilerError(t *testing.T) {
_, err := compiler.Compile(strings.NewReader(src)) _, err := compiler.Compile(strings.NewReader(src))
require.Error(t, err) require.Error(t, err)
} }
func TestForLoopComplexConditions(t *testing.T) {
src := `
package foo
func Main() int {
var ok bool
_ = ok
i := 0
j := 0
%s
for %s {
i++
j++
%s
}
return i
}`
tests := []struct {
Name string
Cond string
Assign string
Result int64
}{
{Cond: "i < 3 && j < 2", Result: 2},
{Cond: "i < 3 || j < 2", Result: 3},
{Cond: "i < 3 && (j < 2 || i < 1)", Result: 2},
{Cond: "i < 3 && (j < 2 && i < 1)", Result: 1},
{Cond: "(i < 1 || j < 3) && (i < 3 || j < 1)", Result: 3},
{Cond: "(i < 2 && j < 4) || (i < 4 && j < 2)", Result: 2},
{Cond: "ok", Assign: "ok = i < 3 && j < 2", Result: 2},
{Cond: "ok", Assign: "ok = i < 3 || j < 2", Result: 3},
{Cond: "ok", Assign: "ok = i < 3 && (j < 2 || i < 1)", Result: 2},
{Cond: "ok", Assign: "ok = i < 3 && (j < 2 && i < 1)", Result: 1},
{Cond: "ok", Assign: "ok = (i < 1 || j < 3) && (i < 3 || j < 1)", Result: 3},
{Cond: "ok", Assign: "ok = (i < 2 && j < 4) || (i < 4 && j < 2)", Result: 2},
}
for _, tc := range tests {
name := tc.Cond
if tc.Assign != "" {
name = tc.Assign
}
t.Run(name, func(t *testing.T) {
s := fmt.Sprintf(src, tc.Assign, tc.Cond, tc.Assign)
eval(t, s, big.NewInt(tc.Result))
})
}
}