-
Notifications
You must be signed in to change notification settings - Fork 43
/
quasigo.go
165 lines (136 loc) · 5.02 KB
/
quasigo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// 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) }