diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index 47b93662e..eef450356 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1325,6 +1325,24 @@ func getJsTag(tag string) string { return "" } +func (v Value) grow(n int) { + if n < 0 { + panic(`reflect.Value.Grow: negative len`) + } + + s := v.object() + len := s.Get(`$length`).Int() + if len+n < 0 { + panic(`reflect.Value.Grow: slice overflow`) + } + + cap := s.Get(`$capacity`).Int() + if len+n > cap { + ns := js.Global.Call("$growSlice", s, len+n) + js.InternalObject(v.ptr).Call("$set", ns) + } +} + func (v Value) Index(i int) Value { switch k := v.kind(); k { case Array: @@ -1810,3 +1828,13 @@ func verifyNotInHeapPtr(p uintptr) bool { // always return true. return true } + +// typedslicecopy is implemented in prelude.js as $copySlice +// +//gopherjs:purge +func typedslicecopy(elemType *rtype, dst, src unsafeheader.Slice) int + +// growslice is implemented in prelude.js as $growSlice. +// +//gopherjs:purge +func growslice(t *rtype, old unsafeheader.Slice, num int) unsafeheader.Slice diff --git a/compiler/prelude/prelude.js b/compiler/prelude/prelude.js index 0f6b9cb80..e925fc321 100644 --- a/compiler/prelude/prelude.js +++ b/compiler/prelude/prelude.js @@ -428,39 +428,74 @@ var $appendSlice = (slice, toAppend) => { return $internalAppend(slice, toAppend.$array, toAppend.$offset, toAppend.$length); }; +// Internal helper function for appending to a slice. +// The given slice will not be modified. +// +// If no values are being appended, the original slice will be returned. +// Otherwise, a new slice will be created with the appended values. +// +// If the underlying array has enough capacity, it will be used. +// Otherwise, a new array will be allocated with enough capacity to hold +// the new values and the original array will not be modified. var $internalAppend = (slice, array, offset, length) => { if (length === 0) { return slice; } - var newArray = slice.$array; - var newOffset = slice.$offset; - var newLength = slice.$length + length; - var newCapacity = slice.$capacity; + let newLength = slice.$length + length; + let newSlice = $growSlice(slice, newLength); + + let newArray = newSlice.$array; + $copyArray(newArray, array, newSlice.$offset + newSlice.$length, offset, length, newSlice.constructor.elem); + + newSlice.$length = newLength; + return newSlice; +}; - if (newLength > newCapacity) { - newOffset = 0; - newCapacity = Math.max(newLength, slice.$capacity < 1024 ? slice.$capacity * 2 : Math.floor(slice.$capacity * 5 / 4)); +// Calculates the new capacity for a slice that is expected to grow to at least +// the given minCapacity. This follows the Go runtime's growth strategy. +// The oldCapacity is the current capacity of the slice that is being grown. +const $calculateNewCapacity = (minCapacity, oldCapacity) => { + return Math.max(minCapacity, oldCapacity < 1024 ? oldCapacity * 2 : Math.floor(oldCapacity * 5 / 4)); +}; - if (slice.$array.constructor === Array) { - newArray = slice.$array.slice(slice.$offset, slice.$offset + slice.$length); - newArray.length = newCapacity; - var zero = slice.constructor.elem.zero; - for (var i = slice.$length; i < newCapacity; i++) { +// Potentially grows the slice to have a capacity of at least minCapacity. +// +// A new slice will always be returned, even if the given slice had the required capacity. +// If the slice didn't have enough capacity, the new slice will have a +// new array created for it with the required minimum capacity. +// +// This takes the place of the growSlice function in the reflect package. +var $growSlice = (slice, minCapacity) => { + let array = slice.$array; + let offset = slice.$offset; + const length = slice.$length; + let capacity = slice.$capacity; + + if (minCapacity > capacity) { + capacity = $calculateNewCapacity(minCapacity, capacity); + + let newArray; + if (array.constructor === Array) { + newArray = array.slice(offset, offset + length); + newArray.length = capacity; + const zero = slice.constructor.elem.zero; + for (let i = slice.$length; i < capacity; i++) { newArray[i] = zero(); } } else { - newArray = new slice.$array.constructor(newCapacity); - newArray.set(slice.$array.subarray(slice.$offset, slice.$offset + slice.$length)); + newArray = new array.constructor(capacity); + newArray.set(array.subarray(offset, offset + length)); } - } - $copyArray(newArray, array, newOffset + slice.$length, offset, length, slice.constructor.elem); + array = newArray; + offset = 0; + } - var newSlice = new slice.constructor(newArray); - newSlice.$offset = newOffset; - newSlice.$length = newLength; - newSlice.$capacity = newCapacity; + let newSlice = new slice.constructor(array); + newSlice.$offset = offset; + newSlice.$length = length; + newSlice.$capacity = capacity; return newSlice; }; diff --git a/compiler/prelude/types.js b/compiler/prelude/types.js index 9570b2fed..08be9593b 100644 --- a/compiler/prelude/types.js +++ b/compiler/prelude/types.js @@ -231,6 +231,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { typ.comparable = false; typ.nativeArray = $nativeArray(elem.kind); typ.nil = new typ([]); + Object.freeze(typ.nil); }; break; diff --git a/tests/arrays_test.go b/tests/arrays_test.go index e79989991..feca983c5 100644 --- a/tests/arrays_test.go +++ b/tests/arrays_test.go @@ -83,3 +83,41 @@ func TestReflectArraySize(t *testing.T) { t.Errorf("array type size gave %v, want %v", got, want) } } + +func TestNilPrototypeNotModifiedByPointer(t *testing.T) { + const growth = 3 + + s1 := []int(nil) + p1 := &s1 + *p1 = make([]int, 0, growth) + if c := cap(s1); c != growth { + t.Errorf(`expected capacity of nil to increase to %d, got %d`, growth, c) + println("s1:", s1) + } + + s2 := []int(nil) + if c := cap(s2); c != 0 { + t.Errorf(`the capacity of nil must always be zero, it was %d`, c) + println("s1:", s1) + println("s2:", s2) + } +} + +func TestNilPrototypeNotModifiedByReflectGrow(t *testing.T) { + const growth = 3 + + s1 := []int(nil) + v1 := reflect.ValueOf(&s1).Elem() + v1.Grow(growth) + if c := cap(s1); c != growth { + t.Errorf(`expected capacity of nil to increase to %d, got %d`, growth, c) + println("s1:", s1) + } + + s2 := []int(nil) + if c := cap(s2); c != 0 { + t.Errorf(`the capacity of nil must always be zero, it was %d`, c) + println("s1:", s1) + println("s2:", s2) + } +}