diff --git a/ruleguard/quasigo/compile.go b/ruleguard/quasigo/compile.go index b9749f5d..c5287eb2 100644 --- a/ruleguard/quasigo/compile.go +++ b/ruleguard/quasigo/compile.go @@ -258,38 +258,43 @@ func (cl *compiler) compileIfStmt(stmt *ast.IfStmt) { } func (cl *compiler) compileAssignStmt(assign *ast.AssignStmt) { - if len(assign.Lhs) != 1 { - panic(cl.errorf(assign, "only single left operand is allowed in assignments")) - } if len(assign.Rhs) != 1 { panic(cl.errorf(assign, "only single right operand is allowed in assignments")) } - lhs := assign.Lhs[0] - rhs := assign.Rhs[0] - varname, ok := lhs.(*ast.Ident) - if !ok { - panic(cl.errorf(lhs, "can assign only to simple variables")) + for _, lhs := range assign.Lhs { + _, ok := lhs.(*ast.Ident) + if !ok { + panic(cl.errorf(lhs, "can assign only to simple variables")) + } } + rhs := assign.Rhs[0] cl.compileExpr(rhs) - typ := cl.ctx.Types.TypeOf(varname) if assign.Tok == token.DEFINE { - if _, ok := cl.locals[varname.String()]; ok { - panic(cl.errorf(lhs, "%s variable shadowing is not allowed", varname)) + for i := len(assign.Lhs) - 1; i >= 0; i-- { + varname := assign.Lhs[i].(*ast.Ident) + typ := cl.ctx.Types.TypeOf(varname) + if _, ok := cl.locals[varname.String()]; ok { + panic(cl.errorf(varname, "%s variable shadowing is not allowed", varname)) + } + if !cl.isSupportedType(typ) { + panic(cl.errorUnsupportedType(varname, typ, varname.String()+" local variable")) + } + if len(cl.locals) == maxFuncLocals { + panic(cl.errorf(varname, "can't define %s: too many locals", varname)) + } + id := len(cl.locals) + cl.locals[varname.String()] = id + cl.emit8(pickOp(typeIsInt(typ), opSetIntLocal, opSetLocal), id) } - if !cl.isSupportedType(typ) { - panic(cl.errorUnsupportedType(varname, typ, varname.String()+" local variable")) - } - if len(cl.locals) == maxFuncLocals { - panic(cl.errorf(lhs, "can't define %s: too many locals", varname)) - } - id := len(cl.locals) - cl.locals[varname.String()] = id - cl.emit8(pickOp(typeIsInt(typ), opSetIntLocal, opSetLocal), id) } else { - id := cl.getLocal(varname, varname.String()) - cl.emit8(pickOp(typeIsInt(typ), opSetIntLocal, opSetLocal), id) + for i := len(assign.Lhs) - 1; i >= 0; i-- { + varname := assign.Lhs[i].(*ast.Ident) + typ := cl.ctx.Types.TypeOf(varname) + id := cl.getLocal(varname, varname.String()) + cl.emit8(pickOp(typeIsInt(typ), opSetIntLocal, opSetLocal), id) + } } } @@ -557,6 +562,16 @@ func (cl *compiler) compileNativeCall(key funcKey, expr ast.Expr, args []ast.Exp if expr != nil { cl.compileExpr(expr) } + if len(args) == 1 { + // Check that it's not a f(g()) call, where g() returns + // a multi-value result; we can't compile that yet. + if call, ok := args[0].(*ast.CallExpr); ok { + results := cl.ctx.Types.TypeOf(call.Fun).(*types.Signature).Results() + if results != nil && results.Len() > 1 { + panic(cl.errorf(args[0], "can't pass tuple as a func argument")) + } + } + } for _, arg := range args { cl.compileExpr(arg) } diff --git a/ruleguard/quasigo/compile_test.go b/ruleguard/quasigo/compile_test.go index 8cfe0360..5136b163 100644 --- a/ruleguard/quasigo/compile_test.go +++ b/ruleguard/quasigo/compile_test.go @@ -323,16 +323,31 @@ func TestCompile(t *testing.T) { ` PushIntLocal 0 # j`, ` ReturnIntTop`, }, + + `v, err := atoi("foo"); println(v); println(err == nil); return err`: { + ` PushConst 0 # value="foo"`, + ` CallNative 2 # testpkg.atoi`, + ` SetLocal 0 # err`, + ` SetIntLocal 1 # v`, + ` PushIntLocal 1 # v`, + ` CallNative 3 # builtin.PrintInt`, + ` PushLocal 0 # err`, + ` IsNil`, + ` CallNative 4 # builtin.Print`, + ` PushLocal 0 # err`, + ` ReturnTop`, + }, } makePackageSource := func(body string) string { return ` - package test + package testpkg func f(i int, s string, b bool, eface interface{}) interface{} { ` + body + ` } func imul(x, y int) int func idiv(x, y int) int + func atoi(s string) (int, error) ` } @@ -343,6 +358,15 @@ func TestCompile(t *testing.T) { env.AddNativeFunc(testPackage, "idiv", func(stack *quasigo.ValueStack) { panic("should not be called") }) + env.AddNativeFunc(testPackage, "atoi", func(stack *quasigo.ValueStack) { + panic("should not be called") + }) + env.AddNativeFunc("builtin", "PrintInt", func(stack *quasigo.ValueStack) { + panic("should not be called") + }) + env.AddNativeFunc("builtin", "Print", func(stack *quasigo.ValueStack) { + panic("should not be called") + }) for testSrc, disasmLines := range tests { src := makePackageSource(testSrc) diff --git a/ruleguard/quasigo/eval_test.go b/ruleguard/quasigo/eval_test.go index 8a3da22a..30e25e4f 100644 --- a/ruleguard/quasigo/eval_test.go +++ b/ruleguard/quasigo/eval_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/internal/evaltest" + "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrconv" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrings" ) @@ -260,15 +261,21 @@ func TestEvalFile(t *testing.T) { } var stdout bytes.Buffer - env.AddNativeFunc("builtin", "Print", func(stack *quasigo.ValueStack) { + env.AddNativeFunc(`builtin`, `Print`, func(stack *quasigo.ValueStack) { arg := stack.Pop() fmt.Fprintln(&stdout, arg) }) - env.AddNativeFunc("builtin", "PrintInt", func(stack *quasigo.ValueStack) { + env.AddNativeFunc(`builtin`, `PrintInt`, func(stack *quasigo.ValueStack) { fmt.Fprintln(&stdout, stack.PopInt()) }) + env.AddNativeMethod(`error`, `Error`, func(stack *quasigo.ValueStack) { + err := stack.Pop().(error) + stack.Push(err.Error()) + }) + qstrings.ImportAll(env) + qstrconv.ImportAll(env) var mainFunc *quasigo.Func for _, decl := range parsed.ast.Decls { @@ -320,5 +327,4 @@ func TestEvalFile(t *testing.T) { runTest(t, mainFile) }) } - } diff --git a/ruleguard/quasigo/stdlib/qstrconv/qstrconv.go b/ruleguard/quasigo/stdlib/qstrconv/qstrconv.go new file mode 100644 index 00000000..8bc2d943 --- /dev/null +++ b/ruleguard/quasigo/stdlib/qstrconv/qstrconv.go @@ -0,0 +1,24 @@ +package qstrconv + +import ( + "strconv" + + "github.com/quasilyte/go-ruleguard/ruleguard/quasigo" +) + +func ImportAll(env *quasigo.Env) { + env.AddNativeFunc(`strconv`, `Atoi`, Atoi) + env.AddNativeFunc(`strconv`, `Itoa`, Itoa) +} + +func Atoi(stack *quasigo.ValueStack) { + s := stack.Pop().(string) + v, err := strconv.Atoi(s) + stack.PushInt(v) + stack.Push(err) +} + +func Itoa(stack *quasigo.ValueStack) { + i := stack.PopInt() + stack.Push(strconv.Itoa(i)) +} diff --git a/ruleguard/quasigo/testdata/qstrconv/main.go b/ruleguard/quasigo/testdata/qstrconv/main.go new file mode 100644 index 00000000..e6901356 --- /dev/null +++ b/ruleguard/quasigo/testdata/qstrconv/main.go @@ -0,0 +1,28 @@ +package main + +import "strconv" + +func main() { + s := "16" + + i, err := strconv.Atoi(s) + println(i) + println(err == nil) + + i2, err2 := strconv.Atoi("bad") + println(i2) + println(err2.Error()) + + println(strconv.Itoa(140)) + println(strconv.Itoa(i) == s) + + i, err2 = strconv.Atoi("foo") + println(i) + println(err2.Error()) + + i, err2 = strconv.Atoi("-349") + println(i) + if err2 == nil { + println("err2 is nil") + } +} diff --git a/ruleguard/ruleguard_error_test.go b/ruleguard/ruleguard_error_test.go index baa6fed0..247cbe74 100644 --- a/ruleguard/ruleguard_error_test.go +++ b/ruleguard/ruleguard_error_test.go @@ -82,7 +82,7 @@ func TestParseFilterFuncError(t *testing.T) { // Assignment errors. { `x, y := 1, 2; return x == y`, - `only single left operand is allowed in assignments`, + `only single right operand is allowed in assignments`, }, { `x := 0; { x := 1; return x == 1 }; return x == 0`,