dop251 / goja

ECMAScript/JavaScript engine in pure Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Sorting a struct slice by a compare method removes and duplicates items

jmattheis opened this issue · comments

The return value of the sort + map somehow returns 3x the bin entry and omits the log/test thing.

package main

import (
	"fmt"

	"github.com/dop251/goja"
)

type Thing struct { Name string `json:"name"` }

func main() {
	vm := goja.New()
	vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))

	vm.Set("read", func(call goja.FunctionCall) goja.Value {
		return vm.ToValue([]Thing{
			{Name: "log"},
			{Name: "etc"},
			{Name: "test"},
			{Name: "bin"},
		})
	})
	// This one works
	// vm.Set("read", func(call goja.FunctionCall) goja.Value {
	// 	x := func(name string) goja.Value {
	// 		o := vm.NewObject()
	// 		o.Set("name", name)
	// 		return o
	// 	}
	// 	return vm.NewArray(x("log"), x("etc"), x("test"), x("bin"))
	// })

	ret, err := vm.RunString(`
read().sort((a, b) => a.name.localeCompare(b.name)).map((x) => x.name);
`)
	if err != nil {
		panic(err)
	}
	fmt.Println(ret.Export())
	// outputs: [bin etc bin bin]
}

This is a very tricky one... With the current implementation (after #378) accessing an array or a struct element of a wrapped Go reflect value returns a reference to that element, not a copy. So when you do this:

a[0].name = '0';
a[1].name = '1';
let tmp = a[0];
a[0] = a[1];

tmp.name will be '1'., because assigning to a[0] changed the referenced value (which tmp also referring to).

Fixing this is non-trivial, because if a[0] returned a copy rather than a reference, a[0].name = '...' would not work as expected (it would create a copy of a[0], set its name and immediately lose the copy). Not to mention that any dereference (even just for reading a property) would result in a copy.

Same thing applies to nested structs, and considering anonymous fields are also nested, the effects would be even more bizarre.

It's easy to fix it so that it at least works for pointers (i.e. it would work for []*Thing), but with non-pointers the only thing I can think of is some kind of copy-on-write mechanism which would require to have a cache of already accessed array elements and struct fields and changing their underlying reflect value to a copy in case the element is overwritten.

If anyone has any suggestions, let me know.

Ok, I think this should sort it (and hopefully shouldn't break anything). Feedback is welcome as usual.