Merge pull request #48 from d5/immutable

Immutable values
This commit is contained in:
Daniel Kang 2019-01-26 06:01:24 -08:00 committed by GitHub
commit 5e2187d94a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 361 additions and 105 deletions

View file

@ -35,7 +35,7 @@ s := sum("", [1, 2, 3]) // == "123"
- Simple and highly readable [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- Dynamic typing with type coercion
- Higher-order functions and closures
- Immutable values _(v1)_
- Immutable values
- Garbage collection
- [Securely Embeddable](https://github.com/d5/tengo/blob/master/docs/interoperability.md) and [Extensible](https://github.com/d5/tengo/blob/master/docs/objects.md)
- Compiler/runtime written in native Go _(no external deps or cgo)_

View file

@ -164,13 +164,17 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool
case *objects.ReturnValue:
return Equal(t, expected.Value, actual.(objects.ReturnValue).Value)
case *objects.Array:
return equalArray(t, expected, actual.(*objects.Array))
return equalObjectSlice(t, expected.Value, actual.(*objects.Array).Value)
case *objects.ImmutableArray:
return equalObjectSlice(t, expected.Value, actual.(*objects.ImmutableArray).Value)
case *objects.Bytes:
if bytes.Compare(expected.Value, actual.(*objects.Bytes).Value) != 0 {
return failExpectedActual(t, expected.Value, actual.(*objects.Bytes).Value, msg...)
}
case *objects.Map:
return equalMap(t, expected, actual.(*objects.Map))
return equalObjectMap(t, expected.Value, actual.(*objects.Map).Value)
case *objects.ImmutableMap:
return equalObjectMap(t, expected.Value, actual.(*objects.ImmutableMap).Value)
case *objects.CompiledFunction:
return equalCompiledFunction(t, expected, actual.(*objects.CompiledFunction))
case *objects.Closure:
@ -252,13 +256,6 @@ func equalSymbol(a, b compiler.Symbol) bool {
a.Scope == b.Scope
}
func equalArray(t *testing.T, expected, actual objects.Object) bool {
expectedT := expected.(*objects.Array).Value
actualT := actual.(*objects.Array).Value
return equalObjectSlice(t, expectedT, actualT)
}
func equalObjectSlice(t *testing.T, expected, actual []objects.Object) bool {
// TODO: this test does not differentiate nil vs empty slice
@ -275,16 +272,13 @@ func equalObjectSlice(t *testing.T, expected, actual []objects.Object) bool {
return true
}
func equalMap(t *testing.T, expected, actual objects.Object) bool {
expectedT := expected.(*objects.Map).Value
actualT := actual.(*objects.Map).Value
if !Equal(t, len(expectedT), len(actualT)) {
func equalObjectMap(t *testing.T, expected, actual map[string]objects.Object) bool {
if !Equal(t, len(expected), len(actual)) {
return false
}
for key, expectedVal := range expectedT {
actualVal := actualT[key]
for key, expectedVal := range expected {
actualVal := actual[key]
if !Equal(t, expectedVal, actualVal) {
return false

View file

@ -0,0 +1,29 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
)
// ImmutableExpr represents an immutable expression
type ImmutableExpr struct {
Expr Expr
ErrorPos source.Pos
LParen source.Pos
RParen source.Pos
}
func (e *ImmutableExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *ImmutableExpr) Pos() source.Pos {
return e.ErrorPos
}
// End returns the position of first character immediately after the node.
func (e *ImmutableExpr) End() source.Pos {
return e.RParen
}
func (e *ImmutableExpr) String() string {
return "immutable(" + e.Expr.String() + ")"
}

View file

@ -42,14 +42,14 @@ func init() {
gob.Register(&objects.Bool{})
gob.Register(&objects.Char{})
gob.Register(&objects.Array{})
gob.Register(&objects.ImmutableArray{})
gob.Register(&objects.Map{})
gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.CompiledFunction{})
gob.Register(&objects.Undefined{})
gob.Register(&objects.Error{})
gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.Bytes{})
gob.Register(&objects.StringIterator{})
gob.Register(&objects.MapIterator{})
gob.Register(&objects.ImmutableMapIterator{})
gob.Register(&objects.ArrayIterator{})
}

View file

@ -473,6 +473,13 @@ func (c *Compiler) Compile(node ast.Node) error {
}
c.emit(OpError)
case *ast.ImmutableExpr:
if err := c.Compile(node.Expr); err != nil {
return err
}
c.emit(OpImmutable)
}
return nil

View file

@ -35,6 +35,7 @@ const (
OpArray // Array object
OpMap // Map object
OpError // Error object
OpImmutable // Immutable object
OpIndex // Index operation
OpSliceIndex // Slice operation
OpCall // Call function
@ -94,6 +95,7 @@ var OpcodeNames = [...]string{
OpArray: "ARR",
OpMap: "MAP",
OpError: "ERROR",
OpImmutable: "IMMUT",
OpIndex: "INDEX",
OpSliceIndex: "SLICE",
OpCall: "CALL",
@ -150,6 +152,7 @@ var OpcodeOperands = [...][]int{
OpArray: {2},
OpMap: {2},
OpError: {},
OpImmutable: {},
OpIndex: {},
OpSliceIndex: {},
OpCall: {1},

View file

@ -361,6 +361,9 @@ func (p *Parser) parseOperand() ast.Expr {
case token.Error: // error expression
return p.parseErrorExpr()
case token.Immutable: // immutable expression
return p.parseImmutableExpr()
}
pos := p.pos
@ -483,6 +486,25 @@ func (p *Parser) parseErrorExpr() ast.Expr {
return expr
}
func (p *Parser) parseImmutableExpr() ast.Expr {
pos := p.pos
p.next()
lparen := p.expect(token.LParen)
value := p.parseExpr()
rparen := p.expect(token.RParen)
expr := &ast.ImmutableExpr{
ErrorPos: pos,
Expr: value,
LParen: lparen,
RParen: rparen,
}
return expr
}
func (p *Parser) parseFuncType() *ast.FuncType {
if p.trace {
defer un(trace(p, "FuncType"))
@ -572,7 +594,7 @@ func (p *Parser) parseStmt() (stmt ast.Stmt) {
switch p.token {
case // simple statements
token.Func, token.Error, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False,
token.Func, token.Error, token.Immutable, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False,
token.Undefined, token.Import, token.LParen, token.LBrace, token.LBrack,
token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not:
s := p.parseSimpleStmt(false)

View file

@ -74,6 +74,7 @@ const (
For
Func
Error
Immutable
If
Return
Switch
@ -149,6 +150,7 @@ var tokens = [...]string{
For: "for",
Func: "func",
Error: "error",
Immutable: "immutable",
If: "if",
Return: "return",
Switch: "switch",

View file

@ -150,9 +150,43 @@ for k, v in {k1: 1, k2: 2} { // map: key and value
}
```
## Immutable Values
Basically, all values of the primitive types (Int, Float, String, Bytes, Char, Bool) are immutable.
```golang
s := "12345"
s[1] = 'b' // error: String is immutable
s = "foo" // ok: this is not mutating the value
// but updating reference 's' with another String value
```
The composite types (Array, Map) are mutable by default, but, you can make them immutable using `immutable` expression.
```golang
a := [1, 2, 3]
a[1] = "foo" // ok: array is mutable
b := immutable([1, 2, 3])
b[1] = "foo" // error: 'b' references to an immutable array.
b = "foo" // ok: this is not mutating the value of array
// but updating reference 'b' with different value
```
Not that, if you copy (using `copy` builtin function) an immutable value, it will return a "mutable" copy. Also, immutability is not applied to the individual elements of the array or map value, unless they are explicitly made immutable.
```golang
a := immutable({b: 4, c: [1, 2, 3]})
a.b = 5 // error
a.c[1] = 5 // ok: because 'a.c' is not immutable
a = immutable({b: 4, c: immutable([1, 2, 3])})
a.c[1] = 5 // error
```
## Errors
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.
An error object is created using `error` expression. An error can contain value of any types, and, the underlying value can be read using `.value` selector.
```golang
err1 := error("oops") // error with string value

View file

@ -60,17 +60,22 @@ func (o *Array) IsFalsy() bool {
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *Array) Equals(x Object) bool {
t, ok := x.(*Array)
if !ok {
var xVal []Object
switch x := x.(type) {
case *Array:
xVal = x.Value
case *ImmutableArray:
xVal = x.Value
default:
return false
}
if len(o.Value) != len(t.Value) {
if len(o.Value) != len(xVal) {
return false
}
for i, e := range o.Value {
if !e.Equals(t.Value[i]) {
if !e.Equals(xVal[i]) {
return false
}
}

109
objects/immautable_array.go Normal file
View file

@ -0,0 +1,109 @@
package objects
import (
"fmt"
"strings"
"github.com/d5/tengo/compiler/token"
)
// ImmutableArray represents an immutable array of objects.
type ImmutableArray struct {
Value []Object
}
// TypeName returns the name of the type.
func (o *ImmutableArray) TypeName() string {
return "immutable-array"
}
func (o *ImmutableArray) String() string {
var elements []string
for _, e := range o.Value {
elements = append(elements, e.String())
}
return fmt.Sprintf("[%s]", strings.Join(elements, ", "))
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *ImmutableArray) BinaryOp(op token.Token, rhs Object) (Object, error) {
if rhs, ok := rhs.(*ImmutableArray); ok {
switch op {
case token.Add:
return &Array{Value: append(o.Value, rhs.Value...)}, nil
}
}
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *ImmutableArray) Copy() Object {
var c []Object
for _, elem := range o.Value {
c = append(c, elem.Copy())
}
return &Array{Value: c}
}
// IsFalsy returns true if the value of the type is falsy.
func (o *ImmutableArray) IsFalsy() bool {
return len(o.Value) == 0
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *ImmutableArray) Equals(x Object) bool {
var xVal []Object
switch x := x.(type) {
case *Array:
xVal = x.Value
case *ImmutableArray:
xVal = x.Value
default:
return false
}
if len(o.Value) != len(xVal) {
return false
}
for i, e := range o.Value {
if !e.Equals(xVal[i]) {
return false
}
}
return true
}
// IndexGet returns an element at a given index.
func (o *ImmutableArray) IndexGet(index Object) (res Object, err error) {
intIdx, ok := index.(*Int)
if !ok {
err = ErrInvalidIndexType
return
}
idxVal := int(intIdx.Value)
if idxVal < 0 || idxVal >= len(o.Value) {
err = ErrIndexOutOfBounds
return
}
res = o.Value[idxVal]
return
}
// Iterate creates an array iterator.
func (o *ImmutableArray) Iterate() Iterator {
return &ArrayIterator{
v: o.Value,
l: len(o.Value),
}
}

View file

@ -39,7 +39,7 @@ func (o *ImmutableMap) Copy() Object {
c[k] = v.Copy()
}
return &ImmutableMap{Value: c}
return &Map{Value: c}
}
// IsFalsy returns true if the value of the type is falsy.
@ -66,17 +66,22 @@ func (o *ImmutableMap) IndexGet(index Object) (res Object, err error) {
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *ImmutableMap) Equals(x Object) bool {
t, ok := x.(*ImmutableMap)
if !ok {
var xVal map[string]Object
switch x := x.(type) {
case *Map:
xVal = x.Value
case *ImmutableMap:
xVal = x.Value
default:
return false
}
if len(o.Value) != len(t.Value) {
if len(o.Value) != len(xVal) {
return false
}
for k, v := range o.Value {
tv := t.Value[k]
tv := xVal[k]
if !v.Equals(tv) {
return false
}
@ -92,7 +97,7 @@ func (o *ImmutableMap) Iterate() Iterator {
keys = append(keys, k)
}
return &ImmutableMapIterator{
return &MapIterator{
v: o.Value,
k: keys,
l: len(keys),

View file

@ -1,62 +0,0 @@
package objects
import "github.com/d5/tengo/compiler/token"
// ImmutableMapIterator represents an iterator for the immutable map.
type ImmutableMapIterator struct {
v map[string]Object
k []string
i int
l int
}
// TypeName returns the name of the type.
func (i *ImmutableMapIterator) TypeName() string {
return "module-iterator"
}
func (i *ImmutableMapIterator) String() string {
return "<module-iterator>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (i *ImmutableMapIterator) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// IsFalsy returns true if the value of the type is falsy.
func (i *ImmutableMapIterator) IsFalsy() bool {
return true
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (i *ImmutableMapIterator) Equals(Object) bool {
return false
}
// Copy returns a copy of the type.
func (i *ImmutableMapIterator) Copy() Object {
return &ImmutableMapIterator{v: i.v, k: i.k, i: i.i, l: i.l}
}
// Next returns true if there are more elements to iterate.
func (i *ImmutableMapIterator) Next() bool {
i.i++
return i.i <= i.l
}
// Key returns the key or index value of the current element.
func (i *ImmutableMapIterator) Key() Object {
k := i.k[i.i-1]
return &String{Value: k}
}
// Value returns the value of the current element.
func (i *ImmutableMapIterator) Value() Object {
k := i.k[i.i-1]
return i.v[k]
}

View file

@ -50,17 +50,22 @@ func (o *Map) IsFalsy() bool {
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *Map) Equals(x Object) bool {
t, ok := x.(*Map)
if !ok {
var xVal map[string]Object
switch x := x.(type) {
case *Map:
xVal = x.Value
case *ImmutableMap:
xVal = x.Value
default:
return false
}
if len(o.Value) != len(t.Value) {
if len(o.Value) != len(xVal) {
return false
}
for k, v := range o.Value {
tv := t.Value[k]
tv := xVal[k]
if !v.Equals(tv) {
return false
}

View file

@ -299,7 +299,7 @@ func (v *VM) Run() error {
return ErrStackOverflow
}
if (*right).Equals(*left) {
if (*left).Equals(*right) {
v.stack[v.sp] = truePtr
} else {
v.stack[v.sp] = falsePtr
@ -315,7 +315,7 @@ func (v *VM) Run() error {
return ErrStackOverflow
}
if (*right).Equals(*left) {
if (*left).Equals(*right) {
v.stack[v.sp] = falsePtr
} else {
v.stack[v.sp] = truePtr
@ -549,18 +549,28 @@ func (v *VM) Run() error {
case compiler.OpError:
value := v.stack[v.sp-1]
v.sp--
var err objects.Object = &objects.Error{
Value: *value,
}
if v.sp >= StackSize {
return ErrStackOverflow
}
v.stack[v.sp-1] = &err
v.stack[v.sp] = &err
v.sp++
case compiler.OpImmutable:
value := v.stack[v.sp-1]
switch value := (*value).(type) {
case *objects.Array:
var immutableArray objects.Object = &objects.ImmutableArray{
Value: value.Value,
}
v.stack[v.sp-1] = &immutableArray
case *objects.Map:
var immutableMap objects.Object = &objects.ImmutableMap{
Value: value.Value,
}
v.stack[v.sp-1] = &immutableMap
}
case compiler.OpIndex:
index := v.stack[v.sp-1]
@ -653,6 +663,31 @@ func (v *VM) Run() error {
v.stack[v.sp] = &val
v.sp++
case *objects.ImmutableArray:
numElements := int64(len(left.Value))
if lowIdx < 0 || lowIdx >= numElements {
return fmt.Errorf("index out of bounds: %d", lowIdx)
}
if highIdx < 0 {
highIdx = numElements
} else if highIdx < 0 || highIdx > numElements {
return fmt.Errorf("index out of bounds: %d", highIdx)
}
if lowIdx > highIdx {
return fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx)
}
if v.sp >= StackSize {
return ErrStackOverflow
}
var val objects.Object = &objects.Array{Value: left.Value[lowIdx:highIdx]}
v.stack[v.sp] = &val
v.sp++
case *objects.String:
numElements := int64(len(left.Value))

View file

@ -0,0 +1,48 @@
package runtime_test
import "testing"
func TestImmutable(t *testing.T) {
// primitive types are already immutable values
// immutable expression has no effects.
expect(t, `a := immutable(1); out = a`, 1)
expect(t, `a := 5; b := immutable(a); out = b`, 5)
expect(t, `a := immutable(1); a = 5; out = a`, 5)
// array
expectError(t, `a := immutable([1, 2, 3]); a[1] = 5`)
expectError(t, `a := immutable(["foo", [1,2,3]]); a[1] = "bar"`)
expect(t, `a := immutable(["foo", [1,2,3]]); a[1][1] = "bar"; out = a`, IARR{"foo", ARR{1, "bar", 3}})
expectError(t, `a := immutable(["foo", immutable([1,2,3])]); a[1][1] = "bar"`)
expectError(t, `a := ["foo", immutable([1,2,3])]; a[1][1] = "bar"`)
expect(t, `a := immutable([1,2,3]); b := copy(a); b[1] = 5; out = b`, ARR{1, 5, 3})
expect(t, `a := immutable([1,2,3]); b := copy(a); b[1] = 5; out = a`, IARR{1, 2, 3})
expect(t, `out = immutable([1,2,3]) == [1,2,3]`, true)
expect(t, `out = immutable([1,2,3]) == immutable([1,2,3])`, true)
expect(t, `out = [1,2,3] == immutable([1,2,3])`, true)
expect(t, `out = immutable([1,2,3]) == [1,2]`, false)
expect(t, `out = immutable([1,2,3]) == immutable([1,2])`, false)
expect(t, `out = [1,2,3] == immutable([1,2])`, false)
expect(t, `out = immutable([1, 2, 3, 4])[1]`, 2)
expect(t, `out = immutable([1, 2, 3, 4])[1:3]`, ARR{2, 3})
expect(t, `a := immutable([1,2,3]); a = 5; out = a`, 5)
// map
expectError(t, `a := immutable({b: 1, c: 2}); a.b = 5`)
expectError(t, `a := immutable({b: 1, c: 2}); a["b"] = "bar"`)
expect(t, `a := immutable({b: 1, c: [1,2,3]}); a.c[1] = "bar"; out = a`, IMAP{"b": 1, "c": ARR{1, "bar", 3}})
expectError(t, `a := immutable({b: 1, c: immutable([1,2,3])}); a.c[1] = "bar"`)
expectError(t, `a := {b: 1, c: immutable([1,2,3])}; a.c[1] = "bar"`)
expect(t, `out = immutable({a:1,b:2}) == {a:1,b:2}`, true)
expect(t, `out = immutable({a:1,b:2}) == immutable({a:1,b:2})`, true)
expect(t, `out = {a:1,b:2} == immutable({a:1,b:2})`, true)
expect(t, `out = immutable({a:1,b:2}) == {a:1,b:3}`, false)
expect(t, `out = immutable({a:1,b:2}) == immutable({a:1,b:3})`, false)
expect(t, `out = {a:1,b:2} == immutable({a:1,b:3})`, false)
expect(t, `out = immutable({a:1,b:2}).b`, 2)
expect(t, `out = immutable({a:1,b:2})["b"]`, 2)
expect(t, `a := immutable({a:1,b:2}); a = 5; out = 5`, 5)
expect(t, `a := immutable({b: 5, c: "foo"}); out = a.b`, 5)
expectError(t, `a := immutable({b: 5, c: "foo"}); a.b = 10`)
}

View file

@ -20,6 +20,8 @@ const (
testOut = "out"
)
type IARR []interface{}
type IMAP map[string]interface{}
type MAP = map[string]interface{}
type ARR = []interface{}
type SYM = map[string]objects.Object
@ -154,6 +156,20 @@ func toObject(v interface{}) objects.Object {
}
return &objects.Array{Value: objs}
case IMAP:
objs := make(map[string]objects.Object)
for k, v := range v {
objs[k] = toObject(v)
}
return &objects.ImmutableMap{Value: objs}
case IARR:
var objs []objects.Object
for _, e := range v {
objs = append(objs, toObject(e))
}
return &objects.ImmutableArray{Value: objs}
}
panic(fmt.Errorf("unknown type: %T", v))
@ -315,6 +331,10 @@ func objectZeroCopy(o objects.Object) objects.Object {
return &objects.Error{}
case *objects.Bytes:
return &objects.Bytes{}
case *objects.ImmutableArray:
return &objects.ImmutableArray{}
case *objects.ImmutableMap:
return &objects.ImmutableMap{}
case nil:
panic("nil")
default: