update script package

This commit is contained in:
Daniel Kang 2019-01-09 12:39:40 -08:00
parent 60c95c5de8
commit cce71f0cd5
9 changed files with 511 additions and 108 deletions

View file

@ -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
View 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
View 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
View 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)
}

View file

@ -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
View 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
View 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
}

View file

@ -1,8 +0,0 @@
package tengo_test
import (
"testing"
)
func TestScript(t *testing.T) {
}

View file

@ -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]