working on script modules (WIP)

This commit is contained in:
Daniel Kang 2019-01-30 21:50:15 -08:00
parent f7b0cd8299
commit edc23cf2f1
3 changed files with 89 additions and 2 deletions

View file

@ -4,7 +4,8 @@
- [Using Scripts](#using-scripts)
- [Type Conversion Table](#type-conversion-table)
- [User Types](#user-types)
- [User Types](#user-types)
- [Importing Scripts](#importing-scripts)
- [Sandbox Environments](#sandbox-environments)
- [Compiler and VM](#compiler-and-vm)
@ -90,6 +91,7 @@ When adding a Variable _([Script.Add](https://godoc.org/github.com/d5/tengo/scri
|`byte`|`Char`||
|`float64`|`Float`||
|`[]byte`|`Bytes`||
|`time.Time`|`Time`||
|`error`|`Error{String}`|use `error.Error()` as String value|
|`map[string]Object`|`Map`||
|`map[string]interface{}`|`Map`|individual elements converted to Tengo objects|
@ -98,10 +100,24 @@ When adding a Variable _([Script.Add](https://godoc.org/github.com/d5/tengo/scri
|`Object`|`Object`|_(no type conversion performed)_|
## User Types
### User Types
One can easily add and use customized value types in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo/objects#Object) interface. Tengo runtime will treat the user types exactly in the same way it does to the runtime types with no performance overhead. See [Tengo Objects](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details.
### Importing Scripts
Using `Script.AddModule` function, a compiled script can be used _(imported)_ by another script as a module, in the same way the script can load the standard library or the user modules.
```golang
mod1, _ := script.New([]byte(`a := 5`)).Compile() // mod1 is a "compiled" script
s := script.New([]byte(`print(import("mod1").a)`)) // main script
_ = s.AddModule("mod1", mod1) // add mod1 using name "mod1"
_, _ = s.Run() // prints "5"
```
Notice that the compiled script (`mod1` in this example code) does not have to be `Run()` before it's added to another script as module. Actually `Script.AddModule` function runs the given compiled script so it can populate values of the global variables.
## Sandbox Environments
To securely compile and execute _potentially_ unsafe script code, you can use the following Script functions.

View file

@ -17,6 +17,7 @@ type Script struct {
variables map[string]*Variable
removedBuiltins map[string]bool
removedStdModules map[string]bool
userModules map[string]*objects.ImmutableMap
userModuleLoader compiler.ModuleLoader
input []byte
}
@ -79,6 +80,31 @@ func (s *Script) SetUserModuleLoader(loader compiler.ModuleLoader) {
s.userModuleLoader = loader
}
// AddModule adds the compiled script as an import module. Note that
// the compiled script must be run at least once before it is added
// to another script.
func (s *Script) AddModule(name string, compiled *Compiled) {
if s.userModules == nil {
s.userModules = make(map[string]*objects.ImmutableMap)
}
mod := &objects.ImmutableMap{
Value: make(map[string]objects.Object),
}
for _, symbolName := range compiled.symbolTable.Names() {
symbol, _, ok := compiled.symbolTable.Resolve(symbolName)
if ok && symbol.Scope == compiler.ScopeGlobal {
value := compiled.machine.Globals()[symbol.Index]
if value != nil {
mod.Value[symbolName] = *value
}
}
}
s.userModules[name] = mod
}
// Compile compiles the script with all the defined variables, and, returns Compiled object.
func (s *Script) Compile() (*Compiled, error) {
symbolTable, stdModules, globals := s.prepCompile()
@ -151,6 +177,9 @@ func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, stdModules ma
stdModules[name] = mod
}
}
for name, mod := range s.userModules {
stdModules[name] = mod
}
globals = make([]*objects.Object, len(names), len(names))

View file

@ -0,0 +1,42 @@
package script_test
import (
"testing"
"github.com/d5/tengo/assert"
"github.com/d5/tengo/script"
)
func TestScript_AddModule(t *testing.T) {
// mod1 module
mod1, err := script.New([]byte(`a := 5`)).Compile()
assert.NoError(t, err)
// script1 imports "mod1"
scr1 := script.New([]byte(`mod1 := import("mod1"); out := mod1.a`))
scr1.AddModule("mod1", mod1) // added before mod1 was run
c, err := scr1.Run()
assert.NoError(t, err)
assert.Nil(t, c.Get("out").Value()) // 'a' is undefined because mod1 was not yet run
err = mod1.Run()
assert.NoError(t, err)
scr1.AddModule("mod1", mod1) // this time, mod1 was run before it's added
c, err = scr1.Run()
assert.NoError(t, err)
assert.Equal(t, int64(5), c.Get("out").Value())
// mod2 module imports "mod1"
mod2Script := script.New([]byte(`mod1 := import("mod1"); b := mod1.a * 2`))
mod2Script.AddModule("mod1", mod1)
mod2, err := mod2Script.Compile()
assert.NoError(t, err)
err = mod2.Run()
assert.NoError(t, err)
// script2 imports "mod2" (which imports "mod1")
scr2 := script.New([]byte(`mod2 := import("mod2"); out := mod2.b`))
scr2.AddModule("mod2", mod2)
c, err = scr2.Run()
assert.NoError(t, err)
assert.Equal(t, int64(10), c.Get("out").Value())
}