415 lines
11 KiB
Markdown
415 lines
11 KiB
Markdown
<p align="center">
|
||
<img src="https://raw.githubusercontent.com/d5/tengolang.com/master/logo_400.png" width="200" height="200">
|
||
</p>
|
||
|
||
# The Tengo Language
|
||
|
||
[![GoDoc](https://godoc.org/github.com/d5/tengo?status.svg)](https://godoc.org/github.com/d5/tengo/script)
|
||
[![Go Report Card](https://goreportcard.com/badge/github.com/d5/tengo)](https://goreportcard.com/report/github.com/d5/tengo)
|
||
|
||
Tengo is an embeddable script language for Go.
|
||
|
||
Tengo is [fast](#benchmark) as it's compiled to bytecode and executed on stack-based VM that's written in native Go.
|
||
|
||
\>> **Try [Tengo Playground](https://tengolang.com/)** <<
|
||
|
||
## Features
|
||
|
||
- Simple and intuitive [syntax](https://github.com/d5/tengo#tengo-syntax-in-5-minutes)
|
||
- Dynamically typed with type coercions
|
||
- First-class functions and Closures
|
||
- Garbage collected _(thanks to Go runtime)_
|
||
- Easily extendible using customizable types
|
||
- Written in pure Go _(no CGO, no external dependencies)_
|
||
- Excutable as a [standalone language](https://github.com/d5/tengo#tengo-as-a-standalone-language) _(without writing any Go code)_
|
||
|
||
## Benchmark
|
||
|
||
| | fib(35) | fibt(35) | Type |
|
||
| :--- | ---: | ---: | :---: |
|
||
| Go | `59ms` | `4ms` | Go (native) |
|
||
| [**Tengo**](https://github.com/d5/tengo) | `4,809ms` | `5ms` | VM on Go |
|
||
| Lua | `1,752ms` | `3ms` | Lua (native) |
|
||
| [go-lua](https://github.com/Shopify/go-lua) | `5,236ms` | `5ms` | Lua VM on Go |
|
||
| [GopherLua](https://github.com/yuin/gopher-lua) | `5,558ms` | `5ms` | Lua VM on Go |
|
||
| Python | `3,132ms` | `28ms` | Python (native) |
|
||
| [otto](https://github.com/robertkrimen/otto) | `85,765ms` | `22ms` | JS Interpreter on Go |
|
||
| [Anko](https://github.com/mattn/anko) | `99,235ms` | `24ms` | Interpreter on Go |
|
||
|
||
[fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo) is a function to compute 35th Fibonacci number, and, [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo) is the [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of the same function. You can see all the code used for this test in [tengobench](https://github.com/d5/tengobench).
|
||
|
||
## Tengo Syntax in 5 Minutes
|
||
|
||
Tengo supports line comments (`//...`) and block comments (`/* ... */`).
|
||
|
||
```golang
|
||
/*
|
||
multi-line block comments
|
||
*/
|
||
|
||
a := 5 // line comments
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=02e384399a0397b0a752f08604ccb244d1a6cb37)
|
||
|
||
|
||
Tengo is a dynamically typed language, and, you can initialize the variables using `:=` operator.
|
||
|
||
```golang
|
||
a := 1984 // int
|
||
b := "aomame" // string
|
||
c := -9.22 // float
|
||
d := true // bool
|
||
e := '九' // char
|
||
f := [1, false, "foo"] // array
|
||
g := { // map
|
||
h: 439,
|
||
i: 12.34,
|
||
j: [0, 9, false]
|
||
}
|
||
k := func(l, m) { // function
|
||
return l + m
|
||
}
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=f8626a711769502ce20e4560ace65c0e9c1279f4)
|
||
|
||
After the variable is initialized, it can be re-assigned different value using `=` operator.
|
||
|
||
```golang
|
||
a := 1928 // int
|
||
a = "foo" // string
|
||
f := func() {
|
||
a := false // 'a' is defined in the function scope
|
||
a = [1, 2, 3] // and thus does not affect 'a' in global scope.
|
||
}
|
||
a == "foo" // still "foo"
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=1d39bc2af5c51417df82b32db47a0e6a156d48ec)
|
||
|
||
|
||
Type is not directly specified, but, you can use type-coercion functions to convert between types.
|
||
|
||
```golang
|
||
s1 := string(1984) // "1984"
|
||
i2 := int("-999") // -999
|
||
f3 := float(-51) // -51.0
|
||
b4 := bool(1) // true
|
||
c5 := char("X") // 'X'
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=8d57905b82959eb244e9bbd2111e12ee04a33045)
|
||
|
||
_See [Variable Types](https://github.com/d5/tengo/wiki/Variable-Types) for more details on the variable types._
|
||
|
||
You can use the dot selector (`.`) and indexer (`[]`) operator to read or write elemens of arrays, strings, or maps.
|
||
|
||
```golang
|
||
["one", "two", "three"][1] // == "two"
|
||
|
||
m := {
|
||
a: 1,
|
||
b: [2, 3, 4],
|
||
c: func() { return 10 }
|
||
}
|
||
m.a // == 1
|
||
m["b"][1] // == 3
|
||
m.c() // == 10
|
||
m.x = 5 // add 'x' to map 'm'
|
||
//m.b[5] = 0 // but this is an error: index out of bounds
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=d510c75ed8f06ef1e22c1aaf8a7d4565c793514c)
|
||
|
||
For sequence types (string, bytes, array), you can use slice operator (`[:]`) too.
|
||
|
||
```golang
|
||
a := [1, 2, 3, 4, 5][1:3] // == [2, 3]
|
||
b := [1, 2, 3, 4, 5][3:] // == [4, 5]
|
||
c := [1, 2, 3, 4, 5][:3] // == [1, 2, 3]
|
||
d := "hello world"[2:10] // == "llo worl"
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=214ab490bb24549578770984985f6b161aed915d)
|
||
|
||
|
||
In Tengo, functions are first-class citizen, and, it also supports closures, functions that captures variables in outer scopes. In the following example, the function returned from `adder` is capturing `base` variable.
|
||
|
||
```golang
|
||
adder := func(base) {
|
||
return func(x) { return base + x } // capturing 'base'
|
||
}
|
||
add5 := adder(5)
|
||
nine := add5(4) // == 9
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=fba79990473d5b38cc944dfa225d38580ddaf422)
|
||
|
||
|
||
For flow control, Tengo currently supports **if-else**, **for**, **for-in** statements.
|
||
|
||
```golang
|
||
// IF-ELSE
|
||
if a < 0 {
|
||
// ...
|
||
} else if a == 0 {
|
||
// ...
|
||
} else {
|
||
// ...
|
||
}
|
||
|
||
// IF with init statement
|
||
if a := 0; a < 10 {
|
||
// ...
|
||
} else {
|
||
// ...
|
||
}
|
||
|
||
// FOR
|
||
for a:=0; a<10; a++ {
|
||
// ...
|
||
}
|
||
|
||
// FOR condition-only (like WHILE in other languages)
|
||
for a < 10 {
|
||
// ...
|
||
}
|
||
|
||
// FOR-IN
|
||
for x in [1, 2, 3] { // array: element
|
||
// ...
|
||
}
|
||
for i, x in [1, 2, 3] { // array: index and element
|
||
// ...
|
||
}
|
||
for k, v in {k1: 1, k2: 2} { // map: key and value
|
||
// ...
|
||
}
|
||
```
|
||
|
||
An error object is created using `error` function-like keyword. An error can have any types of value and the underlying value of the error can be accessed using `.value` selector.
|
||
|
||
```golang
|
||
err1 := error("oops") // error with string value
|
||
err2 := error(1+2+3) // error with int value
|
||
if is_error(err1) { // 'is_error' builtin function
|
||
err_val := err1.value // get underlying value
|
||
}
|
||
```
|
||
> [Run in Playground](https://tengolang.com/?s=5eaba4289c9d284d97704dd09cb15f4f03ad05c1)
|
||
|
||
You can load other scripts as modules using `import` expression.
|
||
|
||
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
|
||
func1 := func(x) { print(x) }
|
||
foo := 2
|
||
```
|
||
|
||
Basically `import` expression returns all the global variables defined in the module as a Map-like value. One can access the functions or variables defined in the module using `.` selector or `["key"]` indexer, but, module variables are immutable.
|
||
|
||
|
||
## Embedding Tengo in Go
|
||
|
||
To execute Tengo code in your Go codebase, you should use **Script**. In the simple use cases, all you need is to do is to create a new Script instance and call its `Script.Run()` function.
|
||
|
||
```golang
|
||
import "github.com/d5/tengo/script"
|
||
|
||
var code = `
|
||
reduce := func(seq, fn) {
|
||
s := 0
|
||
for x in seq { fn(x, s) }
|
||
return s
|
||
}
|
||
|
||
print(reduce([1, 2, 3], func(x, s) { s += x }))
|
||
`
|
||
|
||
func main() {
|
||
s := script.New([]byte(code))
|
||
if _, err := s.Run(); err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
If you want to compile the source script once and execute it multiple times, you can use `Script.Compile()` function that returns **Compiled** instance.
|
||
|
||
```golang
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/d5/tengo/script"
|
||
)
|
||
|
||
func main() {
|
||
s := script.New([]byte(`a := b + 20`))
|
||
|
||
// define variable 'b'
|
||
_ = s.Add("b", 10)
|
||
|
||
// compile the source
|
||
c, err := s.Compile()
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
// run the compiled bytecode
|
||
// a compiled bytecode 'c' can be executed multiple without re-compiling it
|
||
if err := c.Run(); err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
// retrieve value of 'a'
|
||
a := c.Get("a")
|
||
fmt.Println(a.Int())
|
||
}
|
||
```
|
||
|
||
In the example above, a variable `b` is defined by the user before compiliation using `Script.Add()` function. Then a compiled bytecode `c` is used to execute the bytecode and get the value of global variables. In this example, the value of global variable `a` is read using `Compiled.Get()` function.
|
||
|
||
If you need the custom data types (outside Tengo's primitive types), you can define your own `struct` that implements `objects.Object` interface _(and optionally `objects.Callable` if you want to make function-like invokable objects)_.
|
||
|
||
```golang
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/d5/tengo/compiler/token"
|
||
"github.com/d5/tengo/objects"
|
||
"github.com/d5/tengo/script"
|
||
)
|
||
|
||
type Counter struct {
|
||
value int64
|
||
}
|
||
|
||
func (o *Counter) TypeName() string {
|
||
return "counter"
|
||
}
|
||
|
||
func (o *Counter) String() string {
|
||
return fmt.Sprintf("Counter(%d)", o.value)
|
||
}
|
||
|
||
func (o *Counter) BinaryOp(op token.Token, rhs objects.Object) (objects.Object, error) {
|
||
switch rhs := rhs.(type) {
|
||
case *Counter:
|
||
switch op {
|
||
case token.Add:
|
||
return &Counter{value: o.value + rhs.value}, nil
|
||
case token.Sub:
|
||
return &Counter{value: o.value - rhs.value}, nil
|
||
}
|
||
case *objects.Int:
|
||
switch op {
|
||
case token.Add:
|
||
return &Counter{value: o.value + rhs.Value}, nil
|
||
case token.Sub:
|
||
return &Counter{value: o.value - rhs.Value}, nil
|
||
}
|
||
}
|
||
|
||
return nil, errors.New("invalid operator")
|
||
}
|
||
|
||
func (o *Counter) IsFalsy() bool {
|
||
return o.value == 0
|
||
}
|
||
|
||
func (o *Counter) Equals(t objects.Object) bool {
|
||
if tc, ok := t.(*Counter); ok {
|
||
return o.value == tc.value
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
func (o *Counter) Copy() objects.Object {
|
||
return &Counter{value: o.value}
|
||
}
|
||
|
||
func (o *Counter) Call(args ...objects.Object) (objects.Object, error) {
|
||
return &objects.Int{Value: o.value}, nil
|
||
}
|
||
|
||
var code = []byte(`
|
||
arr := [1, 2, 3, 4]
|
||
for x in arr {
|
||
c1 += x
|
||
}
|
||
out := c1()`)
|
||
|
||
func main() {
|
||
s := script.New(code)
|
||
|
||
// define variable 'c1'
|
||
_ = s.Add("c1", &Counter{value: 5})
|
||
|
||
// compile the source
|
||
c, err := s.Run()
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
// retrieve value of 'out'
|
||
out := c.Get("out")
|
||
fmt.Println(out.Int()) // prints "15" ( = 5 + (1 + 2 + 3 + 4) )
|
||
}
|
||
|
||
```
|
||
|
||
As an alternative to using **Script**, you can directly create and interact with the parser, compiler and VMs directly. There's no good documentations yet, but, check out Script code if you are interested.
|
||
|
||
|
||
## Tengo as a Standalone Language
|
||
|
||
Although Tengo is designed as an embedded script language for Go, it can be compiled and executed as native binary without any Go code using `tengo` tool.
|
||
|
||
### Installing Tengo Tool
|
||
|
||
To install `tengo` tool, run:
|
||
|
||
```bash
|
||
go get github.com/d5/tengo/cmd/tengo
|
||
```
|
||
|
||
### Compiling and Executing Tengo Code
|
||
|
||
You can directly execute the Tengo source code by running `tengo` tool with your Tengo source file (`*.tengo`).
|
||
|
||
```bash
|
||
tengo myapp.tengo
|
||
```
|
||
|
||
Or, you can compile the code into a binary file and execute it later.
|
||
|
||
```bash
|
||
tengo -c -o myapp myapp.tengo # compile 'myapp.tengo' into binary file 'myapp'
|
||
tengo myapp # execute the compiled binary `myapp`
|
||
```
|
||
|
||
### Tengo REPL
|
||
|
||
You can run Tengo [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) if you run `tengo` with no arguments.
|
||
|
||
```bash
|
||
tengo
|
||
```
|
||
|
||
## Roadmap
|
||
|
||
Development roadmap for Tengo:
|
||
|
||
- Standard libraries _(modules)_
|
||
- Better documentations
|
||
- More language constructs such as destructuring assignment, `this` binding for object methods, switch-case statements
|
||
- Native executables compilation
|
||
- Performance improvements
|
||
- Syntax highlighter for IDEs
|
||
|