Merge pull request #74 from d5/iarrlen

Some bug fixes
This commit is contained in:
Daniel 2019-02-02 23:21:07 -08:00 committed by GitHub
commit d90f2865ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 569 additions and 441 deletions

View file

@ -121,8 +121,8 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool
if expected != actual.(rune) { if expected != actual.(rune) {
return failExpectedActual(t, expected, actual, msg...) return failExpectedActual(t, expected, actual, msg...)
} }
case compiler.Symbol: case *compiler.Symbol:
if !equalSymbol(expected, actual.(compiler.Symbol)) { if !equalSymbol(expected, actual.(*compiler.Symbol)) {
return failExpectedActual(t, expected, actual, msg...) return failExpectedActual(t, expected, actual, msg...)
} }
case source.Pos: case source.Pos:
@ -238,7 +238,7 @@ func equalIntSlice(a, b []int) bool {
return true return true
} }
func equalSymbol(a, b compiler.Symbol) bool { func equalSymbol(a, b *compiler.Symbol) bool {
return a.Name == b.Name && return a.Name == b.Name &&
a.Index == b.Index && a.Index == b.Index &&
a.Scope == b.Scope a.Scope == b.Scope

View file

@ -0,0 +1,27 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
)
// ExportStmt represents an export statement.
type ExportStmt struct {
ExportPos source.Pos
Result Expr
}
func (s *ExportStmt) stmtNode() {}
// Pos returns the position of first character belonging to the node.
func (s *ExportStmt) Pos() source.Pos {
return s.ExportPos
}
// End returns the position of first character immediately after the node.
func (s *ExportStmt) End() source.Pos {
return s.Result.End()
}
func (s *ExportStmt) String() string {
return "export " + s.Result.String()
}

View file

@ -5,4 +5,5 @@ package compiler
type CompilationScope struct { type CompilationScope struct {
instructions []byte instructions []byte
lastInstructions [2]EmittedInstruction lastInstructions [2]EmittedInstruction
symbolInit map[string]bool
} }

View file

@ -21,7 +21,7 @@ type Compiler struct {
scopeIndex int scopeIndex int
moduleLoader ModuleLoader moduleLoader ModuleLoader
stdModules map[string]*objects.ImmutableMap stdModules map[string]*objects.ImmutableMap
compiledModules map[string]*objects.CompiledModule compiledModules map[string]*objects.CompiledFunction
loops []*Loop loops []*Loop
loopIndex int loopIndex int
trace io.Writer trace io.Writer
@ -60,7 +60,7 @@ func NewCompiler(symbolTable *SymbolTable, stdModules map[string]*objects.Immuta
loopIndex: -1, loopIndex: -1,
trace: trace, trace: trace,
stdModules: stdModules, stdModules: stdModules,
compiledModules: make(map[string]*objects.CompiledModule), compiledModules: make(map[string]*objects.CompiledFunction),
} }
} }
@ -383,7 +383,10 @@ func (c *Compiler) Compile(node ast.Node) error {
c.enterScope() c.enterScope()
for _, p := range node.Type.Params.List { for _, p := range node.Type.Params.List {
c.symbolTable.Define(p.Name) s := c.symbolTable.Define(p.Name)
// function arguments is not assigned directly.
s.LocalAssigned = true
} }
if err := c.Compile(node.Body); err != nil { if err := c.Compile(node.Body); err != nil {
@ -402,6 +405,50 @@ func (c *Compiler) Compile(node ast.Node) error {
for _, s := range freeSymbols { for _, s := range freeSymbols {
switch s.Scope { switch s.Scope {
case ScopeLocal: case ScopeLocal:
if !s.LocalAssigned {
// Here, the closure is capturing a local variable that's not yet assigned its value.
// One example is a local recursive function:
//
// func() {
// foo := func(x) {
// // ..
// return foo(x-1)
// }
// }
//
// which translate into
//
// 0000 GETL 0
// 0002 CLOSURE ? 1
// 0006 DEFL 0
//
// . So the local variable (0) is being captured before it's assigned the value.
//
// Solution is to transform the code into something like this:
//
// func() {
// foo := undefined
// foo = func(x) {
// // ..
// return foo(x-1)
// }
// }
//
// that is equivalent to
//
// 0000 NULL
// 0001 DEFL 0
// 0003 GETL 0
// 0005 CLOSURE ? 1
// 0009 SETL 0
//
c.emit(OpNull)
c.emit(OpDefineLocal, s.Index)
s.LocalAssigned = true
}
c.emit(OpGetLocal, s.Index) c.emit(OpGetLocal, s.Index)
case ScopeFree: case ScopeFree:
c.emit(OpGetFree, s.Index) c.emit(OpGetFree, s.Index)
@ -461,9 +508,28 @@ func (c *Compiler) Compile(node ast.Node) error {
return err return err
} }
c.emit(OpModule, c.addConstant(userMod)) c.emit(OpConstant, c.addConstant(userMod))
c.emit(OpCall, 0)
} }
case *ast.ExportStmt:
// export statement must be in top-level scope
if c.scopeIndex != 0 {
return fmt.Errorf("cannot use 'export' inside function")
}
// export statement is simply ignore when compiling non-module code
if c.parent == nil {
break
}
if err := c.Compile(node.Result); err != nil {
return err
}
c.emit(OpImmutable)
c.emit(OpReturnValue)
case *ast.ErrorExpr: case *ast.ErrorExpr:
if err := c.Compile(node.Expr); err != nil { if err := c.Compile(node.Expr); err != nil {
return err return err

View file

@ -105,12 +105,15 @@ func (c *Compiler) compileAssign(lhs, rhs []ast.Expr, op token.Token) error {
if numSel > 0 { if numSel > 0 {
c.emit(OpSetSelLocal, symbol.Index, numSel) c.emit(OpSetSelLocal, symbol.Index, numSel)
} else { } else {
if op == token.Define { if op == token.Define && !symbol.LocalAssigned {
c.emit(OpDefineLocal, symbol.Index) c.emit(OpDefineLocal, symbol.Index)
} else { } else {
c.emit(OpSetLocal, symbol.Index) c.emit(OpSetLocal, symbol.Index)
} }
} }
// mark the symbol as local-assigned
symbol.LocalAssigned = true
case ScopeFree: case ScopeFree:
if numSel > 0 { if numSel > 0 {
c.emit(OpSetSelFree, symbol.Index, numSel) c.emit(OpSetSelFree, symbol.Index, numSel)

View file

@ -14,7 +14,7 @@ var (
fileSet = source.NewFileSet() fileSet = source.NewFileSet()
) )
func (c *Compiler) compileModule(moduleName string) (*objects.CompiledModule, error) { func (c *Compiler) compileModule(moduleName string) (*objects.CompiledFunction, error) {
compiledModule, exists := c.loadCompiledModule(moduleName) compiledModule, exists := c.loadCompiledModule(moduleName)
if exists { if exists {
return compiledModule, nil return compiledModule, nil
@ -69,7 +69,7 @@ func (c *Compiler) checkCyclicImports(moduleName string) error {
return nil return nil
} }
func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.CompiledModule, error) { func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.CompiledFunction, error) {
p := parser.NewParser(fileSet.AddFile(moduleName, -1, len(src)), src, nil) p := parser.NewParser(fileSet.AddFile(moduleName, -1, len(src)), src, nil)
file, err := p.ParseFile() file, err := p.ParseFile()
if err != nil { if err != nil {
@ -77,27 +77,36 @@ func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.Comp
} }
symbolTable := NewSymbolTable() symbolTable := NewSymbolTable()
globals := make(map[string]int)
// inherit builtin functions
for idx, fn := range objects.Builtins {
s, _, ok := c.symbolTable.Resolve(fn.Name)
if ok && s.Scope == ScopeBuiltin {
symbolTable.DefineBuiltin(idx, fn.Name)
}
}
// no global scope for the module
symbolTable = symbolTable.Fork(false)
// compile module
moduleCompiler := c.fork(moduleName, symbolTable) moduleCompiler := c.fork(moduleName, symbolTable)
if err := moduleCompiler.Compile(file); err != nil { if err := moduleCompiler.Compile(file); err != nil {
return nil, err return nil, err
} }
for _, name := range symbolTable.Names() { // add OpReturn (== export undefined) if export is missing
symbol, _, _ := symbolTable.Resolve(name) if !moduleCompiler.lastInstructionIs(OpReturnValue) {
if symbol.Scope == ScopeGlobal { moduleCompiler.emit(OpReturn)
globals[name] = symbol.Index
}
} }
return &objects.CompiledModule{ return &objects.CompiledFunction{
Instructions: moduleCompiler.Bytecode().Instructions, Instructions: moduleCompiler.Bytecode().Instructions,
Globals: globals, NumLocals: symbolTable.MaxSymbols(),
}, nil }, nil
} }
func (c *Compiler) loadCompiledModule(moduleName string) (mod *objects.CompiledModule, ok bool) { func (c *Compiler) loadCompiledModule(moduleName string) (mod *objects.CompiledFunction, ok bool) {
if c.parent != nil { if c.parent != nil {
return c.parent.loadCompiledModule(moduleName) return c.parent.loadCompiledModule(moduleName)
} }
@ -107,7 +116,7 @@ func (c *Compiler) loadCompiledModule(moduleName string) (mod *objects.CompiledM
return return
} }
func (c *Compiler) storeCompiledModule(moduleName string, module *objects.CompiledModule) { func (c *Compiler) storeCompiledModule(moduleName string, module *objects.CompiledFunction) {
if c.parent != nil { if c.parent != nil {
c.parent.storeCompiledModule(moduleName, module) c.parent.storeCompiledModule(moduleName, module)
} }

View file

@ -7,6 +7,7 @@ func (c *Compiler) currentInstructions() []byte {
func (c *Compiler) enterScope() { func (c *Compiler) enterScope() {
scope := CompilationScope{ scope := CompilationScope{
instructions: make([]byte, 0), instructions: make([]byte, 0),
symbolInit: make(map[string]bool),
} }
c.scopes = append(c.scopes, scope) c.scopes = append(c.scopes, scope)

View file

@ -41,6 +41,7 @@ const (
OpCall // Call function OpCall // Call function
OpReturn // Return OpReturn // Return
OpReturnValue // Return value OpReturnValue // Return value
OpExport // Export
OpGetGlobal // Get global variable OpGetGlobal // Get global variable
OpSetGlobal // Set global variable OpSetGlobal // Set global variable
OpSetSelGlobal // Set global variable using selectors OpSetSelGlobal // Set global variable using selectors
@ -57,7 +58,6 @@ const (
OpIteratorNext // Iterator next OpIteratorNext // Iterator next
OpIteratorKey // Iterator key OpIteratorKey // Iterator key
OpIteratorValue // Iterator value OpIteratorValue // Iterator value
OpModule // Module
) )
// OpcodeNames is opcode names. // OpcodeNames is opcode names.
@ -101,6 +101,7 @@ var OpcodeNames = [...]string{
OpCall: "CALL", OpCall: "CALL",
OpReturn: "RET", OpReturn: "RET",
OpReturnValue: "RETVAL", OpReturnValue: "RETVAL",
OpExport: "EXPORT",
OpGetLocal: "GETL", OpGetLocal: "GETL",
OpSetLocal: "SETL", OpSetLocal: "SETL",
OpDefineLocal: "DEFL", OpDefineLocal: "DEFL",
@ -114,7 +115,6 @@ var OpcodeNames = [...]string{
OpIteratorNext: "ITNXT", OpIteratorNext: "ITNXT",
OpIteratorKey: "ITKEY", OpIteratorKey: "ITKEY",
OpIteratorValue: "ITVAL", OpIteratorValue: "ITVAL",
OpModule: "MODULE",
} }
// OpcodeOperands is the number of operands. // OpcodeOperands is the number of operands.
@ -158,6 +158,7 @@ var OpcodeOperands = [...][]int{
OpCall: {1}, OpCall: {1},
OpReturn: {}, OpReturn: {},
OpReturnValue: {}, OpReturnValue: {},
OpExport: {},
OpGetLocal: {1}, OpGetLocal: {1},
OpSetLocal: {1}, OpSetLocal: {1},
OpDefineLocal: {1}, OpDefineLocal: {1},
@ -171,7 +172,6 @@ var OpcodeOperands = [...][]int{
OpIteratorNext: {}, OpIteratorNext: {},
OpIteratorKey: {}, OpIteratorKey: {},
OpIteratorValue: {}, OpIteratorValue: {},
OpModule: {2},
} }
// ReadOperands reads operands from the bytecode. // ReadOperands reads operands from the bytecode.

View file

@ -628,6 +628,8 @@ func (p *Parser) parseStmt() (stmt ast.Stmt) {
return s return s
case token.Return: case token.Return:
return p.parseReturnStmt() return p.parseReturnStmt()
case token.Export:
return p.parseExportStmt()
case token.If: case token.If:
return p.parseIfStmt() return p.parseIfStmt()
case token.For: case token.For:
@ -874,6 +876,23 @@ func (p *Parser) parseReturnStmt() ast.Stmt {
} }
} }
func (p *Parser) parseExportStmt() ast.Stmt {
if p.trace {
defer un(trace(p, "ExportStmt"))
}
pos := p.pos
p.expect(token.Export)
x := p.parseExpr()
p.expectSemi()
return &ast.ExportStmt{
ExportPos: pos,
Result: x,
}
}
func (p *Parser) parseSimpleStmt(forIn bool) ast.Stmt { func (p *Parser) parseSimpleStmt(forIn bool) ast.Stmt {
if p.trace { if p.trace {
defer un(trace(p, "SimpleStmt")) defer un(trace(p, "SimpleStmt"))

View file

@ -8,23 +8,6 @@ import (
) )
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
// TODO: function declaration currently not parsed.
// All functions are parsed as function literal instead.
// In Go, function declaration is parsed only at the top level.
//expect(t, "func a(b, c, d) {}", func(p pfn) []ast.Stmt {
// return stmts(
// declStmt(
// funcDecl(
// ident("a", p(1, 6)),
// funcType(
// identList(p(1, 7), p(1, 15),
// ident("b", p(1, 8)),
// ident("c", p(1, 11)),
// ident("d", p(1, 14))),
// p(1, 12)),
// blockStmt(p(1, 17), p(1, 18)))))
//})
expect(t, "a = func(b, c, d) { return d }", func(p pfn) []ast.Stmt { expect(t, "a = func(b, c, d) { return d }", func(p pfn) []ast.Stmt {
return stmts( return stmts(
assignStmt( assignStmt(

View file

@ -8,4 +8,5 @@ var stmtStart = map[token.Token]bool{
token.For: true, token.For: true,
token.If: true, token.If: true,
token.Return: true, token.Return: true,
token.Export: true,
} }

View file

@ -77,7 +77,7 @@ func (s *Scanner) Scan() (tok token.Token, literal string, pos source.Pos) {
literal = s.scanIdentifier() literal = s.scanIdentifier()
tok = token.Lookup(literal) tok = token.Lookup(literal)
switch tok { switch tok {
case token.Ident, token.Break, token.Continue, token.Return, token.True, token.False, token.Undefined: case token.Ident, token.Break, token.Continue, token.Return, token.Export, token.True, token.False, token.Undefined:
insertSemi = true insertSemi = true
} }
case '0' <= ch && ch <= '9': case '0' <= ch && ch <= '9':

View file

@ -116,6 +116,7 @@ func TestScanner_Scan(t *testing.T) {
{token.Func, "func"}, {token.Func, "func"},
{token.If, "if"}, {token.If, "if"},
{token.Return, "return"}, {token.Return, "return"},
{token.Export, "export"},
} }
// combine // combine

View file

@ -2,7 +2,8 @@ package compiler
// Symbol represents a symbol in the symbol table. // Symbol represents a symbol in the symbol table.
type Symbol struct { type Symbol struct {
Name string Name string
Scope SymbolScope Scope SymbolScope
Index int Index int
LocalAssigned bool // if the local symbol is assigned at least once
} }

View file

@ -4,22 +4,22 @@ package compiler
type SymbolTable struct { type SymbolTable struct {
parent *SymbolTable parent *SymbolTable
block bool block bool
store map[string]Symbol store map[string]*Symbol
numDefinition int numDefinition int
maxDefinition int maxDefinition int
freeSymbols []Symbol freeSymbols []*Symbol
} }
// NewSymbolTable creates a SymbolTable. // NewSymbolTable creates a SymbolTable.
func NewSymbolTable() *SymbolTable { func NewSymbolTable() *SymbolTable {
return &SymbolTable{ return &SymbolTable{
store: make(map[string]Symbol), store: make(map[string]*Symbol),
} }
} }
// Define adds a new symbol in the current scope. // Define adds a new symbol in the current scope.
func (t *SymbolTable) Define(name string) Symbol { func (t *SymbolTable) Define(name string) *Symbol {
symbol := Symbol{Name: name, Index: t.nextIndex()} symbol := &Symbol{Name: name, Index: t.nextIndex()}
t.numDefinition++ t.numDefinition++
if t.Parent(true) == nil { if t.Parent(true) == nil {
@ -36,8 +36,8 @@ func (t *SymbolTable) Define(name string) Symbol {
} }
// DefineBuiltin adds a symbol for builtin function. // DefineBuiltin adds a symbol for builtin function.
func (t *SymbolTable) DefineBuiltin(index int, name string) Symbol { func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
symbol := Symbol{ symbol := &Symbol{
Name: name, Name: name,
Index: index, Index: index,
Scope: ScopeBuiltin, Scope: ScopeBuiltin,
@ -49,7 +49,7 @@ func (t *SymbolTable) DefineBuiltin(index int, name string) Symbol {
} }
// Resolve resolves a symbol with a given name. // Resolve resolves a symbol with a given name.
func (t *SymbolTable) Resolve(name string) (symbol Symbol, depth int, ok bool) { func (t *SymbolTable) Resolve(name string) (symbol *Symbol, depth int, ok bool) {
symbol, ok = t.store[name] symbol, ok = t.store[name]
if !ok && t.parent != nil { if !ok && t.parent != nil {
symbol, depth, ok = t.parent.Resolve(name) symbol, depth, ok = t.parent.Resolve(name)
@ -76,7 +76,7 @@ func (t *SymbolTable) Resolve(name string) (symbol Symbol, depth int, ok bool) {
// Fork creates a new symbol table for a new scope. // Fork creates a new symbol table for a new scope.
func (t *SymbolTable) Fork(block bool) *SymbolTable { func (t *SymbolTable) Fork(block bool) *SymbolTable {
return &SymbolTable{ return &SymbolTable{
store: make(map[string]Symbol), store: make(map[string]*Symbol),
parent: t, parent: t,
block: block, block: block,
} }
@ -97,7 +97,7 @@ func (t *SymbolTable) MaxSymbols() int {
} }
// FreeSymbols returns free symbols for the scope. // FreeSymbols returns free symbols for the scope.
func (t *SymbolTable) FreeSymbols() []Symbol { func (t *SymbolTable) FreeSymbols() []*Symbol {
return t.freeSymbols return t.freeSymbols
} }
@ -128,12 +128,12 @@ func (t *SymbolTable) updateMaxDefs(numDefs int) {
} }
} }
func (t *SymbolTable) defineFree(original Symbol) Symbol { func (t *SymbolTable) defineFree(original *Symbol) *Symbol {
// TODO: should we check duplicates? // TODO: should we check duplicates?
t.freeSymbols = append(t.freeSymbols, original) t.freeSymbols = append(t.freeSymbols, original)
symbol := Symbol{ symbol := &Symbol{
Name: original.Name, Name: original.Name,
Index: len(t.freeSymbols) - 1, Index: len(t.freeSymbols) - 1,
Scope: ScopeFree, Scope: ScopeFree,

View file

@ -91,23 +91,23 @@ func TestSymbolTable(t *testing.T) {
resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 2) resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 2)
} }
func symbol(name string, scope compiler.SymbolScope, index int) compiler.Symbol { func symbol(name string, scope compiler.SymbolScope, index int) *compiler.Symbol {
return compiler.Symbol{ return &compiler.Symbol{
Name: name, Name: name,
Scope: scope, Scope: scope,
Index: index, Index: index,
} }
} }
func globalSymbol(name string, index int) compiler.Symbol { func globalSymbol(name string, index int) *compiler.Symbol {
return symbol(name, compiler.ScopeGlobal, index) return symbol(name, compiler.ScopeGlobal, index)
} }
func localSymbol(name string, index int) compiler.Symbol { func localSymbol(name string, index int) *compiler.Symbol {
return symbol(name, compiler.ScopeLocal, index) return symbol(name, compiler.ScopeLocal, index)
} }
func freeSymbol(name string, index int) compiler.Symbol { func freeSymbol(name string, index int) *compiler.Symbol {
return symbol(name, compiler.ScopeFree, index) return symbol(name, compiler.ScopeFree, index)
} }
@ -115,7 +115,7 @@ func symbolTable() *compiler.SymbolTable {
return compiler.NewSymbolTable() return compiler.NewSymbolTable()
} }
func resolveExpect(t *testing.T, symbolTable *compiler.SymbolTable, name string, expectedSymbol compiler.Symbol, expectedDepth int) { func resolveExpect(t *testing.T, symbolTable *compiler.SymbolTable, name string, expectedSymbol *compiler.Symbol, expectedDepth int) {
actualSymbol, actualDepth, ok := symbolTable.Resolve(name) actualSymbol, actualDepth, ok := symbolTable.Resolve(name)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, expectedSymbol, actualSymbol) assert.Equal(t, expectedSymbol, actualSymbol)

View file

@ -76,6 +76,7 @@ const (
Immutable Immutable
If If
Return Return
Export
True True
False False
In In
@ -149,6 +150,7 @@ var tokens = [...]string{
Immutable: "immutable", Immutable: "immutable",
If: "if", If: "if",
Return: "return", Return: "return",
Export: "export",
True: "true", True: "true",
False: "false", False: "false",
In: "in", In: "in",

View file

@ -5,7 +5,6 @@
- [Using Scripts](#using-scripts) - [Using Scripts](#using-scripts)
- [Type Conversion Table](#type-conversion-table) - [Type Conversion Table](#type-conversion-table)
- [User Types](#user-types) - [User Types](#user-types)
- [Importing Scripts](#importing-scripts)
- [Sandbox Environments](#sandbox-environments) - [Sandbox Environments](#sandbox-environments)
- [Compiler and VM](#compiler-and-vm) - [Compiler and VM](#compiler-and-vm)
@ -115,20 +114,6 @@ When adding a Variable _([Script.Add](https://godoc.org/github.com/d5/tengo/scri
Users can add and use a custom user type in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo/objects#Object) interface. Tengo runtime will treat the user types in the same way it does to the runtime types with no performance overhead. See [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details. Users can add and use a custom user type in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo/objects#Object) interface. Tengo runtime will treat the user types in the same way it does to the runtime types with no performance overhead. See [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details.
### Importing Scripts
A script can import and use another script in the same way it can load the standard library or the user module. `Script.AddModule` function adds another script as a named module.
```golang
mod1Script := script.New([]byte(`a := 5`)) // mod1 script
mainScript := script.New([]byte(`print(import("mod1").a)`)) // main script
mainScript.AddModule("mod1", mod1Script) // add mod1 using name "mod1"
mainScript.Run() // prints "5"
```
Note that the script modules added using `Script.AddModule` will be compiled and run right before the main script is compiled.
## Sandbox Environments ## Sandbox Environments
To securely compile and execute _potentially_ unsafe script code, you can use the following Script functions. To securely compile and execute _potentially_ unsafe script code, you can use the following Script functions.
@ -145,6 +130,8 @@ s.DisableBuiltinFunction("print")
_, err := s.Run() // compile error _, err := s.Run() // compile error
``` ```
Note that when a script is being added to another script as a module (via `Script.AddModule`), it does not inherit the disabled builtin function list from the main script.
#### Script.DisableStdModule(name string) #### Script.DisableStdModule(name string)
DisableStdModule disables a [standard library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) module. Compile will report a compile-time error if the code tries to import the module with the given name. DisableStdModule disables a [standard library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) module. Compile will report a compile-time error if the code tries to import the module with the given name.
@ -157,6 +144,8 @@ s.DisableStdModule("exec")
_, err := s.Run() // compile error _, err := s.Run() // compile error
``` ```
Note that when a script is being added to another script as a module (via `Script.AddModule`), it does not inherit the disabled standard module list from the main script.
#### Script.SetUserModuleLoader(loader compiler.ModuleLoader) #### Script.SetUserModuleLoader(loader compiler.ModuleLoader)
SetUserModuleLoader replaces the default user-module loader of the compiler, which tries to read the source from a local file. SetUserModuleLoader replaces the default user-module loader of the compiler, which tries to read the source from a local file.
@ -173,6 +162,8 @@ s.SetUserModuleLoader(func(moduleName string) ([]byte, error) {
}) })
``` ```
Note that when a script is being added to another script as a module (via `Script.AddModule`), it does not inherit the module loader from the main script.
## Compiler and VM ## Compiler and VM
Although it's not recommended, you can directly create and run the Tengo [Parser](https://godoc.org/github.com/d5/tengo/compiler/parser#Parser), [Compiler](https://godoc.org/github.com/d5/tengo/compiler#Compiler), and [VM](https://godoc.org/github.com/d5/tengo/runtime#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally. Although it's not recommended, you can directly create and run the Tengo [Parser](https://godoc.org/github.com/d5/tengo/compiler/parser#Parser), [Compiler](https://godoc.org/github.com/d5/tengo/compiler#Compiler), and [VM](https://godoc.org/github.com/d5/tengo/runtime#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally.

View file

@ -217,22 +217,32 @@ if is_error(err1) { // 'is_error' builtin function
You can load other scripts as modules using `import` expression. You can load other scripts as modules using `import` expression.
Main script: Main script:
```golang
mod1 := import("./mod1") // assuming mod1.tengo file exists in the current directory
// same as 'import("./mod1.tengo")' or 'import("mod1")'
mod1.func1(a) // module function
a += mod1.foo // module variable
//mod1.foo = 5 // error: module variables are read-only
```
`mod1.tengo` file:
```golang ```golang
func1 := func(x) { print(x) } sum := import("./sum") // assuming sum.tengo file exists in the current directory
foo := 2 // same as 'import("./sum.tengo")' or 'import("sum")'
print(sum(10)) // module function
``` ```
Basically, `import` expression returns all the global variables defined in the module as an ImmutableMap value. `sum.tengo` file:
```golang
base := 5
export func(x) {
return x + base
}
```
In Tengo, modules are very similar to functions.
- `import` expression loads the module and execute like a function.
- Module should return a value using `export` statement.
- Module can return `export` any Tengo objects: int, string, map, array, function, etc.
- `export` in a module is like `return` in a function: it stops execution and return a value to the importing code.
- `export`-ed values are always immutable.
- If the module does not have any `export` statement, `import` expression simply returns `undefined`. _(Just like the function that has no `return`.)_
- Note that `export` statement is completely ignored and not evaluated if the code is executed as a regular script.
Also, you can use `import` to load the [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md). Also, you can use `import` to load the [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md).

View file

@ -12,6 +12,8 @@ func builtinLen(args ...Object) (Object, error) {
switch arg := args[0].(type) { switch arg := args[0].(type) {
case *Array: case *Array:
return &Int{Value: int64(len(arg.Value))}, nil return &Int{Value: int64(len(arg.Value))}, nil
case *ImmutableArray:
return &Int{Value: int64(len(arg.Value))}, nil
case *String: case *String:
return &Int{Value: int64(len(arg.Value))}, nil return &Int{Value: int64(len(arg.Value))}, nil
case *Bytes: case *Bytes:

View file

@ -6,8 +6,8 @@ import (
// CompiledModule represents a compiled module. // CompiledModule represents a compiled module.
type CompiledModule struct { type CompiledModule struct {
Instructions []byte // compiled instructions Instructions []byte // compiled instructions
Globals map[string]int // global variable name-to-index map NumGlobals int
} }
// TypeName returns the name of the type. // TypeName returns the name of the type.
@ -27,14 +27,9 @@ func (o *CompiledModule) BinaryOp(op token.Token, rhs Object) (Object, error) {
// Copy returns a copy of the type. // Copy returns a copy of the type.
func (o *CompiledModule) Copy() Object { func (o *CompiledModule) Copy() Object {
globals := make(map[string]int, len(o.Globals))
for name, index := range o.Globals {
globals[name] = index
}
return &CompiledModule{ return &CompiledModule{
Instructions: append([]byte{}, o.Instructions...), Instructions: append([]byte{}, o.Instructions...),
Globals: globals, NumGlobals: o.NumGlobals,
} }
} }

View file

@ -47,10 +47,6 @@ type VM struct {
func NewVM(bytecode *compiler.Bytecode, globals []*objects.Object) *VM { func NewVM(bytecode *compiler.Bytecode, globals []*objects.Object) *VM {
if globals == nil { if globals == nil {
globals = make([]*objects.Object, GlobalsSize) globals = make([]*objects.Object, GlobalsSize)
} else if len(globals) < GlobalsSize {
g := make([]*objects.Object, GlobalsSize)
copy(g, globals)
globals = g
} }
frames := make([]Frame, MaxFrames) frames := make([]Frame, MaxFrames)
@ -648,7 +644,7 @@ func (v *VM) Run() error {
case *objects.Array: case *objects.Array:
numElements := int64(len(left.Value)) numElements := int64(len(left.Value))
if lowIdx < 0 || lowIdx >= numElements { if lowIdx < 0 || lowIdx > numElements {
return fmt.Errorf("index out of bounds: %d", lowIdx) return fmt.Errorf("index out of bounds: %d", lowIdx)
} }
if highIdx < 0 { if highIdx < 0 {
@ -673,7 +669,7 @@ func (v *VM) Run() error {
case *objects.ImmutableArray: case *objects.ImmutableArray:
numElements := int64(len(left.Value)) numElements := int64(len(left.Value))
if lowIdx < 0 || lowIdx >= numElements { if lowIdx < 0 || lowIdx > numElements {
return fmt.Errorf("index out of bounds: %d", lowIdx) return fmt.Errorf("index out of bounds: %d", lowIdx)
} }
if highIdx < 0 { if highIdx < 0 {
@ -698,7 +694,7 @@ func (v *VM) Run() error {
case *objects.String: case *objects.String:
numElements := int64(len(left.Value)) numElements := int64(len(left.Value))
if lowIdx < 0 || lowIdx >= numElements { if lowIdx < 0 || lowIdx > numElements {
return fmt.Errorf("index out of bounds: %d", lowIdx) return fmt.Errorf("index out of bounds: %d", lowIdx)
} }
if highIdx < 0 { if highIdx < 0 {
@ -822,14 +818,15 @@ func (v *VM) Run() error {
v.curIPLimit = len(v.curInsts) - 1 v.curIPLimit = len(v.curInsts) - 1
v.ip = v.curFrame.ip v.ip = v.curFrame.ip
v.sp = lastFrame.basePointer - 1 //v.sp = lastFrame.basePointer - 1
v.sp = lastFrame.basePointer
if v.sp >= StackSize { if v.sp-1 >= StackSize {
return ErrStackOverflow return ErrStackOverflow
} }
v.stack[v.sp] = undefinedPtr v.stack[v.sp-1] = undefinedPtr
v.sp++ //v.sp++
case compiler.OpDefineLocal: case compiler.OpDefineLocal:
localIndex := int(v.curInsts[v.ip+1]) localIndex := int(v.curInsts[v.ip+1])
@ -1005,14 +1002,6 @@ func (v *VM) Run() error {
v.stack[v.sp] = &val v.stack[v.sp] = &val
v.sp++ v.sp++
case compiler.OpModule:
cidx := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
if err := v.importModule(v.constants[cidx].(*objects.CompiledModule)); err != nil {
return err
}
default: default:
return fmt.Errorf("unknown opcode: %d", v.curInsts[v.ip]) return fmt.Errorf("unknown opcode: %d", v.curInsts[v.ip])
} }
@ -1020,7 +1009,7 @@ func (v *VM) Run() error {
// check if stack still has some objects left // check if stack still has some objects left
if v.sp > 0 && atomic.LoadInt64(&v.aborting) == 0 { if v.sp > 0 && atomic.LoadInt64(&v.aborting) == 0 {
return fmt.Errorf("non empty stack after execution") return fmt.Errorf("non empty stack after execution: %d", v.sp)
} }
return nil return nil
@ -1033,7 +1022,7 @@ func (v *VM) Globals() []*objects.Object {
// FrameInfo returns the current function call frame information. // FrameInfo returns the current function call frame information.
func (v *VM) FrameInfo() (frameIndex, ip int) { func (v *VM) FrameInfo() (frameIndex, ip int) {
return v.framesIndex - 1, v.frames[v.framesIndex-1].ip return v.framesIndex - 1, v.ip
} }
func (v *VM) pushClosure(constIndex, numFree int) error { func (v *VM) pushClosure(constIndex, numFree int) error {
@ -1160,35 +1149,6 @@ func (v *VM) callFunction(fn *objects.CompiledFunction, freeVars []*objects.Obje
return nil return nil
} }
// TODO: should reuse *objects.ImmutableMap for the same imports?
func (v *VM) importModule(compiledModule *objects.CompiledModule) error {
// import module is basically to create a new instance of VM
// and run the module code and retrieve all global variables after execution.
moduleVM := NewVM(&compiler.Bytecode{
Instructions: compiledModule.Instructions,
Constants: v.constants,
}, nil)
if err := moduleVM.Run(); err != nil {
return err
}
mmValue := make(map[string]objects.Object)
for name, index := range compiledModule.Globals {
mmValue[name] = *moduleVM.globals[index]
}
var mm objects.Object = &objects.ImmutableMap{Value: mmValue}
if v.sp >= StackSize {
return ErrStackOverflow
}
v.stack[v.sp] = &mm
v.sp++
return nil
}
func indexAssign(dst, src *objects.Object, selectors []*objects.Object) error { func indexAssign(dst, src *objects.Object, selectors []*objects.Object) error {
numSel := len(selectors) numSel := len(selectors)

View file

@ -10,6 +10,14 @@ func TestBuiltinFunction(t *testing.T) {
expect(t, `out = len("")`, 0) expect(t, `out = len("")`, 0)
expect(t, `out = len("four")`, 4) expect(t, `out = len("four")`, 4)
expect(t, `out = len("hello world")`, 11) expect(t, `out = len("hello world")`, 11)
expect(t, `out = len([])`, 0)
expect(t, `out = len([1, 2, 3])`, 3)
expect(t, `out = len({})`, 0)
expect(t, `out = len({a:1, b:2})`, 2)
expect(t, `out = len(immutable([]))`, 0)
expect(t, `out = len(immutable([1, 2, 3]))`, 3)
expect(t, `out = len(immutable({}))`, 0)
expect(t, `out = len(immutable({a:1, b:2}))`, 2)
expectError(t, `len(1)`) expectError(t, `len(1)`)
expectError(t, `len("one", "two")`) expectError(t, `len("one", "two")`)

View file

@ -20,35 +20,35 @@ func TestFunction(t *testing.T) {
expect(t, `x := 10; f := func(x) { return x; }; f(5); out = x;`, 10) expect(t, `x := 10; f := func(x) { return x; }; f(5); out = x;`, 10)
expect(t, ` expect(t, `
f2 := func(a) { f2 := func(a) {
f1 := func(a) { f1 := func(a) {
return a * 2; return a * 2;
};
return f1(a) * 3;
}; };
return f1(a) * 3; out = f2(10);
}; `, 60)
out = f2(10);
`, 60)
// closures // closures
expect(t, ` expect(t, `
newAdder := func(x) { newAdder := func(x) {
return func(y) { return x + y }; return func(y) { return x + y };
}; };
add2 := newAdder(2); add2 := newAdder(2);
out = add2(5); out = add2(5);
`, 7) `, 7)
// function as a argument // function as a argument
expect(t, ` expect(t, `
add := func(a, b) { return a + b }; add := func(a, b) { return a + b };
sub := func(a, b) { return a - b }; sub := func(a, b) { return a - b };
applyFunc := func(a, b, f) { return f(a, b) }; applyFunc := func(a, b, f) { return f(a, b) };
out = applyFunc(applyFunc(2, 2, add), 3, sub); out = applyFunc(applyFunc(2, 2, add), 3, sub);
`, 1) `, 1)
expect(t, `f1 := func() { return 5 + 10; }; out = f1();`, 15) expect(t, `f1 := func() { return 5 + 10; }; out = f1();`, 15)
expect(t, `f1 := func() { return 1 }; f2 := func() { return 2 }; out = f1() + f2()`, 3) expect(t, `f1 := func() { return 1 }; f2 := func() { return 2 }; out = f1() + f2()`, 3)
@ -60,170 +60,163 @@ out = applyFunc(applyFunc(2, 2, add), 3, sub);
expect(t, `three := func() { one := 1; two := 2; return one + two }; out = three()`, 3) expect(t, `three := func() { one := 1; two := 2; return one + two }; out = three()`, 3)
expect(t, `three := func() { one := 1; two := 2; return one + two }; seven := func() { three := 3; four := 4; return three + four }; out = three() + seven()`, 10) expect(t, `three := func() { one := 1; two := 2; return one + two }; seven := func() { three := 3; four := 4; return three + four }; out = three() + seven()`, 10)
expect(t, ` expect(t, `
foo1 := func() { foo1 := func() {
foo := 50 foo := 50
return foo return foo
} }
foo2 := func() { foo2 := func() {
foo := 100 foo := 100
return foo return foo
} }
out = foo1() + foo2()`, 150) out = foo1() + foo2()`, 150)
expect(t, ` expect(t, `
g := 50; g := 50;
minusOne := func() { minusOne := func() {
n := 1; n := 1;
return g - n; return g - n;
}; };
minusTwo := func() { minusTwo := func() {
n := 2; n := 2;
return g - n; return g - n;
}; };
out = minusOne() + minusTwo() out = minusOne() + minusTwo()
`, 97) `, 97)
expect(t, ` expect(t, `
f1 := func() { f1 := func() {
f2 := func() { return 1; } f2 := func() { return 1; }
return f2 return f2
}; };
out = f1()() out = f1()()
`, 1) `, 1)
expect(t, ` expect(t, `
f1 := func(a) { return a; }; f1 := func(a) { return a; };
out = f1(4)`, 4) out = f1(4)`, 4)
expect(t, ` expect(t, `
f1 := func(a, b) { return a + b; }; f1 := func(a, b) { return a + b; };
out = f1(1, 2)`, 3) out = f1(1, 2)`, 3)
expect(t, ` expect(t, `
sum := func(a, b) { sum := func(a, b) {
c := a + b; c := a + b;
return c; return c;
}; };
out = sum(1, 2);`, 3) out = sum(1, 2);`, 3)
expect(t, ` expect(t, `
sum := func(a, b) { sum := func(a, b) {
c := a + b; c := a + b;
return c; return c;
}; };
out = sum(1, 2) + sum(3, 4);`, 10) out = sum(1, 2) + sum(3, 4);`, 10)
expect(t, ` expect(t, `
sum := func(a, b) { sum := func(a, b) {
c := a + b c := a + b
return c return c
}; };
outer := func() { outer := func() {
return sum(1, 2) + sum(3, 4) return sum(1, 2) + sum(3, 4)
}; };
out = outer();`, 10) out = outer();`, 10)
expect(t, ` expect(t, `
g := 10; g := 10;
sum := func(a, b) { sum := func(a, b) {
c := a + b; c := a + b;
return c + g; return c + g;
} }
outer := func() { outer := func() {
return sum(1, 2) + sum(3, 4) + g; return sum(1, 2) + sum(3, 4) + g;
} }
out = outer() + g out = outer() + g
`, 50) `, 50)
expectError(t, `func() { return 1; }(1)`) expectError(t, `func() { return 1; }(1)`)
expectError(t, `func(a) { return a; }()`) expectError(t, `func(a) { return a; }()`)
expectError(t, `func(a, b) { return a + b; }(1)`) expectError(t, `func(a, b) { return a + b; }(1)`)
expect(t, ` expect(t, `
f1 := func(a) { f1 := func(a) {
return func() { return a; }; return func() { return a; };
};
f2 := f1(99);
out = f2()
`, 99)
expect(t, `
f1 := func(a, b) {
return func(c) { return a + b + c };
};
f2 := f1(1, 2);
out = f2(8);
`, 11)
expect(t, `
f1 := func(a, b) {
c := a + b;
return func(d) { return c + d };
};
f2 := f1(1, 2);
out = f2(8);
`, 11)
expect(t, `
f1 := func(a, b) {
c := a + b;
return func(d) {
e := d + c;
return func(f) { return e + f };
}
};
f2 := f1(1, 2);
f3 := f2(3);
out = f3(8);
`, 14)
expect(t, `
a := 1;
f1 := func(b) {
return func(c) {
return func(d) { return a + b + c + d }
}; };
}; f2 := f1(99);
f2 := f1(2); out = f2()
f3 := f2(3); `, 99)
out = f3(8);
`, 14)
expect(t, `
f1 := func(a, b) {
one := func() { return a; };
two := func() { return b; };
return func() { return one() + two(); }
};
f2 := f1(9, 90);
out = f2();
`, 99)
// recursion
expect(t, ` expect(t, `
fib := func(x) { f1 := func(a, b) {
if x == 0 { return func(c) { return a + b + c };
return 0 };
} else if x == 1 {
return 1 f2 := f1(1, 2);
} else { out = f2(8);
return fib(x-1) + fib(x-2) `, 11)
} expect(t, `
} f1 := func(a, b) {
out = fib(15)`, 610) c := a + b;
return func(d) { return c + d };
};
f2 := f1(1, 2);
out = f2(8);
`, 11)
expect(t, `
f1 := func(a, b) {
c := a + b;
return func(d) {
e := d + c;
return func(f) { return e + f };
}
};
f2 := f1(1, 2);
f3 := f2(3);
out = f3(8);
`, 14)
expect(t, `
a := 1;
f1 := func(b) {
return func(c) {
return func(d) { return a + b + c + d }
};
};
f2 := f1(2);
f3 := f2(3);
out = f3(8);
`, 14)
expect(t, `
f1 := func(a, b) {
one := func() { return a; };
two := func() { return b; };
return func() { return one() + two(); }
};
f2 := f1(9, 90);
out = f2();
`, 99)
// TODO: currently recursion inside the local scope function definition is not supported. // global function recursion
// Workaround is to define the identifier first then assign the function like below.
// Want to fix this.
expect(t, ` expect(t, `
func() { fib := func(x) {
fib := 0 if x == 0 {
fib = func(x) { return 0
if x == 0 { } else if x == 1 {
return 0 return 1
} else if x == 1 { } else {
return 1 return fib(x-1) + fib(x-2)
} else { }
return fib(x-1) + fib(x-2)
} }
out = fib(15)`, 610)
// local function recursion
expect(t, `
out = func() {
sum := func(x) {
return x == 0 ? 0 : x + sum(x-1)
} }
out = fib(15) return sum(5)
}()`, 610) }()`, 15)
expectError(t, `return 5`)
} }

View file

@ -1,8 +1,12 @@
package runtime_test package runtime_test
import "testing" import (
"testing"
func TestModule(t *testing.T) { "github.com/d5/tengo/objects"
)
func TestStdLib(t *testing.T) {
// stdlib // stdlib
expect(t, `math := import("math"); out = math.abs(1)`, 1.0) expect(t, `math := import("math"); out = math.abs(1)`, 1.0)
expect(t, `math := import("math"); out = math.abs(-1)`, 1.0) expect(t, `math := import("math"); out = math.abs(-1)`, 1.0)
@ -54,27 +58,101 @@ if !is_error(cmd) {
} }
`, []byte("foo bar\n")) `, []byte("foo bar\n"))
}
func TestUserModules(t *testing.T) {
// user modules // user modules
expectWithUserModules(t, `out = import("mod1").bar()`, 5.0, map[string]string{
"mod1": `bar := func() { return 5.0 }`, // export none
expectWithUserModules(t, `out = import("mod1")`, objects.UndefinedValue, map[string]string{
"mod1": `fn := func() { return 5.0 }; a := 2`,
}) })
// export values
expectWithUserModules(t, `out = import("mod1")`, 5, map[string]string{
"mod1": `export 5`,
})
expectWithUserModules(t, `out = import("mod1")`, "foo", map[string]string{
"mod1": `export "foo"`,
})
// export composite types
expectWithUserModules(t, `out = import("mod1")`, IARR{1, 2, 3}, map[string]string{
"mod1": `export [1, 2, 3]`,
})
expectWithUserModules(t, `out = import("mod1")`, IMAP{"a": 1, "b": 2}, map[string]string{
"mod1": `export {a: 1, b: 2}`,
})
// export value is immutable
expectErrorWithUserModules(t, `m1 := import("mod1"); m1.a = 5`, map[string]string{
"mod1": `export {a: 1, b: 2}`,
})
expectErrorWithUserModules(t, `m1 := import("mod1"); m1[1] = 5`, map[string]string{
"mod1": `export [1, 2, 3]`,
})
// code after export statement will not be executed
expectWithUserModules(t, `out = import("mod1")`, 10, map[string]string{
"mod1": `a := 10; export a; a = 20`,
})
expectWithUserModules(t, `out = import("mod1")`, 10, map[string]string{
"mod1": `a := 10; export a; a = 20; export a`,
})
// export function
expectWithUserModules(t, `out = import("mod1")()`, 5.0, map[string]string{
"mod1": `export func() { return 5.0 }`,
})
// export function that reads module-global variable
expectWithUserModules(t, `out = import("mod1")()`, 6.5, map[string]string{
"mod1": `a := 1.5; export func() { return a + 5.0 }`,
})
// export function that read local variable
expectWithUserModules(t, `out = import("mod1")()`, 6.5, map[string]string{
"mod1": `export func() { a := 1.5; return a + 5.0 }`,
})
// export function that read free variables
expectWithUserModules(t, `out = import("mod1")()`, 6.5, map[string]string{
"mod1": `export func() { a := 1.5; return func() { return a + 5.0 }() }`,
})
// recursive function in module
expectWithUserModules(t, `out = import("mod1")`, 15, map[string]string{
"mod1": `
a := func(x) {
return x == 0 ? 0 : x + a(x-1)
}
export a(5)
`})
expectWithUserModules(t, `out = import("mod1")`, 15, map[string]string{
"mod1": `
export func() {
a := func(x) {
return x == 0 ? 0 : x + a(x-1)
}
return a(5)
}()
`})
// (main) -> mod1 -> mod2 // (main) -> mod1 -> mod2
expectWithUserModules(t, `out = import("mod1").mod2.bar()`, 5.0, map[string]string{ expectWithUserModules(t, `out = import("mod1")()`, 5.0, map[string]string{
"mod1": `mod2 := import("mod2")`, "mod1": `export import("mod2")`,
"mod2": `bar := func() { return 5.0 }`, "mod2": `export func() { return 5.0 }`,
}) })
// (main) -> mod1 -> mod2 // (main) -> mod1 -> mod2
// -> mod2 // -> mod2
expectWithUserModules(t, `import("mod1"); out = import("mod2").bar()`, 5.0, map[string]string{ expectWithUserModules(t, `import("mod1"); out = import("mod2")()`, 5.0, map[string]string{
"mod1": `mod2 := import("mod2")`, "mod1": `export import("mod2")`,
"mod2": `bar := func() { return 5.0 }`, "mod2": `export func() { return 5.0 }`,
}) })
// (main) -> mod1 -> mod2 -> mod3 // (main) -> mod1 -> mod2 -> mod3
// -> mod2 -> mod3 // -> mod2 -> mod3
expectWithUserModules(t, `import("mod1"); out = import("mod2").mod3.bar()`, 5.0, map[string]string{ expectWithUserModules(t, `import("mod1"); out = import("mod2")()`, 5.0, map[string]string{
"mod1": `mod2 := import("mod2")`, "mod1": `export import("mod2")`,
"mod2": `mod3 := import("mod3")`, "mod2": `export import("mod3")`,
"mod3": `bar := func() { return 5.0 }`, "mod3": `export func() { return 5.0 }`,
}) })
// cyclic imports // cyclic imports
@ -104,26 +182,29 @@ if !is_error(cmd) {
"mod1": `import("mod2")`, "mod1": `import("mod2")`,
}) })
// for-in
expectWithUserModules(t, `for _, n in import("mod1") { out += n }`, 6, map[string]string{
"mod1": `a := 1; b := 2; c := 3`,
})
expectWithUserModules(t, `for k, _ in import("mod1") { out += k }`, "a", map[string]string{
"mod1": `a := 1`, // only 1 global variable because module map does not sort the keys
})
// mutating global variables inside the module does not affect exported values
expectWithUserModules(t, `m1 := import("mod1"); m1.mutate(); out = m1.a`, 3, map[string]string{
"mod1": `a := 3; mutate := func() { a = 10 }`,
})
// module map is immutable
expectErrorWithUserModules(t, `m1 := import("mod1"); m1.a = 5`, map[string]string{
"mod1": `a := 3`,
})
// module is immutable but its variables is not necessarily immutable. // module is immutable but its variables is not necessarily immutable.
expectWithUserModules(t, `m1 := import("mod1"); m1.a.b = 5; out = m1.a.b`, 5, map[string]string{ expectWithUserModules(t, `m1 := import("mod1"); m1.a.b = 5; out = m1.a.b`, 5, map[string]string{
"mod1": `a := {b: 3}`, "mod1": `export {a: {b: 3}}`,
})
// make sure module has same builtin functions
expectWithUserModules(t, `out = import("mod1")`, "int", map[string]string{
"mod1": `export func() { return type_name(0) }()`,
})
// 'export' statement is ignored outside module
expect(t, `a := 5; export func() { a = 10 }(); out = a`, 5)
// 'export' must be in the top-level
expectErrorWithUserModules(t, `import("mod1")`, map[string]string{
"mod1": `func() { export 5 }()`,
})
expectErrorWithUserModules(t, `import("mod1")`, map[string]string{
"mod1": `func() { func() { export 5 }() }()`,
})
// module cannot access outer scope
expectErrorWithUserModules(t, `a := 5; import("mod")`, map[string]string{
"mod1": `export a`,
}) })
} }

View file

@ -28,7 +28,7 @@ func TestString(t *testing.T) {
expectError(t, fmt.Sprintf("%s[%d]", strStr, strLen)) expectError(t, fmt.Sprintf("%s[%d]", strStr, strLen))
// slice operator // slice operator
for low := 0; low < strLen; low++ { for low := 0; low <= strLen; low++ {
for high := low; high <= strLen; high++ { for high := low; high <= strLen; high++ {
expect(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, high), str[low:high]) expect(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, high), str[low:high])
expect(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", strStr, low, high), str[low:high]) expect(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", strStr, low, high), str[low:high])

View file

@ -247,12 +247,18 @@ func traceCompileRun(file *ast.File, symbols map[string]objects.Object, userModu
bytecode := c.Bytecode() bytecode := c.Bytecode()
var constStr []string var constStr []string
for cidx, cn := range bytecode.Constants { for cidx, cn := range bytecode.Constants {
if cmFn, ok := cn.(*objects.CompiledFunction); ok { switch cn := cn.(type) {
case *objects.CompiledFunction:
constStr = append(constStr, fmt.Sprintf("[% 3d] (Compiled Function|%p)", cidx, &cn)) constStr = append(constStr, fmt.Sprintf("[% 3d] (Compiled Function|%p)", cidx, &cn))
for _, l := range compiler.FormatInstructions(cmFn.Instructions, 0) { for _, l := range compiler.FormatInstructions(cn.Instructions, 0) {
constStr = append(constStr, fmt.Sprintf(" %s", l)) constStr = append(constStr, fmt.Sprintf(" %s", l))
} }
} else { case *objects.CompiledModule:
constStr = append(constStr, fmt.Sprintf("[% 3d] (Compiled Module|%p)", cidx, &cn))
for _, l := range compiler.FormatInstructions(cn.Instructions, 0) {
constStr = append(constStr, fmt.Sprintf(" %s", l))
}
default:
constStr = append(constStr, fmt.Sprintf("[% 3d] %s (%s|%p)", cidx, cn, reflect.TypeOf(cn).Elem().Name(), &cn)) constStr = append(constStr, fmt.Sprintf("[% 3d] %s (%s|%p)", cidx, cn, reflect.TypeOf(cn).Elem().Name(), &cn))
} }
} }
@ -273,22 +279,7 @@ func traceCompileRun(file *ast.File, symbols map[string]objects.Object, userModu
res[name] = *globals[sym.Index] res[name] = *globals[sym.Index]
} }
var globalsStr []string trace = append(trace, fmt.Sprintf("\n[Globals]\n\n%s", strings.Join(formatGlobals(globals), "\n")))
for gidx, g := range globals {
if g == nil {
break
}
if cmFn, ok := (*g).(*objects.Closure); ok {
globalsStr = append(globalsStr, fmt.Sprintf("[% 3d] (Closure|%p)", gidx, g))
for _, l := range compiler.FormatInstructions(cmFn.Fn.Instructions, 0) {
globalsStr = append(globalsStr, fmt.Sprintf(" %s", l))
}
} else {
globalsStr = append(globalsStr, fmt.Sprintf("[% 3d] %s (%s|%p)", gidx, (*g).String(), reflect.TypeOf(*g).Elem().Name(), g))
}
}
trace = append(trace, fmt.Sprintf("\n[Globals]\n\n%s", strings.Join(globalsStr, "\n")))
frameIdx, ip := v.FrameInfo() frameIdx, ip := v.FrameInfo()
trace = append(trace, fmt.Sprintf("\n[IP]\n\nFrame=%d, IP=%d", frameIdx, ip+1)) trace = append(trace, fmt.Sprintf("\n[IP]\n\nFrame=%d, IP=%d", frameIdx, ip+1))
@ -300,6 +291,26 @@ func traceCompileRun(file *ast.File, symbols map[string]objects.Object, userModu
return return
} }
func formatGlobals(globals []*objects.Object) (formatted []string) {
for idx, global := range globals {
if global == nil {
return
}
switch global := (*global).(type) {
case *objects.Closure:
formatted = append(formatted, fmt.Sprintf("[% 3d] (Closure|%p)", idx, global))
for _, l := range compiler.FormatInstructions(global.Fn.Instructions, 0) {
formatted = append(formatted, fmt.Sprintf(" %s", l))
}
default:
formatted = append(formatted, fmt.Sprintf("[% 3d] %s (%s|%p)", idx, global.String(), reflect.TypeOf(global).Elem().Name(), global))
}
}
return
}
func parse(t *testing.T, input string) *ast.File { func parse(t *testing.T, input string) *ast.File {
testFileSet := source.NewFileSet() testFileSet := source.NewFileSet()
testFile := testFileSet.AddFile("", -1, len(input)) testFile := testFileSet.AddFile("", -1, len(input))

View file

@ -17,7 +17,6 @@ type Script struct {
variables map[string]*Variable variables map[string]*Variable
removedBuiltins map[string]bool removedBuiltins map[string]bool
removedStdModules map[string]bool removedStdModules map[string]bool
scriptModules map[string]*Script
userModuleLoader compiler.ModuleLoader userModuleLoader compiler.ModuleLoader
input []byte input []byte
} }
@ -80,16 +79,6 @@ func (s *Script) SetUserModuleLoader(loader compiler.ModuleLoader) {
s.userModuleLoader = loader s.userModuleLoader = loader
} }
// AddModule adds another script as a module. Script module will be
// compiled and run right before the main script s is compiled.
func (s *Script) AddModule(name string, scriptModule *Script) {
if s.scriptModules == nil {
s.scriptModules = make(map[string]*Script)
}
s.scriptModules[name] = scriptModule
}
// Compile compiles the script with all the defined variables, and, returns Compiled object. // Compile compiles the script with all the defined variables, and, returns Compiled object.
func (s *Script) Compile() (*Compiled, error) { func (s *Script) Compile() (*Compiled, error) {
symbolTable, stdModules, globals, err := s.prepCompile() symbolTable, stdModules, globals, err := s.prepCompile()
@ -165,40 +154,8 @@ func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, stdModules ma
stdModules[name] = mod stdModules[name] = mod
} }
} }
for name, scriptModule := range s.scriptModules {
if scriptModule == nil {
err = fmt.Errorf("script module must not be nil: %s", name)
}
var compiledModule *Compiled globals = make([]*objects.Object, runtime.GlobalsSize, runtime.GlobalsSize)
compiledModule, err = scriptModule.Compile()
if err != nil {
return
}
err = compiledModule.Run()
if err != nil {
return
}
mod := &objects.ImmutableMap{
Value: make(map[string]objects.Object),
}
for _, symbolName := range compiledModule.symbolTable.Names() {
symbol, _, ok := compiledModule.symbolTable.Resolve(symbolName)
if ok && symbol.Scope == compiler.ScopeGlobal {
value := compiledModule.machine.Globals()[symbol.Index]
if value != nil {
mod.Value[symbolName] = *value
}
}
}
stdModules[name] = mod
}
globals = make([]*objects.Object, len(names), len(names))
for idx, name := range names { for idx, name := range names {
symbol := symbolTable.Define(name) symbol := symbolTable.Define(name)

View file

@ -1,30 +1,56 @@
package script_test package script_test
import ( import (
"errors"
"testing" "testing"
"github.com/d5/tengo/assert" "github.com/d5/tengo/assert"
"github.com/d5/tengo/script" "github.com/d5/tengo/script"
) )
func TestScript_AddModule(t *testing.T) { func TestScript_SetUserModuleLoader(t *testing.T) {
// mod1 module
mod1 := script.New([]byte(`a := 5`))
// script1 imports "mod1" // script1 imports "mod1"
scr1 := script.New([]byte(`mod1 := import("mod1"); out := mod1.a`)) scr := script.New([]byte(`out := import("mod")`))
scr1.AddModule("mod1", mod1) scr.SetUserModuleLoader(func(name string) ([]byte, error) {
c, err := scr1.Run() return []byte(`export 5`), nil
})
c, err := scr.Run()
assert.Equal(t, int64(5), c.Get("out").Value()) assert.Equal(t, int64(5), c.Get("out").Value())
// mod2 module imports "mod1" // executing module function
mod2 := script.New([]byte(`mod1 := import("mod1"); b := mod1.a * 2`)) scr = script.New([]byte(`fn := import("mod"); out := fn()`))
mod2.AddModule("mod1", mod1) scr.SetUserModuleLoader(func(name string) ([]byte, error) {
return []byte(`a := 3; export func() { return a + 5 }`), nil
// script2 imports "mod2" (which imports "mod1") })
scr2 := script.New([]byte(`mod2 := import("mod2"); out := mod2.b`)) c, err = scr.Run()
scr2.AddModule("mod2", mod2)
c, err = scr2.Run()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(10), c.Get("out").Value()) assert.Equal(t, int64(8), c.Get("out").Value())
// disabled builtin function
scr = script.New([]byte(`out := import("mod")`))
scr.SetUserModuleLoader(func(name string) ([]byte, error) {
return []byte(`export len([1, 2, 3])`), nil
})
c, err = scr.Run()
assert.NoError(t, err)
assert.Equal(t, int64(3), c.Get("out").Value())
scr.DisableBuiltinFunction("len")
_, err = scr.Run()
assert.Error(t, err)
// disabled stdlib
scr = script.New([]byte(`out := import("mod")`))
scr.SetUserModuleLoader(func(name string) ([]byte, error) {
if name == "mod" {
return []byte(`text := import("text"); export text.title("foo")`), nil
}
return nil, errors.New("module not found")
})
c, err = scr.Run()
assert.NoError(t, err)
assert.Equal(t, "Foo", c.Get("out").Value())
scr.DisableStdModule("text")
_, err = scr.Run()
assert.Error(t, err)
} }

View file

@ -1,7 +1,6 @@
package script_test package script_test
import ( import (
"errors"
"testing" "testing"
"github.com/d5/tengo/assert" "github.com/d5/tengo/assert"
@ -59,22 +58,3 @@ func TestScript_DisableStdModule(t *testing.T) {
_, err = s.Run() _, err = s.Run()
assert.Error(t, err) assert.Error(t, err)
} }
func TestScript_SetUserModuleLoader(t *testing.T) {
s := script.New([]byte(`math := import("mod1"); a := math.foo()`))
_, err := s.Run()
assert.Error(t, err)
s.SetUserModuleLoader(func(moduleName string) (res []byte, err error) {
if moduleName == "mod1" {
res = []byte(`foo := func() { return 5 }`)
return
}
err = errors.New("module not found")
return
})
c, err := s.Run()
assert.NoError(t, err)
assert.NotNil(t, c)
compiledGet(t, c, "a", int64(5))
}