12 KiB
Tengo Objects
Table of Contents
Objects
All object types in Tengo implement Object interface.
TypeName() string
TypeName method should return the name of the type. Type names are not directly used by the runtime (except when it reports a run-time error), but, it is generally a good idea to keep it short but distinguishable from other types.
String() string
String method should return a string representation of the underlying value. The value returned by String method will be used whenever string formatting for the value is required, most commonly when being converted into String value.
BinaryOp(op token.Token, rhs Object) (res Object, err error)
In Tengo, a type can overload binary operators (+
, -
, *
, /
, %
, &
, |
, ^
, &^
, >>
, <<
, >
, >=
; note that <
and <=
operators are not overloadable as they're simply implemented by switching left-hand side and right-hand side of >
/>=
operator) by implementing BinaryOp method. BinaryOp method takes the operator op
and the right-hand side object rhs
, and, should return a resulting value res
.
Error value vs runtime error
If BinaryOp method returns an error err
(the second return value), it will be treated as a run-time error, which will halt the execution (VM.Run() error
) and will return the error to the user. All runtime type implementations, for example, will return an ErrInvalidOperator
error when the given operator is not supported by the type.
Alternatively the method can return an Error
value as its result res
(the first return value), which will not halt the runtime and will be treated like any other values. As a dynamically typed language, the receiver (another expression or statement) can determine how to translate Error
value returned from binary operator expression.
IsFalsy() bool
IsFalsy method should return true if the underlying value is considered to be falsy.
Equals(o Object) bool
Equals method should return true if the underlying value is considered to be equal to the underlying value of another object o
. When comparing values of different types, the runtime does not guarantee or force anything, but, it's generally a good idea to make the result consistent. For example, a custom integer type may return true when comparing against String value, but, it should return the same result for the same inputs.
Copy() Object
Copy method should a new copy of the same object. All primitive and composite value types implement this method to return a deep-copy of the value, which is recommended for other user types (as copy
builtin function uses this Copy method), but, it's not a strict requirement by the runtime.
Runtime Object Types
These are the Tengo runtime object types:
- Primitive value types: Int, String, Float, Bool, Char, Bytes
- Composite value types: Array, Map, ImmutableMap
- Functions: CompiledFunction, BuiltinFunction, UserFunction
- Iterators: StringIterator, ArrayIterator, MapIterator, ImmutableMapIterator
- Error
- Undefined
- Other internal objects: Closure, CompiledModule, Break, Continue, ReturnValue
User Object Types
Basically Tengo runtime treats and manages both the runtime types and user types exactly the same way as long as they implement Object interface. You can add values of the custom user types (via either Script.Add method or by directly manipulating the symbol table and the global variables), and, use them directly in Tengo code.
Here's an example user type, Time
:
type Time struct {
Value time.Time
}
func (t *Time) TypeName() string {
return "time"
}
func (t *Time) String() string {
return t.Value.Format(time.RFC3339)
}
func (t *Time) BinaryOp(op token.Token, rhs objects.Object) (objects.Object, error) {
switch rhs := rhs.(type) {
case *Time:
switch op {
case token.Sub:
return &objects.Int{
Value: t.Value.Sub(rhs.Value).Nanoseconds(),
}, nil
}
case *objects.Int:
switch op {
case token.Add:
return &Time{
Value: t.Value.Add(time.Duration(rhs.Value)),
}, nil
case token.Sub:
return &Time{
Value: t.Value.Add(-time.Duration(rhs.Value)),
}, nil
}
}
return nil, objects.ErrInvalidOperator
}
func (t *Time) IsFalsy() bool {
return t.Value.IsZero()
}
func (t *Time) Equals(o objects.Object) bool {
if o, ok := o.(*Time); ok {
return t.Value.Equal(o.Value)
}
return false
}
func (t *Time) Copy() objects.Object {
return &Time{Value: t.Value}
}
Now the Tengo runtime recognizes Time
type, and, any Time
values can be used directly in the Tengo code:
s := script.New([]byte(`
a := currentTime + 10000 // Time + Int = Time
b := a - currentTime // Time - Time = Int
`))
// add Time value 'currentTime'
err := s.Add("currentTime", &Time{Value: time.Now()})
if err != nil {
panic(err)
}
c, err := s.Run()
if err != nil {
panic(err)
}
fmt.Println(c.Get("b")) // "10000"
Callable Objects
Any types that implement Callable interface (in addition to Object interface), values of such types can be used as if they are functions.
type Callable interface {
Call(args ...Object) (ret Object, err error)
}
To make Time
a callable value, add Call method to the previous implementation:
func (t *Time) Call(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
format, ok := objects.ToString(args[0])
if !ok {
return nil, objects.ErrInvalidTypeConversion
}
return &objects.String{Value: t.Value.Format(format)}, nil
}
Now Time
values can be "called" like this:
s := script.New([]byte(`
a := currentTime + 10000 // Time + Int = Time
b := a("15:04:05") // call 'a'
`))
// add Time value 'currentTime'
err := s.Add("currentTime", &Time{Value: time.Now()})
if err != nil {
panic(err)
}
c, err := s.Run()
if err != nil {
panic(err)
}
fmt.Println(c.Get("b")) // something like "21:15:27"
Indexable Objects
If the type implements Indexable interface, it enables dot selector (value = object.index
) or indexer (value = object[index]
) syntax for its values.
type Indexable interface {
IndexGet(index Object) (value Object, err error)
}
If the implementation returns an error (err
), the VM will treat it as a run-time error. Many runtime types such as Map and Array also implement the same interface:
func (o *Map) IndexGet(index Object) (res Object, err error) {
strIdx, ok := index.(*String)
if !ok {
err = ErrInvalidIndexType
return
}
val, ok := o.Value[strIdx.Value]
if !ok {
val = UndefinedValue
}
return val, nil
}
Array and Map implementation forces the type of index Object (Int and String respectively), but, it's not required behavior by the VM. It is completely okay to take various index types (or to do type coercion) as long as its result is consistent.
By convention, Array or Array-like types return ErrIndexOutOfBounds
error (as a runtime error) when the index is invalid (out of the bounds), and, Map or Map-like types return Undefined
value when the key does not exist. But, again this is not a requirement, and, the type can implement the behavior however it fits.
Index-Assignable Objects
If the type implements IndexAssignable interface, the values of that type allow assignment using dot selector (object.index = value
) or indexer (object[index] = value
) in the assignment statements.
type IndexAssignable interface {
IndexSet(index, value Object) error
}
Map, Array, and a couple of other runtime types also implement the same interface:
func (o *Map) IndexSet(index, value Object) (err error) {
strIdx, ok := ToString(index)
if !ok {
err = ErrInvalidTypeConversion
return
}
o.Value[strIdx] = value
return nil
}
Array and Map implementation forces the type of index Object (Int and String respectively), but, it's not required behavior by the VM. It is completely okay to take various index types (or to do type coercion) as long as its result is consistent.
By convention, Array or Array-like types return ErrIndexOutOfBounds
error (as a runtime error) when the index is invalid (out of the bounds). But, this is not a requirement, and, the type can implement the behavior however it fits.
Iterable Objects
Values of the types that implement Iterable interface can be used in for-in
statements (for key, value in object { ... }
).
type Iterable interface {
Iterate() Iterator
}
This Iterate method should return another object that implements Iterator interface.
Iterator Interface
Next() bool
Next method should return true if there are more elements to iterate. When used with for-in
statements, the compiler uses Key and Value methods to populate the current element's key (or index) and value from the object that this iterator represents. The runtime will stop iterating in for-in
statement when this method returns false.
Key() Object
Key method should return a key (or an index) Object for the current element of the underlying object. It should return the same value until Next method is called again. By convention, iterators for the map or map-like objects returns the String key, and, iterators for array or array-like objects returns the Int index. But, it's not a requirement by the VM.
Value() Object
Value method should return a value Object for the current element of the underlying object. It should return the same value until Next method is called again.