Forked and more actively developed tengo version
Find a file
2019-01-12 23:14:36 -08:00
assert update directory package structure 2019-01-11 02:27:28 -08:00
cmd update README, remove some unnecessary code 2019-01-12 22:58:12 -08:00
compiler update directory package structure 2019-01-11 02:27:28 -08:00
objects update directory package structure 2019-01-11 02:27:28 -08:00
runtime remove VM.Stack() function 2019-01-11 21:50:23 -08:00
script add Script.Run() method 2019-01-12 18:25:45 -08:00
.gitignore initial commit 2019-01-08 23:17:42 -08:00
LICENSE Create LICENSE 2019-01-11 01:17:07 -08:00
Makefile initial commit 2019-01-08 23:17:42 -08:00
README.md Update README.md 2019-01-12 23:14:36 -08:00
tengo.go update README, remove some unnecessary code 2019-01-12 22:58:12 -08:00

The Tengo Language

Tengo is an embedded script language for Go.

>> Try Tengo in online Playground <<

Features

  • Simple and intuitive syntax
  • Dynamically typed with type coercions
  • Bytecode compiled (see the benchmark results)
  • 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 (without writing any Go code)

Benchmark

fib(35) fibt(35) Type
Go 68,713,331 3,264,992 Go (native)
Tengo 6,811,234,411 4,699,512 Go-VM
Lua 1,946,451,017 3,220,991 Lua (native)
go-lua 5,658,423,479 4,247,160 Go-Lua-VM
GopherLua 6,301,424,553 5,194,735 Go-Lua-VM
Python 3,159,870,102 28,512,040 Python (native)
otto 91,616,109,035 13,780,650 Go-JS-Interpreter
Anko 119,395,411,432 22,266,008 Go-Interpreter

*Nanoseconds

fib(35) is a function to calculate 35th Fibonacci number.

fib := func(x) {
	if x == 0 {
		return 0
	} else if x == 1 {
		return 1
	} else {
		return fib(x-1) + fib(x-2)
	}
}
fib(35)

fibt(35) is a tail-call version of fib(35).

fibt := func(x, a, b) {
	if x == 0 {
		return a
	} else if x == 1 {
		return b
	} else {
		return fibt(x-1, b, a+b)
	}
}
fibt(35, 0, 1)

Please see tengobench for more details.

Tengo Syntax in 5 Minutes

Tengo supports line comments (//...) and block comments (/* ... */).

/* 
  multi-line block comments 
*/

a := 5 // line comments

Run in Playground

Tengo is a dynamically typed language, and, you can initialize the variables using := operator.

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

After the variable is initialized, it can be re-assigned different value using = operator.

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

Type is not explicitly specified, but, you can use type coercion functions to convert between types.

s1 := string(1984)  // "1984"
i2 := int("-999")   // -999
f3 := float(-51)    // -51.0
b4 := bool(1)       // true
c5 := char("X")     // 'X'

Run in Playground

You can use dot selector (.) and indexer ([]) operator to read or write elemens of arrays or maps.

["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

For sequence types (string or array), you can use slice operator ([:]) too.

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

In Tengo, functions are first-class citizen and be treated like any other variables. Tengo also supports closures, functions that captures variables in outer scopes. In the following example, the function that's being returned from adder function is capturing base variable.

adder := func(base) {
    return func(x) { return base + x }	// capturing 'base'
}
add5 := adder(5)
nine := add5(4)		// nine

Run in Playground

For flow control, Tengo currently supports if-else, for, for-in statements.

// 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
    // ...
}

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.

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.

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 thie 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 optinoally objects.Callable if you want to make function-like invokable objects).

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:

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).

tengo myapp.tengo

Or, you can compile the code into a binary file and execute it later.

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 if you run tengo with no arguments.

tengo

Roadmap

The next big features planned include:

  • Module system (or packages)
  • Standard libraries
  • Better documentations
  • More language constructs such as error handling, object methods, switch-case statements
  • Native executables compilation
  • Performance improvements
  • Syntax highlighter for IDEs