diff --git a/docs/objects.md b/docs/objects.md index b36c127..b067e08 100644 --- a/docs/objects.md +++ b/docs/objects.md @@ -84,7 +84,7 @@ If `IndexGet` returns an error (`err`), the VM will treat it as a run-time error Array and Map implementation forces the type of index Object to be Int and String respectively, but, it's not a required behavior of the VM. It is completely okay to take various index types as long as it 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 (instead of a run-time error) when the key does not exist. But, again, this is not a required behavior. +By convention, Array or Array-like types and Map or Map-like types return `Undefined` value when the key does not exist. But, again, this is not a required behavior. ### Index-Assignable Interface @@ -98,8 +98,6 @@ type IndexAssignable interface { Array and Map implementation forces the type of index Object to be Int and String respectively, but, it's not a required behavior of the VM. It is completely okay to take various index types as long as it 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). - ### Iterable Interface If the type implements [Iterable](https://godoc.org/github.com/d5/tengo/objects#Iterable) interface, its values can be used in `for-in` statements (`for key, value in object { ... }`). diff --git a/docs/tutorial.md b/docs/tutorial.md index 5f446c8..a652e83 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -68,6 +68,8 @@ _See [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types. You can use the dot selector (`.`) and indexer (`[]`) operator to read or write elements of arrays, strings, or maps. +Reading a nonexistent index returns `Undefined` value. + ```golang ["one", "two", "three"][1] // == "two" @@ -80,6 +82,8 @@ m.a // == 1 m["b"][1] // == 3 m.c() // == 10 m.x = 5 // add 'x' to map 'm' +m["b"][5] // == undefined +m["b"][5].d // == undefined //m.b[5] = 0 // but this is an error: index out of bounds ``` > [Run in Playground](https://tengolang.com/?s=d510c75ed8f06ef1e22c1aaf8a7d4565c793514c) @@ -91,6 +95,7 @@ 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" +c := [1, 2, 3, 4, 5][-1:10] // == [1, 2, 3, 4, 5] ``` > [Run in Playground](https://tengolang.com/?s=214ab490bb24549578770984985f6b161aed915d) diff --git a/objects/array.go b/objects/array.go index 20de2e1..7f3cd0a 100644 --- a/objects/array.go +++ b/objects/array.go @@ -94,7 +94,7 @@ func (o *Array) IndexGet(index Object) (res Object, err error) { idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { - err = ErrIndexOutOfBounds + res = UndefinedValue return } diff --git a/objects/bytes.go b/objects/bytes.go index f77d969..7d8d669 100644 --- a/objects/bytes.go +++ b/objects/bytes.go @@ -66,7 +66,7 @@ func (o *Bytes) IndexGet(index Object) (res Object, err error) { idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { - err = ErrIndexOutOfBounds + res = UndefinedValue return } diff --git a/objects/immautable_array.go b/objects/immautable_array.go index 36767ae..f3621e2 100644 --- a/objects/immautable_array.go +++ b/objects/immautable_array.go @@ -91,7 +91,7 @@ func (o *ImmutableArray) IndexGet(index Object) (res Object, err error) { idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { - err = ErrIndexOutOfBounds + res = UndefinedValue return } diff --git a/objects/string.go b/objects/string.go index f1c20e8..6a53b44 100644 --- a/objects/string.go +++ b/objects/string.go @@ -73,7 +73,7 @@ func (o *String) IndexGet(index Object) (res Object, err error) { } if idxVal < 0 || idxVal >= len(o.runeStr) { - err = ErrIndexOutOfBounds + res = UndefinedValue return } diff --git a/objects/undefined.go b/objects/undefined.go index ab6f549..79a380f 100644 --- a/objects/undefined.go +++ b/objects/undefined.go @@ -35,3 +35,8 @@ func (o *Undefined) IsFalsy() bool { func (o *Undefined) Equals(x Object) bool { return o == x } + +// IndexGet returns an element at a given index. +func (o *Undefined) IndexGet(index Object) (Object, error) { + return UndefinedValue, nil +} diff --git a/runtime/vm.go b/runtime/vm.go index cc1990b..f0b85d9 100644 --- a/runtime/vm.go +++ b/runtime/vm.go @@ -631,57 +631,69 @@ func (v *VM) Run() error { } } - var highIdx int64 - if *high == objects.UndefinedValue { - highIdx = -1 - } else if high, ok := (*high).(*objects.Int); ok { - highIdx = high.Value - } else { - return fmt.Errorf("non-integer slice index: %s", high.TypeName()) - } - switch left := (*left).(type) { case *objects.Array: numElements := int64(len(left.Value)) - - if lowIdx < 0 || lowIdx > numElements { - return fmt.Errorf("index out of bounds: %d", lowIdx) - } - if highIdx < 0 { + var highIdx int64 + if *high == objects.UndefinedValue { highIdx = numElements - } else if highIdx < 0 || highIdx > numElements { - return fmt.Errorf("index out of bounds: %d", highIdx) + } else if high, ok := (*high).(*objects.Int); ok { + highIdx = high.Value + } else { + return fmt.Errorf("non-integer slice index: %s", high.TypeName()) } if lowIdx > highIdx { return fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) } + if lowIdx < 0 { + lowIdx = 0 + } else if lowIdx > numElements { + lowIdx = numElements + } + + if highIdx < 0 { + highIdx = 0 + } else if highIdx > numElements { + highIdx = numElements + } + 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.ImmutableArray: numElements := int64(len(left.Value)) - - if lowIdx < 0 || lowIdx > numElements { - return fmt.Errorf("index out of bounds: %d", lowIdx) - } - if highIdx < 0 { + var highIdx int64 + if *high == objects.UndefinedValue { highIdx = numElements - } else if highIdx < 0 || highIdx > numElements { - return fmt.Errorf("index out of bounds: %d", highIdx) + } else if high, ok := (*high).(*objects.Int); ok { + highIdx = high.Value + } else { + return fmt.Errorf("non-integer slice index: %s", high.TypeName()) } if lowIdx > highIdx { return fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) } + if lowIdx < 0 { + lowIdx = 0 + } else if lowIdx > numElements { + lowIdx = numElements + } + + if highIdx < 0 { + highIdx = 0 + } else if highIdx > numElements { + highIdx = numElements + } + if v.sp >= StackSize { return ErrStackOverflow } @@ -693,20 +705,31 @@ func (v *VM) Run() error { case *objects.String: numElements := int64(len(left.Value)) - - if lowIdx < 0 || lowIdx > numElements { - return fmt.Errorf("index out of bounds: %d", lowIdx) - } - if highIdx < 0 { + var highIdx int64 + if *high == objects.UndefinedValue { highIdx = numElements - } else if highIdx < 0 || highIdx > numElements { - return fmt.Errorf("index out of bounds: %d", highIdx) + } else if high, ok := (*high).(*objects.Int); ok { + highIdx = high.Value + } else { + return fmt.Errorf("non-integer slice index: %s", high.TypeName()) } if lowIdx > highIdx { return fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) } + if lowIdx < 0 { + lowIdx = 0 + } else if lowIdx > numElements { + lowIdx = numElements + } + + if highIdx < 0 { + highIdx = 0 + } else if highIdx > numElements { + highIdx = numElements + } + if v.sp >= StackSize { return ErrStackOverflow } @@ -718,20 +741,31 @@ func (v *VM) Run() error { case *objects.Bytes: numElements := int64(len(left.Value)) - - if lowIdx < 0 || lowIdx >= numElements { - return fmt.Errorf("index out of bounds: %d", lowIdx) - } - if highIdx < 0 { + var highIdx int64 + if *high == objects.UndefinedValue { highIdx = numElements - } else if highIdx < 0 || highIdx > numElements { - return fmt.Errorf("index out of bounds: %d", highIdx) + } else if high, ok := (*high).(*objects.Int); ok { + highIdx = high.Value + } else { + return fmt.Errorf("non-integer slice index: %s", high.TypeName()) } if lowIdx > highIdx { return fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) } + if lowIdx < 0 { + lowIdx = 0 + } else if lowIdx > numElements { + lowIdx = numElements + } + + if highIdx < 0 { + highIdx = 0 + } else if highIdx > numElements { + highIdx = numElements + } + if v.sp >= StackSize { return ErrStackOverflow } @@ -740,9 +774,6 @@ func (v *VM) Run() error { v.stack[v.sp] = &val v.sp++ - - default: - return fmt.Errorf("cannot slice %s", left.TypeName()) } case compiler.OpCall: diff --git a/runtime/vm_array_test.go b/runtime/vm_array_test.go index 2fe32e4..a793382 100644 --- a/runtime/vm_array_test.go +++ b/runtime/vm_array_test.go @@ -3,6 +3,8 @@ package runtime_test import ( "fmt" "testing" + + "github.com/d5/tengo/objects" ) func TestArray(t *testing.T) { @@ -12,6 +14,9 @@ func TestArray(t *testing.T) { expect(t, `a1 := [1, 2, 3]; a2 := a1; a1[0] = 5; out = a2`, ARR{5, 2, 3}) expect(t, `func () { a1 := [1, 2, 3]; a2 := a1; a1[0] = 5; out = a2 }()`, ARR{5, 2, 3}) + // array index set + expectError(t, `a1 := [1, 2, 3]; a1[3] = 5`) + // index operator arr := ARR{1, 2, 3, 4, 5, 6} arrStr := `[1, 2, 3, 4, 5, 6]` @@ -22,21 +27,29 @@ func TestArray(t *testing.T) { expect(t, fmt.Sprintf("out = %s[1 + %d - 1]", arrStr, idx), arr[idx]) expect(t, fmt.Sprintf("idx := %d; out = %s[idx]", idx, arrStr), arr[idx]) } - expectError(t, fmt.Sprintf("%s[%d]", arrStr, -1)) - expectError(t, fmt.Sprintf("%s[%d]", arrStr, arrLen)) + + expect(t, fmt.Sprintf("%s[%d]", arrStr, -1), objects.UndefinedValue) + expect(t, fmt.Sprintf("%s[%d]", arrStr, arrLen), objects.UndefinedValue) // slice operator for low := 0; low < arrLen; low++ { + expect(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, low, low), ARR{}) for high := low; high <= arrLen; high++ { expect(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, low, high), arr[low:high]) expect(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", arrStr, low, high), arr[low:high]) expect(t, fmt.Sprintf("out = %s[1 + %d - 1 : 1 + %d - 1]", arrStr, low, high), arr[low:high]) expect(t, fmt.Sprintf("out = %s[:%d]", arrStr, high), arr[:high]) expect(t, fmt.Sprintf("out = %s[%d:]", arrStr, low), arr[low:]) - expect(t, fmt.Sprintf("out = %s[:]", arrStr), arr[:]) } } - expectError(t, fmt.Sprintf("%s[%d:]", arrStr, -1)) - expectError(t, fmt.Sprintf("%s[:%d]", arrStr, arrLen+1)) + + expect(t, fmt.Sprintf("out = %s[:]", arrStr), arr) + expect(t, fmt.Sprintf("out = %s[%d:]", arrStr, -1), arr) + expect(t, fmt.Sprintf("out = %s[:%d]", arrStr, arrLen+1), arr) + expect(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, 2, 2), ARR{}) + + expectError(t, fmt.Sprintf("out = %s[:%d]", arrStr, -1)) + expectError(t, fmt.Sprintf("out = %s[%d:]", arrStr, arrLen+1)) + expectError(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, 0, -1)) expectError(t, fmt.Sprintf("%s[%d:%d]", arrStr, 2, 1)) } diff --git a/runtime/vm_bytes_test.go b/runtime/vm_bytes_test.go index 9ea13c7..3003f22 100644 --- a/runtime/vm_bytes_test.go +++ b/runtime/vm_bytes_test.go @@ -2,6 +2,8 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestBytes(t *testing.T) { @@ -12,4 +14,5 @@ func TestBytes(t *testing.T) { expect(t, `out = bytes("abcde")[0]`, 97) expect(t, `out = bytes("abcde")[1]`, 98) expect(t, `out = bytes("abcde")[4]`, 101) + expect(t, `out = bytes("abcde")[10]`, objects.UndefinedValue) } diff --git a/runtime/vm_immutable_test.go b/runtime/vm_immutable_test.go index 248f0cf..73fed92 100644 --- a/runtime/vm_immutable_test.go +++ b/runtime/vm_immutable_test.go @@ -1,6 +1,10 @@ package runtime_test -import "testing" +import ( + "testing" + + "github.com/d5/tengo/objects" +) func TestImmutable(t *testing.T) { // primitive types are already immutable values @@ -26,6 +30,7 @@ func TestImmutable(t *testing.T) { 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) + expect(t, `a := immutable([1, 2, 3]); out = a[5]`, objects.UndefinedValue) // map expectError(t, `a := immutable({b: 1, c: 2}); a.b = 5`) @@ -42,6 +47,7 @@ func TestImmutable(t *testing.T) { 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({a:1,b:2}); out = a.c`, objects.UndefinedValue) expect(t, `a := immutable({b: 5, c: "foo"}); out = a.b`, 5) expectError(t, `a := immutable({b: 5, c: "foo"}); a.b = 10`) diff --git a/runtime/vm_string_test.go b/runtime/vm_string_test.go index 76611ff..7b13a4f 100644 --- a/runtime/vm_string_test.go +++ b/runtime/vm_string_test.go @@ -3,6 +3,8 @@ package runtime_test import ( "fmt" "testing" + + "github.com/d5/tengo/objects" ) func TestString(t *testing.T) { @@ -24,22 +26,31 @@ func TestString(t *testing.T) { expect(t, fmt.Sprintf("out = %s[1 + %d - 1]", strStr, idx), str[idx]) expect(t, fmt.Sprintf("idx := %d; out = %s[idx]", idx, strStr), str[idx]) } - expectError(t, fmt.Sprintf("%s[%d]", strStr, -1)) - expectError(t, fmt.Sprintf("%s[%d]", strStr, strLen)) + + expect(t, fmt.Sprintf("%s[%d]", strStr, -1), objects.UndefinedValue) + expect(t, fmt.Sprintf("%s[%d]", strStr, strLen), objects.UndefinedValue) // slice operator for low := 0; low <= strLen; low++ { + expect(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, low), "") for high := low; high <= strLen; high++ { expect(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, high), str[low:high]) expect(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", strStr, low, high), str[low:high]) expect(t, fmt.Sprintf("out = %s[1 + %d - 1 : 1 + %d - 1]", strStr, low, high), str[low:high]) expect(t, fmt.Sprintf("out = %s[:%d]", strStr, high), str[:high]) expect(t, fmt.Sprintf("out = %s[%d:]", strStr, low), str[low:]) - expect(t, fmt.Sprintf("out = %s[:]", strStr), str[:]) } } - expectError(t, fmt.Sprintf("%s[%d:]", strStr, -1)) - expectError(t, fmt.Sprintf("%s[:%d]", strStr, strLen+1)) + + expect(t, fmt.Sprintf("out = %s[:]", strStr), str[:]) + expect(t, fmt.Sprintf("out = %s[:]", strStr), str) + expect(t, fmt.Sprintf("out = %s[%d:]", strStr, -1), str) + expect(t, fmt.Sprintf("out = %s[:%d]", strStr, strLen+1), str) + expect(t, fmt.Sprintf("out = %s[%d:%d]", strStr, 2, 2), "") + + expectError(t, fmt.Sprintf("out = %s[:%d]", strStr, -1)) + expectError(t, fmt.Sprintf("out = %s[%d:]", strStr, strLen+1)) + expectError(t, fmt.Sprintf("out = %s[%d:%d]", strStr, 0, -1)) expectError(t, fmt.Sprintf("%s[%d:%d]", strStr, 2, 1)) // string concatenation with other types diff --git a/runtime/vm_undefined_test.go b/runtime/vm_undefined_test.go index 4e8b75e..460a9a9 100644 --- a/runtime/vm_undefined_test.go +++ b/runtime/vm_undefined_test.go @@ -8,6 +8,11 @@ import ( func TestUndefined(t *testing.T) { expect(t, `out = undefined`, objects.UndefinedValue) + expect(t, `out = undefined.a`, objects.UndefinedValue) + expect(t, `out = undefined[1]`, objects.UndefinedValue) + expect(t, `out = undefined.a.b`, objects.UndefinedValue) + expect(t, `out = undefined[1][2]`, objects.UndefinedValue) + expect(t, `out = undefined ? 1 : 2`, 2) expect(t, `out = undefined == undefined`, true) expect(t, `out = undefined == 1`, false) expect(t, `out = 1 == undefined`, false)