Skip to content

Commit

Permalink
all: allow calling quasigo functions from quasigo (#378)
Browse files Browse the repository at this point in the history
Now it's possible to call one bytecode function from another.
  • Loading branch information
quasilyte authored Feb 13, 2022
1 parent dbd4b2c commit 2e73e37
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 132 deletions.
14 changes: 12 additions & 2 deletions analyzer/testdata/src/quasigo/rules.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build ignore
// +build ignore

package gorules
Expand All @@ -7,12 +8,16 @@ import (
"github.com/quasilyte/go-ruleguard/dsl/types"
)

func derefPointer(ptr *types.Pointer) *types.Pointer {
return types.AsPointer(ptr.Elem())
}

func tooManyPointers(ctx *dsl.VarFilterContext) bool {
indir := 0
ptr := types.AsPointer(ctx.Type)
for ptr != nil {
indir++
ptr = types.AsPointer(ptr.Elem())
ptr = derefPointer(ptr)
}
return indir >= 3
}
Expand All @@ -33,11 +38,16 @@ func isPointer(ctx *dsl.VarFilterContext) bool {
return ptr != nil
}

func isInterface(ctx *dsl.VarFilterContext) bool {
func isInterfaceImpl(ctx *dsl.VarFilterContext) bool {
// Nil can be used on either side.
return nil != types.AsInterface(ctx.Type.Underlying())
}

func isInterface(ctx *dsl.VarFilterContext) bool {
// Forwarding a call to other function.
return isInterfaceImpl(ctx)
}

func isError(ctx *dsl.VarFilterContext) bool {
// Testing Interface.String() method.
iface := types.AsInterface(ctx.Type.Underlying())
Expand Down
7 changes: 4 additions & 3 deletions ruleguard/ir_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,10 @@ func (l *irLoader) compileFilterFuncs(filename string, irfile *ir.File) error {
continue
}
ctx := &quasigo.CompileContext{
Env: l.state.env,
Types: f.Types,
Fset: fset,
Env: l.state.env,
Package: f.Pkg,
Types: f.Types,
Fset: fset,
}
compiled, err := quasigo.Compile(ctx, decl)
if err != nil {
Expand Down
55 changes: 45 additions & 10 deletions ruleguard/quasigo/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,12 @@ func (cl *compiler) compileFunc(fn *ast.FuncDecl) *Func {
}

compiled := &Func{
code: cl.code,
constants: cl.constants,
intConstants: cl.intConstants,
code: cl.code,
constants: cl.constants,
intConstants: cl.intConstants,
numObjectParams: len(cl.params),
numIntParams: len(cl.intParams),
name: cl.ctx.Package.Path() + "." + fn.Name.String(),
}
if len(cl.locals) != 0 {
dbg.localNames = make([]string, len(cl.locals))
Expand Down Expand Up @@ -575,19 +578,51 @@ func (cl *compiler) compileCallExpr(call *ast.CallExpr) {
if sig.Variadic() {
variadic = sig.Params().Len() - 1
}
if !cl.compileNativeCall(key, variadic, expr, call.Args) {
panic(cl.errorf(call.Fun, "can't compile a call to %s func", key))
if expr != nil {
cl.compileExpr(expr)
}
if cl.compileNativeCall(key, variadic, expr, call.Args) {
return
}
if cl.compileCall(key, sig, call.Args) {
return
}
panic(cl.errorf(call.Fun, "can't compile a call to %s func", key))
}

func (cl *compiler) compileNativeCall(key funcKey, variadic int, expr ast.Expr, args []ast.Expr) bool {
funcID, ok := cl.ctx.Env.nameToNativeFuncID[key]
func (cl *compiler) compileCall(key funcKey, sig *types.Signature, args []ast.Expr) bool {
if sig.Variadic() {
return false
}

funcID, ok := cl.ctx.Env.nameToFuncID[key]
if !ok {
return false
}
if expr != nil {
cl.compileExpr(expr)

for _, arg := range args {
cl.compileExpr(arg)
}

var op opcode
if sig.Results().Len() == 0 {
op = opVoidCall
} else if typeIsInt(sig.Results().At(0).Type()) {
op = opIntCall
} else {
op = opCall
}

cl.emit16(op, int(funcID))
return true
}

func (cl *compiler) compileNativeCall(key funcKey, variadic int, funcExpr ast.Expr, args []ast.Expr) bool {
funcID, ok := cl.ctx.Env.nameToNativeFuncID[key]
if !ok {
return false
}

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.
Expand Down Expand Up @@ -619,7 +654,7 @@ func (cl *compiler) compileNativeCall(key funcKey, variadic int, expr ast.Expr,
}
}
if len(variadicArgs) > 255 {
panic(cl.errorf(expr, "too many variadic args"))
panic(cl.errorf(funcExpr, "too many variadic args"))
}
// Even if len(variadicArgs) is 0, we still need to overwrite
// the old variadicLen value, so the variadic func is not confused
Expand Down
71 changes: 44 additions & 27 deletions ruleguard/quasigo/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,28 @@ func TestCompile(t *testing.T) {
` PushLocal 0 # v`,
` ReturnTop`,
},

`return add1(10)`: {
` PushIntConst 0 # value=10`,
` IntCall 0 # testpkg.add1`,
` ReturnIntTop`,
},

`return concat(concat("x", "y"), "z")`: {
` PushConst 0 # value="x"`,
` PushConst 1 # value="y"`,
` Call 1 # testpkg.concat`,
` PushConst 2 # value="z"`,
` Call 1 # testpkg.concat`,
` ReturnTop`,
},
}

makePackageSource := func(body string) string {
return `
package testpkg
package ` + testPackage + `
func add1(x int) int { return x + 1 }
func concat(s1, s2 string) string { return s1 + s2 }
func f(i int, s string, b bool, eface interface{}) interface{} {
` + body + `
}
Expand All @@ -373,37 +390,37 @@ func TestCompile(t *testing.T) {
`
}

env := quasigo.NewEnv()
env.AddNativeFunc(testPackage, "imul", func(stack *quasigo.ValueStack) {
panic("should not be called")
})
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(testPackage, "sprintf", 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 {
env := quasigo.NewEnv()
env.AddNativeFunc(testPackage, "imul", func(stack *quasigo.ValueStack) {
panic("should not be called")
})
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(testPackage, "sprintf", 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")
})
src := makePackageSource(testSrc)
parsed, err := parseGoFile(src)
parsed, err := parseGoFile(testPackage, src)
if err != nil {
t.Errorf("parse %s: %v", testSrc, err)
continue
t.Fatalf("parse %s: %v", testSrc, err)
}
compiled, err := compileTestFunc(env, "f", parsed)
compiled, err := compileTestFile(env, "f", testPackage, parsed)
if err != nil {
t.Errorf("compile %s: %v", testSrc, err)
continue
t.Fatal(err)
}
if compiled == nil {
t.Fatal("can't find f function")
}
want := disasmLines
have := strings.Split(quasigo.Disasm(env, compiled), "\n")
Expand Down
4 changes: 4 additions & 0 deletions ruleguard/quasigo/disasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func disasm(env *Env, fn *Func) string {
id := decode16(code, pc+1)
arg = id
comment = env.nativeFuncs[id].name
case opCall, opIntCall, opVoidCall:
id := decode16(code, pc+1)
arg = id
comment = env.userFuncs[id].name
case opPushParam:
index := int(code[pc+1])
arg = index
Expand Down
17 changes: 17 additions & 0 deletions ruleguard/quasigo/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ func eval(env *EvalEnv, fn *Func, top, intTop int) CallResult {
fn := env.nativeFuncs[id].mappedFunc
fn(stack)
pc += 3
case opCall:
id := decode16(code, pc+1)
fn := env.userFuncs[id]
result := eval(env, fn, len(stack.objects)-fn.numObjectParams, len(stack.ints)-fn.numIntParams)
stack.Push(result.Value())
pc += 3
case opIntCall:
id := decode16(code, pc+1)
fn := env.userFuncs[id]
result := eval(env, fn, len(stack.objects)-fn.numObjectParams, len(stack.ints)-fn.numIntParams)
stack.PushInt(result.IntValue())
pc += 3
case opVoidCall:
id := decode16(code, pc+1)
fn := env.userFuncs[id]
eval(env, fn, len(stack.objects)-fn.numObjectParams, len(stack.ints)-fn.numIntParams)
pc += 3

case opJump:
offset := decode16(code, pc+1)
Expand Down
4 changes: 2 additions & 2 deletions ruleguard/quasigo/eval_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func pushArgs(env *quasigo.EvalEnv, args ...interface{}) {
func compileBenchFunc(t testing.TB, paramsSig, bodySrc string) (*quasigo.Env, *quasigo.Func) {
makePackageSource := func(body string) string {
return `
package test
package ` + testPackage + `
import "fmt"
var _ = fmt.Sprintf
func f(` + paramsSig + `) interface{} {
Expand All @@ -161,7 +161,7 @@ func compileBenchFunc(t testing.TB, paramsSig, bodySrc string) (*quasigo.Env, *q
})
qfmt.ImportAll(env)
src := makePackageSource(bodySrc)
parsed, err := parseGoFile(src)
parsed, err := parseGoFile(testPackage, src)
if err != nil {
t.Fatalf("parse %s: %v", bodySrc, err)
}
Expand Down
28 changes: 6 additions & 22 deletions ruleguard/quasigo/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"go/ast"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -170,7 +169,7 @@ func TestEval(t *testing.T) {
t.Fatalf("unexpected result type: %T", result)
}
return `
package test
package ` + testPackage + `
import "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/internal/evaltest"
func target(i int, s string, b bool, foo, nilfoo *evaltest.Foo, nileface interface{}) ` + returnType + ` {
` + body + `
Expand Down Expand Up @@ -213,7 +212,7 @@ func TestEval(t *testing.T) {
for i := range tests {
test := tests[i]
src := makePackageSource(test.src, test.result)
parsed, err := parseGoFile(src)
parsed, err := parseGoFile(testPackage, src)
if err != nil {
t.Fatalf("parse %s: %v", test.src, err)
}
Expand Down Expand Up @@ -261,7 +260,7 @@ func TestEvalFile(t *testing.T) {
return "", err
}
env := quasigo.NewEnv()
parsed, err := parseGoFile(string(src))
parsed, err := parseGoFile("main", string(src))
if err != nil {
return "", fmt.Errorf("parse: %v", err)
}
Expand All @@ -284,24 +283,9 @@ func TestEvalFile(t *testing.T) {
qstrconv.ImportAll(env)
qfmt.ImportAll(env)

var mainFunc *quasigo.Func
for _, decl := range parsed.ast.Decls {
decl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
ctx := &quasigo.CompileContext{
Env: env,
Types: parsed.types,
Fset: parsed.fset,
}
fn, err := quasigo.Compile(ctx, decl)
if err != nil {
return "", fmt.Errorf("compile %s func: %v", decl.Name, err)
}
if decl.Name.String() == "main" {
mainFunc = fn
}
mainFunc, err := compileTestFile(env, "main", "main", parsed)
if err != nil {
return "", err
}
if mainFunc == nil {
return "", errors.New("can't find main() function")
Expand Down
3 changes: 3 additions & 0 deletions ruleguard/quasigo/gen_opcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ var opcodePrototypes = []opcodeProto{

{"SetVariadicLen", "op len:u8", stackUnchanged},
{"CallNative", "op funcid:u16", "(args...) -> (results...)"},
{"Call", "op funcid:u16", "(args...) -> (result)"},
{"IntCall", "op funcid:u16", "(args...) -> (result:int)"},
{"VoidCall", "op funcid:u16", "(args...) -> ()"},

{"IsNil", "op", "(value) -> (result:bool)"},
{"IsNotNil", "op", "(value) -> (result:bool)"},
Expand Down
Loading

0 comments on commit 2e73e37

Please sign in to comment.