From edc23cf2f1f1b51ad183e6edf0d91b1a4ec44ecc Mon Sep 17 00:00:00 2001 From: Daniel Kang Date: Wed, 30 Jan 2019 21:50:15 -0800 Subject: [PATCH] working on script modules (WIP) --- docs/interoperability.md | 20 +++++++++++++++-- script/script.go | 29 +++++++++++++++++++++++++ script/script_module_test.go | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 script/script_module_test.go diff --git a/docs/interoperability.md b/docs/interoperability.md index 0be8bd5..ea7484f 100644 --- a/docs/interoperability.md +++ b/docs/interoperability.md @@ -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. diff --git a/script/script.go b/script/script.go index 781691b..53b38ba 100644 --- a/script/script.go +++ b/script/script.go @@ -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)) diff --git a/script/script_module_test.go b/script/script_module_test.go new file mode 100644 index 0000000..5b3290c --- /dev/null +++ b/script/script_module_test.go @@ -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()) +}