update script package
This commit is contained in:
parent
60c95c5de8
commit
cce71f0cd5
9 changed files with 511 additions and 108 deletions
|
@ -1,52 +0,0 @@
|
|||
package objects
|
||||
|
||||
import "fmt"
|
||||
|
||||
func FromValue(v interface{}) (Object, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return &String{Value: v}, nil
|
||||
case int64:
|
||||
return &Int{Value: v}, nil
|
||||
case int:
|
||||
return &Int{Value: int64(v)}, nil
|
||||
case bool:
|
||||
return &Bool{Value: v}, nil
|
||||
case rune:
|
||||
return &Char{Value: v}, nil
|
||||
case byte:
|
||||
return &Char{Value: rune(v)}, nil
|
||||
case float64:
|
||||
return &Float{Value: v}, nil
|
||||
case map[string]Object:
|
||||
return &Map{Value: v}, nil
|
||||
case map[string]interface{}:
|
||||
kv := make(map[string]Object)
|
||||
for vk, vv := range v {
|
||||
vo, err := FromValue(vv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kv[vk] = vo
|
||||
}
|
||||
return &Map{Value: kv}, nil
|
||||
case []Object:
|
||||
return &Array{Value: v}, nil
|
||||
case []interface{}:
|
||||
arr := make([]Object, len(v), len(v))
|
||||
for _, e := range v {
|
||||
vo, err := FromValue(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr = append(arr, vo)
|
||||
}
|
||||
return &Array{Value: arr}, nil
|
||||
case Object:
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported value type: %T", v)
|
||||
}
|
77
script/compiled.go
Normal file
77
script/compiled.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package script
|
||||
|
||||
import (
|
||||
"github.com/d5/tengo/compiler"
|
||||
"github.com/d5/tengo/objects"
|
||||
"github.com/d5/tengo/vm"
|
||||
)
|
||||
|
||||
var undefined objects.Object = &objects.Undefined{}
|
||||
|
||||
// Compiled is a compiled instance of the user script.
|
||||
// Use Script.Compile() to create Compiled object.
|
||||
type Compiled struct {
|
||||
symbolTable *compiler.SymbolTable
|
||||
machine *vm.VM
|
||||
}
|
||||
|
||||
// Run executes the compiled script in the virtual machine.
|
||||
func (c *Compiled) Run() error {
|
||||
return c.machine.Run()
|
||||
}
|
||||
|
||||
// IsDefined returns true if the variable name is defined (has value) before or after the execution.
|
||||
func (c *Compiled) IsDefined(name string) bool {
|
||||
symbol, _, ok := c.symbolTable.Resolve(name)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
v := c.machine.Globals()[symbol.Index]
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, isUndefined := (*v).(*objects.Undefined)
|
||||
|
||||
return !isUndefined
|
||||
}
|
||||
|
||||
// Get returns a variable identified by the name.
|
||||
func (c *Compiled) Get(name string) *Variable {
|
||||
value := &undefined
|
||||
|
||||
symbol, _, ok := c.symbolTable.Resolve(name)
|
||||
if ok && symbol.Scope == compiler.ScopeGlobal {
|
||||
value = c.machine.Globals()[symbol.Index]
|
||||
if value == nil {
|
||||
value = &undefined
|
||||
}
|
||||
}
|
||||
|
||||
return &Variable{
|
||||
name: name,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns all the variables that are defined by the compiled script.
|
||||
func (c *Compiled) GetAll() []*Variable {
|
||||
var vars []*Variable
|
||||
for _, name := range c.symbolTable.Names() {
|
||||
symbol, _, ok := c.symbolTable.Resolve(name)
|
||||
if ok && symbol.Scope == compiler.ScopeGlobal {
|
||||
value := c.machine.Globals()[symbol.Index]
|
||||
if value == nil {
|
||||
value = &undefined
|
||||
}
|
||||
|
||||
vars = append(vars, &Variable{
|
||||
name: name,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
121
script/compiled_test.go
Normal file
121
script/compiled_test.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package script_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/d5/tengo/assert"
|
||||
"github.com/d5/tengo/script"
|
||||
)
|
||||
|
||||
type M map[string]interface{}
|
||||
|
||||
func TestCompiled_Get(t *testing.T) {
|
||||
// simple script
|
||||
c := compile(t, `a = 5`, nil)
|
||||
compiledRun(t, c)
|
||||
compiledGet(t, c, "a", int64(5))
|
||||
|
||||
// user-defined variables
|
||||
compileError(t, `a = b`, nil) // compile error because "b" is not defined
|
||||
c = compile(t, `a = b`, M{"b": "foo"}) // now compile with b = "foo" defined
|
||||
compiledGet(t, c, "a", nil) // a = undefined; because it's before Compiled.Run()
|
||||
compiledRun(t, c) // Compiled.Run()
|
||||
compiledGet(t, c, "a", "foo") // a = "foo"
|
||||
}
|
||||
|
||||
func TestCompiled_GetAll(t *testing.T) {
|
||||
c := compile(t, `a = 5`, nil)
|
||||
compiledRun(t, c)
|
||||
compiledGetAll(t, c, M{"a": int64(5)})
|
||||
|
||||
c = compile(t, `a = b`, M{"b": "foo"})
|
||||
compiledRun(t, c)
|
||||
compiledGetAll(t, c, M{"a": "foo", "b": "foo"})
|
||||
|
||||
c = compile(t, `a = b; b = 5`, M{"b": "foo"})
|
||||
compiledRun(t, c)
|
||||
compiledGetAll(t, c, M{"a": "foo", "b": int64(5)})
|
||||
}
|
||||
|
||||
func TestCompiled_IsDefined(t *testing.T) {
|
||||
c := compile(t, `a = 5`, nil)
|
||||
compiledIsDefined(t, c, "a", false) // a is not defined before Run()
|
||||
compiledRun(t, c)
|
||||
compiledIsDefined(t, c, "a", true)
|
||||
compiledIsDefined(t, c, "b", false)
|
||||
}
|
||||
|
||||
func compile(t *testing.T, input string, vars M) *script.Compiled {
|
||||
s := script.New([]byte(input))
|
||||
for vn, vv := range vars {
|
||||
err := s.Add(vn, vv)
|
||||
if !assert.NoError(t, err) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
c, err := s.Compile()
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, c) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func compileError(t *testing.T, input string, vars M) bool {
|
||||
s := script.New([]byte(input))
|
||||
for vn, vv := range vars {
|
||||
err := s.Add(vn, vv)
|
||||
if !assert.NoError(t, err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.Compile()
|
||||
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
|
||||
func compiledRun(t *testing.T, c *script.Compiled) bool {
|
||||
err := c.Run()
|
||||
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func compiledGet(t *testing.T, c *script.Compiled, name string, expected interface{}) bool {
|
||||
v := c.Get(name)
|
||||
if !assert.NotNil(t, v) {
|
||||
return false
|
||||
}
|
||||
|
||||
return assert.Equal(t, expected, v.Value())
|
||||
}
|
||||
|
||||
func compiledGetAll(t *testing.T, c *script.Compiled, expected M) bool {
|
||||
vars := c.GetAll()
|
||||
|
||||
if !assert.Equal(t, len(expected), len(vars)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, v := range expected {
|
||||
var found bool
|
||||
for _, e := range vars {
|
||||
if e.Name() == k {
|
||||
if !assert.Equal(t, v, e.Value()) {
|
||||
return false
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
assert.Fail(t, "variable '%s' not found", k)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func compiledIsDefined(t *testing.T, c *script.Compiled, name string, expected bool) bool {
|
||||
return assert.Equal(t, expected, c.IsDefined(name))
|
||||
}
|
111
script/conversion.go
Normal file
111
script/conversion.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/d5/tengo/objects"
|
||||
)
|
||||
|
||||
func objectToString(o objects.Object) string {
|
||||
switch val := o.(type) {
|
||||
case *objects.Array:
|
||||
var s []string
|
||||
for _, e := range val.Value {
|
||||
s = append(s, objectToString(e))
|
||||
}
|
||||
return "[" + strings.Join(s, ", ") + "]"
|
||||
case *objects.Map:
|
||||
var s []string
|
||||
for k, v := range val.Value {
|
||||
s = append(s, k+": "+objectToString(v))
|
||||
}
|
||||
return "{" + strings.Join(s, ", ") + "}"
|
||||
case *objects.Int:
|
||||
return strconv.FormatInt(val.Value, 10)
|
||||
case *objects.Float:
|
||||
return strconv.FormatFloat(val.Value, 'f', -1, 64)
|
||||
case *objects.Bool:
|
||||
if val.Value {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case *objects.Char:
|
||||
return string(val.Value)
|
||||
case *objects.String:
|
||||
return val.Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func objectToInterface(o objects.Object) interface{} {
|
||||
switch val := o.(type) {
|
||||
case *objects.Array:
|
||||
return val.Value
|
||||
case *objects.Map:
|
||||
return val.Value
|
||||
case *objects.Int:
|
||||
return val.Value
|
||||
case *objects.Float:
|
||||
return val.Value
|
||||
case *objects.Bool:
|
||||
return val.Value
|
||||
case *objects.Char:
|
||||
return val.Value
|
||||
case *objects.String:
|
||||
return val.Value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func interfaceToObject(v interface{}) (objects.Object, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return &objects.String{Value: v}, nil
|
||||
case int64:
|
||||
return &objects.Int{Value: v}, nil
|
||||
case int:
|
||||
return &objects.Int{Value: int64(v)}, nil
|
||||
case bool:
|
||||
return &objects.Bool{Value: v}, nil
|
||||
case rune:
|
||||
return &objects.Char{Value: v}, nil
|
||||
case byte:
|
||||
return &objects.Char{Value: rune(v)}, nil
|
||||
case float64:
|
||||
return &objects.Float{Value: v}, nil
|
||||
case map[string]objects.Object:
|
||||
return &objects.Map{Value: v}, nil
|
||||
case map[string]interface{}:
|
||||
kv := make(map[string]objects.Object)
|
||||
for vk, vv := range v {
|
||||
vo, err := interfaceToObject(vv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kv[vk] = vo
|
||||
}
|
||||
return &objects.Map{Value: kv}, nil
|
||||
case []objects.Object:
|
||||
return &objects.Array{Value: v}, nil
|
||||
case []interface{}:
|
||||
arr := make([]objects.Object, len(v), len(v))
|
||||
for _, e := range v {
|
||||
vo, err := interfaceToObject(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr = append(arr, vo)
|
||||
}
|
||||
return &objects.Array{Value: arr}, nil
|
||||
case objects.Object:
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported value type: %T", v)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package tengo
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -10,42 +10,37 @@ import (
|
|||
"github.com/d5/tengo/vm"
|
||||
)
|
||||
|
||||
type Variable struct {
|
||||
name string
|
||||
value *objects.Object
|
||||
}
|
||||
|
||||
func (v *Variable) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *Variable) Value() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Script can simplify compilation and execution of embedded scripts.
|
||||
type Script struct {
|
||||
variables map[string]*objects.Object
|
||||
variables map[string]*Variable
|
||||
input []byte
|
||||
}
|
||||
|
||||
func NewScript(input []byte) *Script {
|
||||
// New creates a Script instance with an input script.
|
||||
func New(input []byte) *Script {
|
||||
return &Script{
|
||||
variables: make(map[string]*objects.Object),
|
||||
variables: make(map[string]*Variable),
|
||||
input: input,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a new variable or updates an existing variable to the script.
|
||||
func (s *Script) Add(name string, value interface{}) error {
|
||||
obj, err := objects.FromValue(value)
|
||||
obj, err := interfaceToObject(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.variables[name] = &obj
|
||||
s.variables[name] = &Variable{
|
||||
name: name,
|
||||
value: &obj,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes (undefines) an existing variable for the script.
|
||||
// It returns false if the variable name is not defined.
|
||||
func (s *Script) Remove(name string) bool {
|
||||
if _, ok := s.variables[name]; !ok {
|
||||
return false
|
||||
|
@ -56,7 +51,8 @@ func (s *Script) Remove(name string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (s *Script) Compile() (*CompiledScript, error) {
|
||||
// Compile compiles the script with all the defined variables, and, returns Compiled object.
|
||||
func (s *Script) Compile() (*Compiled, error) {
|
||||
symbolTable, globals := s.prepCompile()
|
||||
|
||||
fileSet := scanner.NewFileSet()
|
||||
|
@ -72,10 +68,9 @@ func (s *Script) Compile() (*CompiledScript, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &CompiledScript{
|
||||
bytecode: c.Bytecode(),
|
||||
return &Compiled{
|
||||
symbolTable: symbolTable,
|
||||
globals: globals,
|
||||
machine: vm.NewVM(c.Bytecode(), globals),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -94,36 +89,17 @@ func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, globals []*ob
|
|||
panic(fmt.Errorf("wrong symbol index: %d != %d", idx, symbol.Index))
|
||||
}
|
||||
|
||||
globals[symbol.Index] = s.variables[name]
|
||||
globals[symbol.Index] = s.variables[name].value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type CompiledScript struct {
|
||||
bytecode *compiler.Bytecode
|
||||
symbolTable *compiler.SymbolTable
|
||||
globals []*objects.Object
|
||||
}
|
||||
|
||||
func (c *CompiledScript) Run() error {
|
||||
v := vm.NewVM(c.bytecode, c.globals)
|
||||
|
||||
return v.Run()
|
||||
}
|
||||
|
||||
func (c *CompiledScript) Update(name string, value interface{}) error {
|
||||
symbol, _, ok := c.symbolTable.Resolve(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("name not found: %s", name)
|
||||
func (s *Script) copyVariables() map[string]*Variable {
|
||||
vars := make(map[string]*Variable)
|
||||
for n, v := range s.variables {
|
||||
vars[n] = v
|
||||
}
|
||||
|
||||
updated, err := objects.FromValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.globals[symbol.Index] = &updated
|
||||
|
||||
return nil
|
||||
return vars
|
||||
}
|
28
script/script_test.go
Normal file
28
script/script_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package script_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/d5/tengo/assert"
|
||||
"github.com/d5/tengo/script"
|
||||
)
|
||||
|
||||
func TestScript_Add(t *testing.T) {
|
||||
s := script.New([]byte(`a = b`))
|
||||
assert.NoError(t, s.Add("b", 5)) // b = 5
|
||||
assert.NoError(t, s.Add("b", "foo")) // b = "foo" (re-define before compilation)
|
||||
c, err := s.Compile()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, c.Run())
|
||||
assert.Equal(t, "foo", c.Get("a").Value())
|
||||
assert.Equal(t, "foo", c.Get("b").Value())
|
||||
}
|
||||
|
||||
func TestScript_Remove(t *testing.T) {
|
||||
s := script.New([]byte(`a = b`))
|
||||
err := s.Add("b", 5)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, s.Remove("b")) // b is removed
|
||||
_, err = s.Compile() // should not compile because b is undefined
|
||||
assert.Error(t, err)
|
||||
}
|
142
script/variable.go
Normal file
142
script/variable.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package script
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/d5/tengo/objects"
|
||||
)
|
||||
|
||||
// Variable is a user-defined variable for the script.
|
||||
type Variable struct {
|
||||
name string
|
||||
value *objects.Object
|
||||
}
|
||||
|
||||
// Name returns the name of the variable.
|
||||
func (v *Variable) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
// Value returns an empty interface of the variable value.
|
||||
func (v *Variable) Value() interface{} {
|
||||
return objectToInterface(*v.value)
|
||||
}
|
||||
|
||||
// ValueType returns the name of the value type.
|
||||
func (v *Variable) ValueType() string {
|
||||
return (*v.value).TypeName()
|
||||
}
|
||||
|
||||
// Int returns int value of the variable value.
|
||||
// It returns 0 if the value is not convertible to int.
|
||||
func (v *Variable) Int() int {
|
||||
return int(v.Int64())
|
||||
}
|
||||
|
||||
// Int64 returns int64 value of the variable value.
|
||||
// It returns 0 if the value is not convertible to int64.
|
||||
func (v *Variable) Int64() int {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Int:
|
||||
return int(val.Value)
|
||||
case *objects.Float:
|
||||
return int(val.Value)
|
||||
case *objects.Bool:
|
||||
if val.Value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case *objects.Char:
|
||||
return int(val.Value)
|
||||
case *objects.String:
|
||||
n, _ := strconv.ParseInt(val.Value, 10, 64)
|
||||
return int(n)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Float returns float64 value of the variable value.
|
||||
// It returns 0.0 if the value is not convertible to float64.
|
||||
func (v *Variable) Float() float64 {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Int:
|
||||
return float64(val.Value)
|
||||
case *objects.Float:
|
||||
return val.Value
|
||||
case *objects.Bool:
|
||||
if val.Value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case *objects.String:
|
||||
f, _ := strconv.ParseFloat(val.Value, 64)
|
||||
return f
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Char returns rune value of the variable value.
|
||||
// It returns 0 if the value is not convertible to rune.
|
||||
func (v *Variable) Char() rune {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Char:
|
||||
return val.Value
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Bool returns bool value of the variable value.
|
||||
// It returns 0 if the value is not convertible to bool.
|
||||
func (v *Variable) Bool() bool {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Bool:
|
||||
return val.Value
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Array returns []interface value of the variable value.
|
||||
// It returns 0 if the value is not convertible to []interface.
|
||||
func (v *Variable) Array() []interface{} {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Array:
|
||||
var arr []interface{}
|
||||
for _, e := range val.Value {
|
||||
arr = append(arr, objectToInterface(e))
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Map returns map[string]interface{} value of the variable value.
|
||||
// It returns 0 if the value is not convertible to map[string]interface{}.
|
||||
func (v *Variable) Map() map[string]interface{} {
|
||||
switch val := (*v.value).(type) {
|
||||
case *objects.Map:
|
||||
kv := make(map[string]interface{})
|
||||
for mk, mv := range val.Value {
|
||||
kv[mk] = objectToInterface(mv)
|
||||
}
|
||||
return kv
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns string value of the variable value.
|
||||
// It returns 0 if the value is not convertible to string.
|
||||
func (v *Variable) String() string {
|
||||
return objectToString(*v.value)
|
||||
}
|
||||
|
||||
// Object returns an underlying Object of the variable value.
|
||||
// Note that returned Object is a copy of an actual Object used in the script.
|
||||
func (v *Variable) Object() objects.Object {
|
||||
return *v.value
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package tengo_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScript(t *testing.T) {
|
||||
}
|
8
vm/vm.go
8
vm/vm.go
|
@ -34,6 +34,10 @@ type VM struct {
|
|||
func NewVM(bytecode *compiler.Bytecode, globals []*objects.Object) *VM {
|
||||
if globals == nil {
|
||||
globals = make([]*objects.Object, GlobalsSize)
|
||||
} else if len(globals) < GlobalsSize {
|
||||
g := make([]*objects.Object, GlobalsSize)
|
||||
copy(g, globals)
|
||||
globals = g
|
||||
}
|
||||
|
||||
frames := make([]Frame, MaxFrames)
|
||||
|
@ -680,6 +684,10 @@ func (v *VM) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *VM) Globals() []*objects.Object {
|
||||
return v.globals
|
||||
}
|
||||
|
||||
// for tests
|
||||
func (v *VM) Stack() []*objects.Object {
|
||||
return v.stack[:v.sp]
|
||||
|
|
Loading…
Reference in a new issue