woodpecker/vendor/github.com/quasilyte/go-ruleguard/ruleguard/quasigo/quasigo.go

166 lines
5 KiB
Go
Raw Normal View History

// Package quasigo implements a Go subset compiler and interpreter.
//
// The implementation details are not part of the contract of this package.
package quasigo
import (
"go/ast"
"go/token"
"go/types"
)
// TODO(quasilyte): document what is thread-safe and what not.
// TODO(quasilyte): add a readme.
// Env is used to hold both compilation and evaluation data.
type Env struct {
// TODO(quasilyte): store both native and user func ids in one map?
nativeFuncs []nativeFunc
nameToNativeFuncID map[funcKey]uint16
userFuncs []*Func
nameToFuncID map[funcKey]uint16
// debug contains all information that is only needed
// for better debugging and compiled code introspection.
// Right now it's always enabled, but we may allow stripping it later.
debug *debugInfo
}
// EvalEnv is a goroutine-local handle for Env.
// To get one, use Env.GetEvalEnv() method.
type EvalEnv struct {
nativeFuncs []nativeFunc
userFuncs []*Func
stack *ValueStack
}
// NewEnv creates a new empty environment.
func NewEnv() *Env {
return newEnv()
}
// GetEvalEnv creates a new goroutine-local handle of env.
func (env *Env) GetEvalEnv() *EvalEnv {
return &EvalEnv{
nativeFuncs: env.nativeFuncs,
userFuncs: env.userFuncs,
stack: &ValueStack{
objects: make([]interface{}, 0, 32),
ints: make([]int, 0, 16),
},
}
}
// AddNativeMethod binds `$typeName.$methodName` symbol with f.
// A typeName should be fully qualified, like `github.com/user/pkgname.TypeName`.
// It method is defined only on pointer type, the typeName should start with `*`.
func (env *Env) AddNativeMethod(typeName, methodName string, f func(*ValueStack)) {
env.addNativeFunc(funcKey{qualifier: typeName, name: methodName}, f)
}
// AddNativeFunc binds `$pkgPath.$funcName` symbol with f.
// A pkgPath should be a full package path in which funcName is defined.
func (env *Env) AddNativeFunc(pkgPath, funcName string, f func(*ValueStack)) {
env.addNativeFunc(funcKey{qualifier: pkgPath, name: funcName}, f)
}
// AddFunc binds `$pkgPath.$funcName` symbol with f.
func (env *Env) AddFunc(pkgPath, funcName string, f *Func) {
env.addFunc(funcKey{qualifier: pkgPath, name: funcName}, f)
}
// GetFunc finds previously bound function searching for the `$pkgPath.$funcName` symbol.
func (env *Env) GetFunc(pkgPath, funcName string) *Func {
id := env.nameToFuncID[funcKey{qualifier: pkgPath, name: funcName}]
return env.userFuncs[id]
}
// CompileContext is used to provide necessary data to the compiler.
type CompileContext struct {
// Env is shared environment that should be used for all functions
// being compiled; then it should be used to execute these functions.
Env *Env
Types *types.Info
Fset *token.FileSet
}
// Compile prepares an executable version of fn.
func Compile(ctx *CompileContext, fn *ast.FuncDecl) (compiled *Func, err error) {
return compile(ctx, fn)
}
// Call invokes a given function with provided arguments.
func Call(env *EvalEnv, fn *Func, args ...interface{}) CallResult {
env.stack.objects = env.stack.objects[:0]
env.stack.ints = env.stack.ints[:0]
return eval(env, fn, args)
}
// CallResult is a return value of Call function.
// For most functions, Value() should be called to get the actual result.
// For int-typed functions, IntValue() should be used instead.
type CallResult struct {
value interface{}
scalarValue uint64
}
// Value unboxes an actual call return value.
// For int results, use IntValue().
func (res CallResult) Value() interface{} { return res.value }
// IntValue unboxes an actual call return value.
func (res CallResult) IntValue() int { return int(res.scalarValue) }
// Disasm returns the compiled function disassembly text.
// This output is not guaranteed to be stable between versions
// and should be used only for debugging purposes.
func Disasm(env *Env, fn *Func) string {
return disasm(env, fn)
}
// Func is a compiled function that is ready to be executed.
type Func struct {
code []byte
constants []interface{}
intConstants []int
}
// ValueStack is used to manipulate runtime values during the evaluation.
// Function arguments are pushed to the stack.
// Function results are returned via stack as well.
//
// For the sake of efficiency, it stores different types separately.
// If int was pushed with PushInt(), it should be retrieved by PopInt().
// It's a bad idea to do a Push() and then PopInt() and vice-versa.
type ValueStack struct {
objects []interface{}
ints []int
}
// Pop removes the top stack element and returns it.
// Important: for int-typed values, use PopInt.
func (s *ValueStack) Pop() interface{} {
x := s.objects[len(s.objects)-1]
s.objects = s.objects[:len(s.objects)-1]
return x
}
// PopInt removes the top stack element and returns it.
func (s *ValueStack) PopInt() int {
x := s.ints[len(s.ints)-1]
s.ints = s.ints[:len(s.ints)-1]
return x
}
// Push adds x to the stack.
// Important: for int-typed values, use PushInt.
func (s *ValueStack) Push(x interface{}) { s.objects = append(s.objects, x) }
// PushInt adds x to the stack.
func (s *ValueStack) PushInt(x int) { s.ints = append(s.ints, x) }