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) - Simple and highly readable [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- Dynamic typing with type coercion - Dynamic typing with type coercion
- Higher-order functions and closures - Higher-order functions and closures
- Immutable values _(v1)_ - Immutable values
- Garbage collection - 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) - [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)_ - 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: case *objects.ReturnValue:
return Equal(t, expected.Value, actual.(objects.ReturnValue).Value) return Equal(t, expected.Value, actual.(objects.ReturnValue).Value)
case *objects.Array: 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: case *objects.Bytes:
if bytes.Compare(expected.Value, actual.(*objects.Bytes).Value) != 0 { if bytes.Compare(expected.Value, actual.(*objects.Bytes).Value) != 0 {
return failExpectedActual(t, expected.Value, actual.(*objects.Bytes).Value, msg...) return failExpectedActual(t, expected.Value, actual.(*objects.Bytes).Value, msg...)
} }
case *objects.Map: 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: case *objects.CompiledFunction:
return equalCompiledFunction(t, expected, actual.(*objects.CompiledFunction)) return equalCompiledFunction(t, expected, actual.(*objects.CompiledFunction))
case *objects.Closure: case *objects.Closure:
@ -252,13 +256,6 @@ func equalSymbol(a, b compiler.Symbol) bool {
a.Scope == b.Scope 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 { func equalObjectSlice(t *testing.T, expected, actual []objects.Object) bool {
// TODO: this test does not differentiate nil vs empty slice // 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 return true
} }
func equalMap(t *testing.T, expected, actual objects.Object) bool { func equalObjectMap(t *testing.T, expected, actual map[string]objects.Object) bool {
expectedT := expected.(*objects.Map).Value if !Equal(t, len(expected), len(actual)) {
actualT := actual.(*objects.Map).Value
if !Equal(t, len(expectedT), len(actualT)) {
return false return false
} }
for key, expectedVal := range expectedT { for key, expectedVal := range expected {
actualVal := actualT[key] actualVal := actual[key]
if !Equal(t, expectedVal, actualVal) { if !Equal(t, expectedVal, actualVal) {
return false 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.Bool{})
gob.Register(&objects.Char{}) gob.Register(&objects.Char{})
gob.Register(&objects.Array{}) gob.Register(&objects.Array{})
gob.Register(&objects.ImmutableArray{})
gob.Register(&objects.Map{}) gob.Register(&objects.Map{})
gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.CompiledFunction{}) gob.Register(&objects.CompiledFunction{})
gob.Register(&objects.Undefined{}) gob.Register(&objects.Undefined{})
gob.Register(&objects.Error{}) gob.Register(&objects.Error{})
gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.Bytes{}) gob.Register(&objects.Bytes{})
gob.Register(&objects.StringIterator{}) gob.Register(&objects.StringIterator{})
gob.Register(&objects.MapIterator{}) gob.Register(&objects.MapIterator{})
gob.Register(&objects.ImmutableMapIterator{})
gob.Register(&objects.ArrayIterator{}) gob.Register(&objects.ArrayIterator{})
} }

View file

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

View file

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

View file

@ -361,6 +361,9 @@ func (p *Parser) parseOperand() ast.Expr {
case token.Error: // error expression case token.Error: // error expression
return p.parseErrorExpr() return p.parseErrorExpr()
case token.Immutable: // immutable expression
return p.parseImmutableExpr()
} }
pos := p.pos pos := p.pos
@ -483,6 +486,25 @@ func (p *Parser) parseErrorExpr() ast.Expr {
return 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 { func (p *Parser) parseFuncType() *ast.FuncType {
if p.trace { if p.trace {
defer un(trace(p, "FuncType")) defer un(trace(p, "FuncType"))
@ -572,7 +594,7 @@ func (p *Parser) parseStmt() (stmt ast.Stmt) {
switch p.token { switch p.token {
case // simple statements 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.Undefined, token.Import, token.LParen, token.LBrace, token.LBrack,
token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not: token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not:
s := p.parseSimpleStmt(false) s := p.parseSimpleStmt(false)

View file

@ -74,6 +74,7 @@ const (
For For
Func Func
Error Error
Immutable
If If
Return Return
Switch Switch
@ -149,6 +150,7 @@ var tokens = [...]string{
For: "for", For: "for",
Func: "func", Func: "func",
Error: "error", Error: "error",
Immutable: "immutable",
If: "if", If: "if",
Return: "return", Return: "return",
Switch: "switch", 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 ## 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 ```golang
err1 := error("oops") // error with string value 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 // Equals returns true if the value of the type
// is equal to the value of another object. // is equal to the value of another object.
func (o *Array) Equals(x Object) bool { func (o *Array) Equals(x Object) bool {
t, ok := x.(*Array) var xVal []Object
if !ok { switch x := x.(type) {
case *Array:
xVal = x.Value
case *ImmutableArray:
xVal = x.Value
default:
return false return false
} }
if len(o.Value) != len(t.Value) { if len(o.Value) != len(xVal) {
return false return false
} }
for i, e := range o.Value { for i, e := range o.Value {
if !e.Equals(t.Value[i]) { if !e.Equals(xVal[i]) {
return false 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() c[k] = v.Copy()
} }
return &ImmutableMap{Value: c} return &Map{Value: c}
} }
// IsFalsy returns true if the value of the type is falsy. // 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 // Equals returns true if the value of the type
// is equal to the value of another object. // is equal to the value of another object.
func (o *ImmutableMap) Equals(x Object) bool { func (o *ImmutableMap) Equals(x Object) bool {
t, ok := x.(*ImmutableMap) var xVal map[string]Object
if !ok { switch x := x.(type) {
case *Map:
xVal = x.Value
case *ImmutableMap:
xVal = x.Value
default:
return false return false
} }
if len(o.Value) != len(t.Value) { if len(o.Value) != len(xVal) {
return false return false
} }
for k, v := range o.Value { for k, v := range o.Value {
tv := t.Value[k] tv := xVal[k]
if !v.Equals(tv) { if !v.Equals(tv) {
return false return false
} }
@ -92,7 +97,7 @@ func (o *ImmutableMap) Iterate() Iterator {
keys = append(keys, k) keys = append(keys, k)
} }
return &ImmutableMapIterator{ return &MapIterator{
v: o.Value, v: o.Value,
k: keys, k: keys,
l: len(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 // Equals returns true if the value of the type
// is equal to the value of another object. // is equal to the value of another object.
func (o *Map) Equals(x Object) bool { func (o *Map) Equals(x Object) bool {
t, ok := x.(*Map) var xVal map[string]Object
if !ok { switch x := x.(type) {
case *Map:
xVal = x.Value
case *ImmutableMap:
xVal = x.Value
default:
return false return false
} }
if len(o.Value) != len(t.Value) { if len(o.Value) != len(xVal) {
return false return false
} }
for k, v := range o.Value { for k, v := range o.Value {
tv := t.Value[k] tv := xVal[k]
if !v.Equals(tv) { if !v.Equals(tv) {
return false return false
} }

View file

@ -299,7 +299,7 @@ func (v *VM) Run() error {
return ErrStackOverflow return ErrStackOverflow
} }
if (*right).Equals(*left) { if (*left).Equals(*right) {
v.stack[v.sp] = truePtr v.stack[v.sp] = truePtr
} else { } else {
v.stack[v.sp] = falsePtr v.stack[v.sp] = falsePtr
@ -315,7 +315,7 @@ func (v *VM) Run() error {
return ErrStackOverflow return ErrStackOverflow
} }
if (*right).Equals(*left) { if (*left).Equals(*right) {
v.stack[v.sp] = falsePtr v.stack[v.sp] = falsePtr
} else { } else {
v.stack[v.sp] = truePtr v.stack[v.sp] = truePtr
@ -549,18 +549,28 @@ func (v *VM) Run() error {
case compiler.OpError: case compiler.OpError:
value := v.stack[v.sp-1] value := v.stack[v.sp-1]
v.sp--
var err objects.Object = &objects.Error{ var err objects.Object = &objects.Error{
Value: *value, Value: *value,
} }
if v.sp >= StackSize { v.stack[v.sp-1] = &err
return ErrStackOverflow
}
v.stack[v.sp] = &err case compiler.OpImmutable:
v.sp++ 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: case compiler.OpIndex:
index := v.stack[v.sp-1] index := v.stack[v.sp-1]
@ -653,6 +663,31 @@ func (v *VM) Run() error {
v.stack[v.sp] = &val v.stack[v.sp] = &val
v.sp++ 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: case *objects.String:
numElements := int64(len(left.Value)) 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" testOut = "out"
) )
type IARR []interface{}
type IMAP map[string]interface{}
type MAP = map[string]interface{} type MAP = map[string]interface{}
type ARR = []interface{} type ARR = []interface{}
type SYM = map[string]objects.Object type SYM = map[string]objects.Object
@ -154,6 +156,20 @@ func toObject(v interface{}) objects.Object {
} }
return &objects.Array{Value: objs} 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)) panic(fmt.Errorf("unknown type: %T", v))
@ -315,6 +331,10 @@ func objectZeroCopy(o objects.Object) objects.Object {
return &objects.Error{} return &objects.Error{}
case *objects.Bytes: case *objects.Bytes:
return &objects.Bytes{} return &objects.Bytes{}
case *objects.ImmutableArray:
return &objects.ImmutableArray{}
case *objects.ImmutableMap:
return &objects.ImmutableMap{}
case nil: case nil:
panic("nil") panic("nil")
default: default: