From 2d7358bc927abb4e969157d7ecc64daa3b7b0182 Mon Sep 17 00:00:00 2001 From: "Iskander (Alex) Sharipov" Date: Thu, 18 Nov 2021 00:35:50 +0300 Subject: [PATCH] ruleguard/quasigo: implement void funcs (#307) Added EvalTest files to simplify the quasigo testing. --- ruleguard/quasigo/compile.go | 77 ++++++++++++++--- ruleguard/quasigo/eval.go | 2 + ruleguard/quasigo/eval_test.go | 95 +++++++++++++++++++++ ruleguard/quasigo/gen_opcodes.go | 3 +- ruleguard/quasigo/opcode_string.go | 49 +++++------ ruleguard/quasigo/opcodes.gen.go | 77 +++++++++-------- ruleguard/quasigo/testdata/voidfunc/main.go | 9 ++ ruleguard/ruleguard_error_test.go | 6 +- 8 files changed, 239 insertions(+), 79 deletions(-) create mode 100644 ruleguard/quasigo/testdata/voidfunc/main.go diff --git a/ruleguard/quasigo/compile.go b/ruleguard/quasigo/compile.go index db61b40e..6e5f4512 100644 --- a/ruleguard/quasigo/compile.go +++ b/ruleguard/quasigo/compile.go @@ -11,6 +11,8 @@ import ( "golang.org/x/tools/go/ast/astutil" ) +var voidType = &types.Tuple{} + func compile(ctx *CompileContext, fn *ast.FuncDecl) (compiled *Func, err error) { defer func() { if err != nil { @@ -74,10 +76,14 @@ type compileError string func (e compileError) Error() string { return string(e) } func (cl *compiler) compileFunc(fn *ast.FuncDecl) *Func { - if cl.fnType.Results().Len() != 1 { - panic(cl.errorf(fn.Name, "only functions with a single non-void results are supported")) + switch cl.fnType.Results().Len() { + case 0: + cl.retType = voidType + case 1: + cl.retType = cl.fnType.Results().At(0).Type() + default: + panic(cl.errorf(fn.Name, "multi-result functions are not supported")) } - cl.retType = cl.fnType.Results().At(0).Type() if !cl.isSupportedType(cl.retType) { panic(cl.errorUnsupportedType(fn.Name, cl.retType, "function result")) @@ -136,6 +142,9 @@ func (cl *compiler) compileStmt(stmt ast.Stmt) { case *ast.BranchStmt: cl.compileBranchStmt(stmt) + case *ast.ExprStmt: + cl.compileExprStmt(stmt) + case *ast.BlockStmt: for i := range stmt.List { cl.compileStmt(stmt.List[i]) @@ -172,6 +181,19 @@ func (cl *compiler) compileBranchStmt(branch *ast.BranchStmt) { } } +func (cl *compiler) compileExprStmt(stmt *ast.ExprStmt) { + if call, ok := stmt.X.(*ast.CallExpr); ok { + sig := cl.ctx.Types.TypeOf(call.Fun).(*types.Signature) + if sig.Results() != nil { + panic(cl.errorf(call, "only void funcs can be used in stmt context")) + } + cl.compileCallExpr(call) + return + } + + panic(cl.errorf(stmt.X, "can't compile this expr stmt yet: %T", stmt.X)) +} + func (cl *compiler) compileForStmt(stmt *ast.ForStmt) { labelBreak := cl.newLabel() labelContinue := cl.newLabel() @@ -279,6 +301,11 @@ func (cl *compiler) getLocal(v ast.Expr, varname string) int { } func (cl *compiler) compileReturnStmt(ret *ast.ReturnStmt) { + if cl.retType == voidType { + cl.emit(opReturn) + return + } + if ret.Results == nil { panic(cl.errorf(ret, "'naked' return statements are not allowed")) } @@ -471,6 +498,20 @@ func (cl *compiler) compileBuiltinCall(fn *ast.Ident, call *ast.CallExpr) { panic(cl.errorf(s, "can't compile len() with non-string argument yet")) } cl.emit(opStringLen) + + case `println`: + if len(call.Args) != 1 { + panic(cl.errorf(call, "only 1-arg form of println() is supported")) + } + funcName := "Print" + if typeIsInt(cl.ctx.Types.TypeOf(call.Args[0])) { + funcName = "PrintInt" + } + key := funcKey{qualifier: "builtin", name: funcName} + if !cl.compileNativeCall(key, nil, call.Args) { + panic(cl.errorf(fn, "builtin.%s native func is not registered", funcName)) + } + default: panic(cl.errorf(fn, "can't compile %s() builtin function call yet", fn)) } @@ -499,18 +540,24 @@ func (cl *compiler) compileCallExpr(call *ast.CallExpr) { key.qualifier = fn.Pkg().Path() } - if funcID, ok := cl.ctx.Env.nameToNativeFuncID[key]; ok { - if expr != nil { - cl.compileExpr(expr) - } - for _, arg := range call.Args { - cl.compileExpr(arg) - } - cl.emit16(opCallNative, int(funcID)) - return + if !cl.compileNativeCall(key, expr, call.Args) { + panic(cl.errorf(call.Fun, "can't compile a call to %s func", key)) } +} - panic(cl.errorf(call.Fun, "can't compile a call to %s func", key)) +func (cl *compiler) compileNativeCall(key funcKey, expr ast.Expr, args []ast.Expr) bool { + funcID, ok := cl.ctx.Env.nameToNativeFuncID[key] + if !ok { + return false + } + if expr != nil { + cl.compileExpr(expr) + } + for _, arg := range args { + cl.compileExpr(arg) + } + cl.emit16(opCallNative, int(funcID)) + return true } func (cl *compiler) compileUnaryOp(op opcode, e *ast.UnaryExpr) { @@ -681,6 +728,10 @@ func (cl *compiler) isUncondJump(op opcode) bool { } func (cl *compiler) isSupportedType(typ types.Type) bool { + if typ == voidType { + return true + } + switch typ := typ.Underlying().(type) { case *types.Pointer: // 1. Pointers to structs are supported. diff --git a/ruleguard/quasigo/eval.go b/ruleguard/quasigo/eval.go index afc000ea..18ce5108 100644 --- a/ruleguard/quasigo/eval.go +++ b/ruleguard/quasigo/eval.go @@ -114,6 +114,8 @@ func eval(env *EvalEnv, fn *Func, args []interface{}) CallResult { return CallResult{value: stack.top()} case opReturnIntTop: return CallResult{scalarValue: uint64(stack.topInt())} + case opReturn: + return CallResult{} case opCallNative: id := decode16(code, pc+1) diff --git a/ruleguard/quasigo/eval_test.go b/ruleguard/quasigo/eval_test.go index 90548c5d..a76d83f2 100644 --- a/ruleguard/quasigo/eval_test.go +++ b/ruleguard/quasigo/eval_test.go @@ -1,9 +1,17 @@ package quasigo import ( + "bytes" + "errors" "fmt" + "go/ast" + "io/ioutil" + "os" + "os/exec" + "path/filepath" "testing" + "github.com/google/go-cmp/cmp" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/internal/evaltest" ) @@ -221,3 +229,90 @@ func TestEval(t *testing.T) { } } } + +func TestEvalFile(t *testing.T) { + files, err := ioutil.ReadDir("testdata") + if err != nil { + t.Fatal(err) + } + + runGo := func(main string) (string, error) { + out, err := exec.Command("go", "run", main).CombinedOutput() + if err != nil { + return "", fmt.Errorf("%v: %s", err, out) + } + return string(out), nil + } + + runQuasigo := func(main string) (string, error) { + src, err := os.ReadFile(main) + if err != nil { + return "", err + } + env := NewEnv() + parsed, err := parseGoFile(string(src)) + if err != nil { + return "", fmt.Errorf("parse: %v", err) + } + + var stdout bytes.Buffer + env.AddNativeFunc("builtin", "Print", func(stack *ValueStack) { + arg := stack.Pop() + fmt.Fprintln(&stdout, arg) + }) + env.AddNativeFunc("builtin", "PrintInt", func(stack *ValueStack) { + fmt.Fprintln(&stdout, stack.PopInt()) + }) + + var mainFunc *Func + for _, decl := range parsed.ast.Decls { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + ctx := &CompileContext{ + Env: env, + Types: parsed.types, + Fset: parsed.fset, + } + fn, err := Compile(ctx, decl) + if err != nil { + return "", fmt.Errorf("compile %s func: %v", decl.Name, err) + } + if decl.Name.String() == "main" { + mainFunc = fn + } + } + if mainFunc == nil { + return "", errors.New("can't find main() function") + } + + Call(env.GetEvalEnv(), mainFunc) + return stdout.String(), nil + } + + runTest := func(t *testing.T, mainFile string) { + goResult, err := runGo(mainFile) + if err != nil { + t.Fatalf("run go: %v", err) + } + quasigoResult, err := runQuasigo(mainFile) + if err != nil { + t.Fatalf("run quasigo: %v", err) + } + if diff := cmp.Diff(quasigoResult, goResult); diff != "" { + t.Errorf("output mismatch:\nhave (+): `%s`\nwant (-): `%s`\ndiff: %s", quasigoResult, goResult, diff) + } + } + + for _, f := range files { + if !f.IsDir() { + continue + } + mainFile := filepath.Join("testdata", f.Name(), "main.go") + t.Run(f.Name(), func(t *testing.T) { + runTest(t, mainFile) + }) + } + +} diff --git a/ruleguard/quasigo/gen_opcodes.go b/ruleguard/quasigo/gen_opcodes.go index fde48b7c..e9abcae6 100644 --- a/ruleguard/quasigo/gen_opcodes.go +++ b/ruleguard/quasigo/gen_opcodes.go @@ -34,6 +34,7 @@ var opcodePrototypes = []opcodeProto{ {"ReturnIntTop", "op", "(value) -> (value)"}, {"ReturnFalse", "op", stackUnchanged}, {"ReturnTrue", "op", stackUnchanged}, + {"Return", "op", stackUnchanged}, {"Jump", "op offset:i16", stackUnchanged}, {"JumpFalse", "op offset:i16", "(cond:bool) -> ()"}, @@ -45,7 +46,7 @@ var opcodePrototypes = []opcodeProto{ {"IsNotNil", "op", "(value) -> (result:bool)"}, {"Not", "op", "(value:bool) -> (result:bool)"}, - + {"EqInt", "op", "(x:int y:int) -> (result:bool)"}, {"NotEqInt", "op", "(x:int y:int) -> (result:bool)"}, {"GtInt", "op", "(x:int y:int) -> (result:bool)"}, diff --git a/ruleguard/quasigo/opcode_string.go b/ruleguard/quasigo/opcode_string.go index 27dfc1f6..f6d37d14 100644 --- a/ruleguard/quasigo/opcode_string.go +++ b/ruleguard/quasigo/opcode_string.go @@ -27,33 +27,34 @@ func _() { _ = x[opReturnIntTop-16] _ = x[opReturnFalse-17] _ = x[opReturnTrue-18] - _ = x[opJump-19] - _ = x[opJumpFalse-20] - _ = x[opJumpTrue-21] - _ = x[opCallNative-22] - _ = x[opIsNil-23] - _ = x[opIsNotNil-24] - _ = x[opNot-25] - _ = x[opEqInt-26] - _ = x[opNotEqInt-27] - _ = x[opGtInt-28] - _ = x[opGtEqInt-29] - _ = x[opLtInt-30] - _ = x[opLtEqInt-31] - _ = x[opEqString-32] - _ = x[opNotEqString-33] - _ = x[opConcat-34] - _ = x[opAdd-35] - _ = x[opSub-36] - _ = x[opStringSlice-37] - _ = x[opStringSliceFrom-38] - _ = x[opStringSliceTo-39] - _ = x[opStringLen-40] + _ = x[opReturn-19] + _ = x[opJump-20] + _ = x[opJumpFalse-21] + _ = x[opJumpTrue-22] + _ = x[opCallNative-23] + _ = x[opIsNil-24] + _ = x[opIsNotNil-25] + _ = x[opNot-26] + _ = x[opEqInt-27] + _ = x[opNotEqInt-28] + _ = x[opGtInt-29] + _ = x[opGtEqInt-30] + _ = x[opLtInt-31] + _ = x[opLtEqInt-32] + _ = x[opEqString-33] + _ = x[opNotEqString-34] + _ = x[opConcat-35] + _ = x[opAdd-36] + _ = x[opSub-37] + _ = x[opStringSlice-38] + _ = x[opStringSliceFrom-39] + _ = x[opStringSliceTo-40] + _ = x[opStringLen-41] } -const _opcode_name = "InvalidPopDupPushParamPushIntParamPushLocalPushIntLocalPushFalsePushTruePushConstPushIntConstSetLocalSetIntLocalIncLocalDecLocalReturnTopReturnIntTopReturnFalseReturnTrueJumpJumpFalseJumpTrueCallNativeIsNilIsNotNilNotEqIntNotEqIntGtIntGtEqIntLtIntLtEqIntEqStringNotEqStringConcatAddSubStringSliceStringSliceFromStringSliceToStringLen" +const _opcode_name = "InvalidPopDupPushParamPushIntParamPushLocalPushIntLocalPushFalsePushTruePushConstPushIntConstSetLocalSetIntLocalIncLocalDecLocalReturnTopReturnIntTopReturnFalseReturnTrueReturnJumpJumpFalseJumpTrueCallNativeIsNilIsNotNilNotEqIntNotEqIntGtIntGtEqIntLtIntLtEqIntEqStringNotEqStringConcatAddSubStringSliceStringSliceFromStringSliceToStringLen" -var _opcode_index = [...]uint16{0, 7, 10, 13, 22, 34, 43, 55, 64, 72, 81, 93, 101, 112, 120, 128, 137, 149, 160, 170, 174, 183, 191, 201, 206, 214, 217, 222, 230, 235, 242, 247, 254, 262, 273, 279, 282, 285, 296, 311, 324, 333} +var _opcode_index = [...]uint16{0, 7, 10, 13, 22, 34, 43, 55, 64, 72, 81, 93, 101, 112, 120, 128, 137, 149, 160, 170, 176, 180, 189, 197, 207, 212, 220, 223, 228, 236, 241, 248, 253, 260, 268, 279, 285, 288, 291, 302, 317, 330, 339} func (i opcode) String() string { if i >= opcode(len(_opcode_index)-1) { diff --git a/ruleguard/quasigo/opcodes.gen.go b/ruleguard/quasigo/opcodes.gen.go index 268b42a1..61d13d7d 100644 --- a/ruleguard/quasigo/opcodes.gen.go +++ b/ruleguard/quasigo/opcodes.gen.go @@ -80,93 +80,97 @@ const ( // Stack effect: unchanged opReturnTrue opcode = 18 - // Encoding: 0x13 offset:i16 (width=3) + // Encoding: 0x13 (width=1) // Stack effect: unchanged - opJump opcode = 19 + opReturn opcode = 19 // Encoding: 0x14 offset:i16 (width=3) - // Stack effect: (cond:bool) -> () - opJumpFalse opcode = 20 + // Stack effect: unchanged + opJump opcode = 20 // Encoding: 0x15 offset:i16 (width=3) // Stack effect: (cond:bool) -> () - opJumpTrue opcode = 21 + opJumpFalse opcode = 21 - // Encoding: 0x16 funcid:u16 (width=3) - // Stack effect: (args...) -> (results...) - opCallNative opcode = 22 + // Encoding: 0x16 offset:i16 (width=3) + // Stack effect: (cond:bool) -> () + opJumpTrue opcode = 22 - // Encoding: 0x17 (width=1) - // Stack effect: (value) -> (result:bool) - opIsNil opcode = 23 + // Encoding: 0x17 funcid:u16 (width=3) + // Stack effect: (args...) -> (results...) + opCallNative opcode = 23 // Encoding: 0x18 (width=1) // Stack effect: (value) -> (result:bool) - opIsNotNil opcode = 24 + opIsNil opcode = 24 // Encoding: 0x19 (width=1) - // Stack effect: (value:bool) -> (result:bool) - opNot opcode = 25 + // Stack effect: (value) -> (result:bool) + opIsNotNil opcode = 25 // Encoding: 0x1a (width=1) - // Stack effect: (x:int y:int) -> (result:bool) - opEqInt opcode = 26 + // Stack effect: (value:bool) -> (result:bool) + opNot opcode = 26 // Encoding: 0x1b (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opNotEqInt opcode = 27 + opEqInt opcode = 27 // Encoding: 0x1c (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opGtInt opcode = 28 + opNotEqInt opcode = 28 // Encoding: 0x1d (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opGtEqInt opcode = 29 + opGtInt opcode = 29 // Encoding: 0x1e (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opLtInt opcode = 30 + opGtEqInt opcode = 30 // Encoding: 0x1f (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opLtEqInt opcode = 31 + opLtInt opcode = 31 // Encoding: 0x20 (width=1) - // Stack effect: (x:string y:string) -> (result:bool) - opEqString opcode = 32 + // Stack effect: (x:int y:int) -> (result:bool) + opLtEqInt opcode = 32 // Encoding: 0x21 (width=1) // Stack effect: (x:string y:string) -> (result:bool) - opNotEqString opcode = 33 + opEqString opcode = 33 // Encoding: 0x22 (width=1) - // Stack effect: (x:string y:string) -> (result:string) - opConcat opcode = 34 + // Stack effect: (x:string y:string) -> (result:bool) + opNotEqString opcode = 34 // Encoding: 0x23 (width=1) - // Stack effect: (x:int y:int) -> (result:int) - opAdd opcode = 35 + // Stack effect: (x:string y:string) -> (result:string) + opConcat opcode = 35 // Encoding: 0x24 (width=1) // Stack effect: (x:int y:int) -> (result:int) - opSub opcode = 36 + opAdd opcode = 36 // Encoding: 0x25 (width=1) - // Stack effect: (s:string from:int to:int) -> (result:string) - opStringSlice opcode = 37 + // Stack effect: (x:int y:int) -> (result:int) + opSub opcode = 37 // Encoding: 0x26 (width=1) - // Stack effect: (s:string from:int) -> (result:string) - opStringSliceFrom opcode = 38 + // Stack effect: (s:string from:int to:int) -> (result:string) + opStringSlice opcode = 38 // Encoding: 0x27 (width=1) - // Stack effect: (s:string to:int) -> (result:string) - opStringSliceTo opcode = 39 + // Stack effect: (s:string from:int) -> (result:string) + opStringSliceFrom opcode = 39 // Encoding: 0x28 (width=1) + // Stack effect: (s:string to:int) -> (result:string) + opStringSliceTo opcode = 40 + + // Encoding: 0x29 (width=1) // Stack effect: (s:string) -> (result:int) - opStringLen opcode = 40 + opStringLen opcode = 41 ) type opcodeInfo struct { @@ -194,6 +198,7 @@ var opcodeInfoTable = [256]opcodeInfo{ opReturnIntTop: {width: 1}, opReturnFalse: {width: 1}, opReturnTrue: {width: 1}, + opReturn: {width: 1}, opJump: {width: 3}, opJumpFalse: {width: 3}, opJumpTrue: {width: 3}, diff --git a/ruleguard/quasigo/testdata/voidfunc/main.go b/ruleguard/quasigo/testdata/voidfunc/main.go new file mode 100644 index 00000000..7f9dd79f --- /dev/null +++ b/ruleguard/quasigo/testdata/voidfunc/main.go @@ -0,0 +1,9 @@ +package main + +func main() { + println("hello") + println(1) + x := "var" + println(x) + return +} diff --git a/ruleguard/ruleguard_error_test.go b/ruleguard/ruleguard_error_test.go index 3adc653d..baa6fed0 100644 --- a/ruleguard/ruleguard_error_test.go +++ b/ruleguard/ruleguard_error_test.go @@ -137,13 +137,9 @@ func TestParseFilterFuncError(t *testing.T) { `can't compile a call to *gorules.Foo.String func`, }, - { - `func f() {}`, - `only functions with a single non-void results are supported`, - }, { `func f() (int, int) { return 0, 0 }`, - `only functions with a single non-void results are supported`, + `multi-result functions are not supported`, }, {