Skip to content

[go1.20] Fixing reflect Value.Grow #1315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions compiler/natives/src/reflect/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
75 changes: 55 additions & 20 deletions compiler/prelude/prelude.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
1 change: 1 addition & 0 deletions compiler/prelude/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
38 changes: 38 additions & 0 deletions tests/arrays_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading