Ver código fonte

Improved export of Map, Set and iterable objects. Closes #368.

Dmitry Panov 3 anos atrás
pai
commit
f3aaa50fcb
15 arquivos alterados com 657 adições e 87 exclusões
  1. 29 2
      array.go
  2. 30 2
      array_sparse.go
  3. 32 0
      array_sparse_test.go
  4. 56 0
      builtin_map.go
  5. 48 0
      builtin_map_test.go
  6. 63 0
      builtin_set.go
  7. 61 1
      builtin_set_test.go
  8. 8 0
      destruct.go
  9. 107 12
      object.go
  10. 2 2
      object_args.go
  11. 8 0
      object_dynamic.go
  12. 12 0
      object_lazy.go
  13. 93 0
      object_test.go
  14. 71 66
      runtime.go
  15. 37 2
      value.go

+ 29 - 2
array.go

@@ -1,6 +1,7 @@
 package goja
 
 import (
+	"fmt"
 	"math"
 	"math/bits"
 	"reflect"
@@ -480,11 +481,11 @@ func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool {
 }
 
 func (a *arrayObject) export(ctx *objectExportCtx) interface{} {
-	if v, exists := ctx.get(a); exists {
+	if v, exists := ctx.get(a.val); exists {
 		return v
 	}
 	arr := make([]interface{}, a.length)
-	ctx.put(a, arr)
+	ctx.put(a.val, arr)
 	if a.propValueCount == 0 && a.length == uint32(len(a.values)) && uint32(a.objCount) == a.length {
 		for i, v := range a.values {
 			if v != nil {
@@ -506,6 +507,32 @@ func (a *arrayObject) exportType() reflect.Type {
 	return reflectTypeArray
 }
 
+func (a *arrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	r := a.val.runtime
+	if iter := a.getSym(SymIterator, nil); iter == r.global.arrayValues || iter == nil {
+		l := toIntStrict(int64(a.length))
+		if dst.IsNil() || dst.Len() != l {
+			dst.Set(reflect.MakeSlice(typ, l, l))
+		}
+		ctx.putTyped(a.val, typ, dst.Interface())
+		for i := 0; i < l; i++ {
+			if i >= len(a.values) {
+				break
+			}
+			val := a.values[i]
+			if p, ok := val.(*valueProperty); ok {
+				val = p.get(a.val)
+			}
+			err := r.toReflectValue(val, dst.Index(i), ctx)
+			if err != nil {
+				return fmt.Errorf("could not convert array element %v to %v at %d: %w", val, typ, i, err)
+			}
+		}
+		return nil
+	}
+	return a.baseObject.exportToSlice(dst, typ, ctx)
+}
+
 func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem, newMaxIdx int) {
 	a.values = make([]Value, newMaxIdx+1)
 	for _, item := range items {

+ 30 - 2
array_sparse.go

@@ -1,6 +1,7 @@
 package goja
 
 import (
+	"fmt"
 	"math"
 	"math/bits"
 	"reflect"
@@ -420,11 +421,11 @@ func (a *sparseArrayObject) sortLen() int64 {
 }
 
 func (a *sparseArrayObject) export(ctx *objectExportCtx) interface{} {
-	if v, exists := ctx.get(a); exists {
+	if v, exists := ctx.get(a.val); exists {
 		return v
 	}
 	arr := make([]interface{}, a.length)
-	ctx.put(a, arr)
+	ctx.put(a.val, arr)
 	var prevIdx uint32
 	for _, item := range a.items {
 		idx := item.idx
@@ -457,3 +458,30 @@ func (a *sparseArrayObject) export(ctx *objectExportCtx) interface{} {
 func (a *sparseArrayObject) exportType() reflect.Type {
 	return reflectTypeArray
 }
+
+func (a *sparseArrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	r := a.val.runtime
+	if iter := a.getSym(SymIterator, nil); iter == r.global.arrayValues || iter == nil {
+		l := toIntStrict(int64(a.length))
+		if dst.IsNil() || dst.Len() != l {
+			dst.Set(reflect.MakeSlice(typ, l, l))
+		}
+		ctx.putTyped(a.val, typ, dst.Interface())
+		for _, item := range a.items {
+			val := item.value
+			if p, ok := val.(*valueProperty); ok {
+				val = p.get(a.val)
+			}
+			idx := toIntStrict(int64(item.idx))
+			if idx >= l {
+				break
+			}
+			err := r.toReflectValue(val, dst.Index(idx), ctx)
+			if err != nil {
+				return fmt.Errorf("could not convert array element %v to %v at %d: %w", item.value, typ, idx, err)
+			}
+		}
+		return nil
+	}
+	return a.baseObject.exportToSlice(dst, typ, ctx)
+}

+ 32 - 0
array_sparse_test.go

@@ -230,3 +230,35 @@ func TestArraySparseExportProps(t *testing.T) {
 		t.Fatalf("Invalid export type: %T", actual)
 	}
 }
+
+func TestSparseArrayExportToSlice(t *testing.T) {
+	vm := New()
+	arr := vm.NewArray()
+	err := arr.Set("20470", 120470)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = arr.DefineDataProperty("20471", vm.ToValue(220471), FLAG_TRUE, FLAG_FALSE, FLAG_TRUE)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var exp []int
+	err = vm.ExportTo(arr, &exp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(exp) != 20472 {
+		t.Fatalf("len: %d", len(exp))
+	}
+	if e := exp[20470]; e != 120470 {
+		t.Fatalf("20470: %d", e)
+	}
+	if e := exp[20471]; e != 220471 {
+		t.Fatalf("20471: %d", e)
+	}
+	for i := 0; i < 20470; i++ {
+		if exp[i] != 0 {
+			t.Fatalf("at %d: %d", i, exp[i])
+		}
+	}
+}

+ 56 - 0
builtin_map.go

@@ -1,5 +1,11 @@
 package goja
 
+import (
+	"reflect"
+)
+
+var mapExportType = reflect.TypeOf([][2]interface{}{})
+
 type mapObject struct {
 	baseObject
 	m *orderedMap
@@ -40,6 +46,56 @@ func (mo *mapObject) init() {
 	mo.m = newOrderedMap(mo.val.runtime.getHash())
 }
 
+func (mo *mapObject) exportType() reflect.Type {
+	return mapExportType
+}
+
+func (mo *mapObject) export(ctx *objectExportCtx) interface{} {
+	m := make([][2]interface{}, mo.m.size)
+	ctx.put(mo.val, m)
+
+	iter := mo.m.newIter()
+	for i := 0; i < len(m); i++ {
+		entry := iter.next()
+		if entry == nil {
+			break
+		}
+		m[i][0] = exportValue(entry.key, ctx)
+		m[i][1] = exportValue(entry.value, ctx)
+	}
+
+	return m
+}
+
+func (mo *mapObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	if dst.IsNil() {
+		dst.Set(reflect.MakeMap(typ))
+	}
+	ctx.putTyped(mo.val, typ, dst.Interface())
+	keyTyp := typ.Key()
+	elemTyp := typ.Elem()
+	iter := mo.m.newIter()
+	r := mo.val.runtime
+	for {
+		entry := iter.next()
+		if entry == nil {
+			break
+		}
+		keyVal := reflect.New(keyTyp).Elem()
+		err := r.toReflectValue(entry.key, keyVal, ctx)
+		if err != nil {
+			return err
+		}
+		elemVal := reflect.New(elemTyp).Elem()
+		err = r.toReflectValue(entry.value, elemVal, ctx)
+		if err != nil {
+			return err
+		}
+		dst.SetMapIndex(keyVal, elemVal)
+	}
+	return nil
+}
+
 func (r *Runtime) mapProto_clear(call FunctionCall) Value {
 	thisObj := r.toObject(call.This)
 	mo, ok := thisObj.self.(*mapObject)

+ 48 - 0
builtin_map_test.go

@@ -1,6 +1,7 @@
 package goja
 
 import (
+	"fmt"
 	"hash/maphash"
 	"testing"
 )
@@ -61,6 +62,53 @@ func TestMapEvilIterator(t *testing.T) {
 	testScriptWithTestLib(SCRIPT, _undefined, t)
 }
 
+func ExampleObject_Export_map() {
+	vm := New()
+	m, err := vm.RunString(`
+	new Map([[1, true], [2, false]]);
+	`)
+	if err != nil {
+		panic(err)
+	}
+	exp := m.Export()
+	fmt.Printf("%T, %v\n", exp, exp)
+	// Output: [][2]interface {}, [[1 true] [2 false]]
+}
+
+func ExampleRuntime_ExportTo_mapToMap() {
+	vm := New()
+	m, err := vm.RunString(`
+	new Map([[1, true], [2, false]]);
+	`)
+	if err != nil {
+		panic(err)
+	}
+	exp := make(map[int]bool)
+	err = vm.ExportTo(m, &exp)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(exp)
+	// Output: map[1:true 2:false]
+}
+
+func ExampleRuntime_ExportTo_mapToSlice() {
+	vm := New()
+	m, err := vm.RunString(`
+	new Map([[1, true], [2, false]]);
+	`)
+	if err != nil {
+		panic(err)
+	}
+	exp := make([][]interface{}, 0)
+	err = vm.ExportTo(m, &exp)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(exp)
+	// Output: [[1 true] [2 false]]
+}
+
 func BenchmarkMapDelete(b *testing.B) {
 	var key1 Value = asciiString("a")
 	var key2 Value = asciiString("b")

+ 63 - 0
builtin_set.go

@@ -1,5 +1,9 @@
 package goja
 
+import "reflect"
+
+var setExportType = reflectTypeArray
+
 type setObject struct {
 	baseObject
 	m *orderedMap
@@ -38,6 +42,65 @@ func (so *setObject) init() {
 	so.m = newOrderedMap(so.val.runtime.getHash())
 }
 
+func (so *setObject) exportType() reflect.Type {
+	return setExportType
+}
+
+func (so *setObject) export(ctx *objectExportCtx) interface{} {
+	a := make([]interface{}, so.m.size)
+	ctx.put(so.val, a)
+	iter := so.m.newIter()
+	for i := 0; i < len(a); i++ {
+		entry := iter.next()
+		if entry == nil {
+			break
+		}
+		a[i] = exportValue(entry.key, ctx)
+	}
+	return a
+}
+
+func (so *setObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	l := so.m.size
+	if dst.IsNil() || dst.Len() != l {
+		dst.Set(reflect.MakeSlice(typ, l, l))
+	}
+	ctx.putTyped(so.val, typ, dst.Interface())
+	iter := so.m.newIter()
+	r := so.val.runtime
+	for i := 0; i < l; i++ {
+		entry := iter.next()
+		if entry == nil {
+			break
+		}
+		err := r.toReflectValue(entry.key, dst.Index(i), ctx)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (so *setObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	keyTyp := typ.Key()
+	elemTyp := typ.Elem()
+	iter := so.m.newIter()
+	r := so.val.runtime
+	for {
+		entry := iter.next()
+		if entry == nil {
+			break
+		}
+		keyVal := reflect.New(keyTyp).Elem()
+		err := r.toReflectValue(entry.key, keyVal, ctx)
+		if err != nil {
+			return err
+		}
+		dst.SetMapIndex(keyVal, reflect.Zero(elemTyp))
+	}
+	return nil
+}
+
 func (r *Runtime) setProto_add(call FunctionCall) Value {
 	thisObj := r.toObject(call.This)
 	so, ok := thisObj.self.(*setObject)

+ 61 - 1
builtin_set_test.go

@@ -1,6 +1,9 @@
 package goja
 
-import "testing"
+import (
+	"fmt"
+	"testing"
+)
 
 func TestSetEvilIterator(t *testing.T) {
 	const SCRIPT = `
@@ -21,3 +24,60 @@ func TestSetEvilIterator(t *testing.T) {
 	`
 	testScript(SCRIPT, _undefined, t)
 }
+
+func ExampleRuntime_ExportTo_setToMap() {
+	vm := New()
+	s, err := vm.RunString(`
+	new Set([1, 2, 3])
+	`)
+	if err != nil {
+		panic(err)
+	}
+	m := make(map[int]struct{})
+	err = vm.ExportTo(s, &m)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(m)
+	// Output: map[1:{} 2:{} 3:{}]
+}
+
+func ExampleRuntime_ExportTo_setToSlice() {
+	vm := New()
+	s, err := vm.RunString(`
+	new Set([1, 2, 3])
+	`)
+	if err != nil {
+		panic(err)
+	}
+	var a []int
+	err = vm.ExportTo(s, &a)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(a)
+	// Output: [1 2 3]
+}
+
+func TestSetExportToSliceCircular(t *testing.T) {
+	vm := New()
+	s, err := vm.RunString(`
+	let s = new Set();
+	s.add(s);
+	s;
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var a []Value
+	err = vm.ExportTo(s, &a)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(a) != 1 {
+		t.Fatalf("len: %d", len(a))
+	}
+	if a[0] != s {
+		t.Fatalf("a: %v", a)
+	}
+}

+ 8 - 0
destruct.go

@@ -241,6 +241,14 @@ func (d *destructKeyedSource) exportType() reflect.Type {
 	return d.w().exportType()
 }
 
+func (d *destructKeyedSource) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return d.w().exportToMap(dst, typ, ctx)
+}
+
+func (d *destructKeyedSource) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return d.w().exportToSlice(dst, typ, ctx)
+}
+
 func (d *destructKeyedSource) equal(impl objectImpl) bool {
 	return d.w().equal(impl)
 }

+ 107 - 12
object.go

@@ -142,7 +142,7 @@ func (p *PropertyDescriptor) complete() {
 type objectExportCacheItem map[reflect.Type]interface{}
 
 type objectExportCtx struct {
-	cache map[objectImpl]interface{}
+	cache map[*Object]interface{}
 }
 
 type objectImpl interface {
@@ -193,6 +193,8 @@ type objectImpl interface {
 
 	export(ctx *objectExportCtx) interface{}
 	exportType() reflect.Type
+	exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error
+	exportToSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error
 	equal(objectImpl) bool
 
 	iterateStringKeys() iterNextFunc
@@ -943,12 +945,12 @@ func (o *baseObject) swap(i, j int64) {
 }
 
 func (o *baseObject) export(ctx *objectExportCtx) interface{} {
-	if v, exists := ctx.get(o); exists {
+	if v, exists := ctx.get(o.val); exists {
 		return v
 	}
 	keys := o.stringKeys(false, nil)
 	m := make(map[string]interface{}, len(keys))
-	ctx.put(o, m)
+	ctx.put(o.val, m)
 	for _, itemName := range keys {
 		itemNameStr := itemName.String()
 		v := o.val.self.getStr(itemName.string(), nil)
@@ -966,6 +968,99 @@ func (o *baseObject) exportType() reflect.Type {
 	return reflectTypeMap
 }
 
+func genericExportToMap(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	if dst.IsNil() {
+		dst.Set(reflect.MakeMap(typ))
+	}
+	ctx.putTyped(o, typ, dst.Interface())
+	keyTyp := typ.Key()
+	elemTyp := typ.Elem()
+	needConvertKeys := !reflectTypeString.AssignableTo(keyTyp)
+	iter := &enumerableIter{
+		o:       o,
+		wrapped: o.self.iterateStringKeys(),
+	}
+	r := o.runtime
+	for item, next := iter.next(); next != nil; item, next = next() {
+		var kv reflect.Value
+		var err error
+		if needConvertKeys {
+			kv = reflect.New(keyTyp).Elem()
+			err = r.toReflectValue(item.name, kv, ctx)
+			if err != nil {
+				return fmt.Errorf("could not convert map key %s to %v: %w", item.name.String(), typ, err)
+			}
+		} else {
+			kv = reflect.ValueOf(item.name.String())
+		}
+
+		ival := o.self.getStr(item.name.string(), nil)
+		if ival != nil {
+			vv := reflect.New(elemTyp).Elem()
+			err = r.toReflectValue(ival, vv, ctx)
+			if err != nil {
+				return fmt.Errorf("could not convert map value %v to %v at key %s: %w", ival, typ, item.name.String(), err)
+			}
+			dst.SetMapIndex(kv, vv)
+		} else {
+			dst.SetMapIndex(kv, reflect.Zero(elemTyp))
+		}
+	}
+
+	return nil
+}
+
+func (o *baseObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return genericExportToMap(o.val, m, typ, ctx)
+}
+
+func genericExportToSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) (err error) {
+	r := o.runtime
+
+	if method := toMethod(r.getV(o, SymIterator)); method != nil {
+		// iterable
+
+		var values []Value
+		// cannot change (append to) the slice once it's been put into the cache, so we need to know its length beforehand
+		ex := r.try(func() {
+			values = r.iterableToList(o, method)
+		})
+		if ex != nil {
+			return ex
+		}
+		if dst.IsNil() || dst.Len() != len(values) {
+			dst.Set(reflect.MakeSlice(typ, len(values), len(values)))
+		}
+		ctx.putTyped(o, typ, dst.Interface())
+		for i, val := range values {
+			err = r.toReflectValue(val, dst.Index(i), ctx)
+			if err != nil {
+				return
+			}
+		}
+	} else {
+		// array-like
+		l := toIntStrict(toLength(o.self.getStr("length", nil)))
+		if dst.IsNil() || dst.Len() != l {
+			dst.Set(reflect.MakeSlice(typ, l, l))
+		}
+		ctx.putTyped(o, typ, dst.Interface())
+		for i := 0; i < l; i++ {
+			val := nilSafe(o.self.getIdx(valueInt(i), nil))
+			err = r.toReflectValue(val, dst.Index(i), ctx)
+			if err != nil {
+				return
+			}
+		}
+	}
+
+	return
+}
+
+func (o *baseObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return genericExportToSlice(o.val, dst, typ, ctx)
+}
+
 type enumerableFlag int
 
 const (
@@ -1561,10 +1656,10 @@ func (o *guardedObject) deleteStr(name unistring.String, throw bool) bool {
 	return res
 }
 
-func (ctx *objectExportCtx) get(key objectImpl) (interface{}, bool) {
+func (ctx *objectExportCtx) get(key *Object) (interface{}, bool) {
 	if v, exists := ctx.cache[key]; exists {
 		if item, ok := v.(objectExportCacheItem); ok {
-			r, exists := item[key.exportType()]
+			r, exists := item[key.self.exportType()]
 			return r, exists
 		} else {
 			return v, true
@@ -1573,7 +1668,7 @@ func (ctx *objectExportCtx) get(key objectImpl) (interface{}, bool) {
 	return nil, false
 }
 
-func (ctx *objectExportCtx) getTyped(key objectImpl, typ reflect.Type) (interface{}, bool) {
+func (ctx *objectExportCtx) getTyped(key *Object, typ reflect.Type) (interface{}, bool) {
 	if v, exists := ctx.cache[key]; exists {
 		if item, ok := v.(objectExportCacheItem); ok {
 			r, exists := item[typ]
@@ -1587,20 +1682,20 @@ func (ctx *objectExportCtx) getTyped(key objectImpl, typ reflect.Type) (interfac
 	return nil, false
 }
 
-func (ctx *objectExportCtx) put(key objectImpl, value interface{}) {
+func (ctx *objectExportCtx) put(key *Object, value interface{}) {
 	if ctx.cache == nil {
-		ctx.cache = make(map[objectImpl]interface{})
+		ctx.cache = make(map[*Object]interface{})
 	}
 	if item, ok := ctx.cache[key].(objectExportCacheItem); ok {
-		item[key.exportType()] = value
+		item[key.self.exportType()] = value
 	} else {
 		ctx.cache[key] = value
 	}
 }
 
-func (ctx *objectExportCtx) putTyped(key objectImpl, typ reflect.Type, value interface{}) {
+func (ctx *objectExportCtx) putTyped(key *Object, typ reflect.Type, value interface{}) {
 	if ctx.cache == nil {
-		ctx.cache = make(map[objectImpl]interface{})
+		ctx.cache = make(map[*Object]interface{})
 	}
 	v, exists := ctx.cache[key]
 	if exists {
@@ -1608,7 +1703,7 @@ func (ctx *objectExportCtx) putTyped(key objectImpl, typ reflect.Type, value int
 			item[typ] = value
 		} else {
 			m := make(objectExportCacheItem, 2)
-			m[key.exportType()] = v
+			m[key.self.exportType()] = v
 			m[typ] = value
 			ctx.cache[key] = m
 		}

+ 2 - 2
object_args.go

@@ -124,11 +124,11 @@ func (a *argumentsObject) defineOwnPropertyStr(name unistring.String, descr Prop
 }
 
 func (a *argumentsObject) export(ctx *objectExportCtx) interface{} {
-	if v, exists := ctx.get(a); exists {
+	if v, exists := ctx.get(a.val); exists {
 		return v
 	}
 	arr := make([]interface{}, a.length)
-	ctx.put(a, arr)
+	ctx.put(a.val, arr)
 	for i := range arr {
 		v := a.getIdx(valueInt(int64(i)), nil)
 		if v != nil {

+ 8 - 0
object_dynamic.go

@@ -483,6 +483,14 @@ func (o *dynamicObject) exportType() reflect.Type {
 	return reflect.TypeOf(o.d)
 }
 
+func (o *baseDynamicObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return genericExportToMap(o.val, dst, typ, ctx)
+}
+
+func (o *baseDynamicObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	return genericExportToSlice(o.val, dst, typ, ctx)
+}
+
 func (o *dynamicObject) equal(impl objectImpl) bool {
 	if other, ok := impl.(*dynamicObject); ok {
 		return o.d == other.d

+ 12 - 0
object_lazy.go

@@ -253,6 +253,18 @@ func (o *lazyObject) exportType() reflect.Type {
 	return obj.exportType()
 }
 
+func (o *lazyObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	obj := o.create(o.val)
+	o.val.self = obj
+	return obj.exportToMap(m, typ, ctx)
+}
+
+func (o *lazyObject) exportToSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error {
+	obj := o.create(o.val)
+	o.val.self = obj
+	return obj.exportToSlice(s, typ, ctx)
+}
+
 func (o *lazyObject) equal(other objectImpl) bool {
 	obj := o.create(o.val)
 	o.val.self = obj

+ 93 - 0
object_test.go

@@ -297,6 +297,99 @@ func TestExportToWrappedMapCustom(t *testing.T) {
 	}
 }
 
+func TestExportToSliceNonIterable(t *testing.T) {
+	vm := New()
+	o := vm.NewObject()
+	var a []interface{}
+	err := vm.ExportTo(o, &a)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(a) != 0 {
+		t.Fatalf("a: %v", a)
+	}
+}
+
+func ExampleRuntime_ExportTo_iterableToSlice() {
+	vm := New()
+	v, err := vm.RunString(`
+	function reverseIterator() {
+	    const arr = this;
+	    let idx = arr.length;
+	    return {
+			next: () => idx > 0 ? {value: arr[--idx]} : {done: true}
+	    }
+	}
+	const arr = [1,2,3];
+	arr[Symbol.iterator] = reverseIterator;
+	arr;
+	`)
+	if err != nil {
+		panic(err)
+	}
+
+	var arr []int
+	err = vm.ExportTo(v, &arr)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(arr)
+	// Output: [3 2 1]
+}
+
+func TestRuntime_ExportTo_proxiedIterableToSlice(t *testing.T) {
+	vm := New()
+	v, err := vm.RunString(`
+	function reverseIterator() {
+	    const arr = this;
+	    let idx = arr.length;
+	    return {
+			next: () => idx > 0 ? {value: arr[--idx]} : {done: true}
+	    }
+	}
+	const arr = [1,2,3];
+	arr[Symbol.iterator] = reverseIterator;
+	new Proxy(arr, {});
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var arr []int
+	err = vm.ExportTo(v, &arr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if out := fmt.Sprint(arr); out != "[3 2 1]" {
+		t.Fatal(out)
+	}
+}
+
+func ExampleRuntime_ExportTo_arrayLikeToSlice() {
+	vm := New()
+	v, err := vm.RunString(`
+	({
+		length: 3,
+		0: 1,
+		1: 2,
+		2: 3
+	});
+	`)
+	if err != nil {
+		panic(err)
+	}
+
+	var arr []int
+	err = vm.ExportTo(v, &arr)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(arr)
+	// Output: [1 2 3]
+}
+
 func TestSetForeignReturnValue(t *testing.T) {
 	const SCRIPT = `
 	var array = [1, 2, 3];

+ 71 - 66
runtime.go

@@ -2014,82 +2014,29 @@ func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCt
 		return nil
 	case reflect.Slice:
 		if o, ok := v.(*Object); ok {
-			if o.self.className() == classArray {
-				if v, exists := ctx.getTyped(o.self, typ); exists {
-					dst.Set(reflect.ValueOf(v))
-					return nil
-				}
-				l := int(toLength(o.self.getStr("length", nil)))
-				if dst.IsNil() || dst.Len() != l {
-					dst.Set(reflect.MakeSlice(typ, l, l))
-				}
-				s := dst
-				ctx.putTyped(o.self, typ, s.Interface())
-				for i := 0; i < l; i++ {
-					item := o.self.getIdx(valueInt(int64(i)), nil)
-					err := r.toReflectValue(item, s.Index(i), ctx)
-					if err != nil {
-						return fmt.Errorf("could not convert array element %v to %v at %d: %w", v, typ, i, err)
-					}
-				}
+			if v, exists := ctx.getTyped(o, typ); exists {
+				dst.Set(reflect.ValueOf(v))
 				return nil
 			}
+			return o.self.exportToSlice(dst, typ, ctx)
 		}
 	case reflect.Map:
 		if o, ok := v.(*Object); ok {
-			if v, exists := ctx.getTyped(o.self, typ); exists {
+			if v, exists := ctx.getTyped(o, typ); exists {
 				dst.Set(reflect.ValueOf(v))
 				return nil
 			}
-			if dst.IsNil() {
-				dst.Set(reflect.MakeMap(typ))
-			}
-			m := dst
-			ctx.putTyped(o.self, typ, m.Interface())
-			keyTyp := typ.Key()
-			elemTyp := typ.Elem()
-			needConvertKeys := !reflect.ValueOf("").Type().AssignableTo(keyTyp)
-			iter := &enumerableIter{
-				o:       o,
-				wrapped: o.self.iterateStringKeys(),
-			}
-			for item, next := iter.next(); next != nil; item, next = next() {
-				var kv reflect.Value
-				var err error
-				if needConvertKeys {
-					kv = reflect.New(keyTyp).Elem()
-					err = r.toReflectValue(item.name, kv, ctx)
-					if err != nil {
-						return fmt.Errorf("could not convert map key %s to %v", item.name.String(), typ)
-					}
-				} else {
-					kv = reflect.ValueOf(item.name.String())
-				}
-
-				ival := o.self.getStr(item.name.string(), nil)
-				if ival != nil {
-					vv := reflect.New(elemTyp).Elem()
-					err := r.toReflectValue(ival, vv, ctx)
-					if err != nil {
-						return fmt.Errorf("could not convert map value %v to %v at key %s", ival, typ, item.name.String())
-					}
-					m.SetMapIndex(kv, vv)
-				} else {
-					m.SetMapIndex(kv, reflect.Zero(elemTyp))
-				}
-			}
-
-			return nil
+			return o.self.exportToMap(dst, typ, ctx)
 		}
 	case reflect.Struct:
 		if o, ok := v.(*Object); ok {
 			t := reflect.PtrTo(typ)
-			if v, exists := ctx.getTyped(o.self, t); exists {
+			if v, exists := ctx.getTyped(o, t); exists {
 				dst.Set(reflect.ValueOf(v).Elem())
 				return nil
 			}
 			s := dst
-			ctx.putTyped(o.self, t, s.Addr().Interface())
+			ctx.putTyped(o, t, s.Addr().Interface())
 			for i := 0; i < typ.NumField(); i++ {
 				field := typ.Field(i)
 				if ast.IsExported(field.Name) {
@@ -2121,7 +2068,7 @@ func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCt
 		}
 	case reflect.Ptr:
 		if o, ok := v.(*Object); ok {
-			if v, exists := ctx.getTyped(o.self, typ); exists {
+			if v, exists := ctx.getTyped(o, typ); exists {
 				dst.Set(reflect.ValueOf(v))
 				return nil
 			}
@@ -2173,10 +2120,68 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.
 }
 
 // ExportTo converts a JavaScript value into the specified Go value. The second parameter must be a non-nil pointer.
-// Exporting to an interface{} results in a value of the same type as Export() would produce.
-// Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning
-// values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32
 // Returns error if conversion is not possible.
+//
+// Notes on specific cases:
+//
+// Empty interface
+//
+// Exporting to an interface{} results in a value of the same type as Value.Export() would produce.
+//
+// Numeric types
+//
+// Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning
+// values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32.
+//
+// Functions
+//
+// Exporting to a 'func' creates a strictly typed 'gateway' into an ES function which can be called from Go.
+// The arguments are converted into ES values using Runtime.ToValue(). If the func has no return values,
+// the return value is ignored. If the func has exactly one return value, it is converted to the appropriate
+// type using ExportTo(). If the func has exactly 2 return values and the second value is 'error', exceptions
+// are caught and returned as *Exception. In all other cases exceptions result in a panic. Any extra return values
+// are zeroed.
+//
+// Note, if you want to catch and return exceptions as an `error` and you don't need the return value,
+// 'func(...) error' will not work as expected. The 'error' in this case is mapped to the function return value, not
+// the exception which will still result in a panic. Use 'func(...) (Value, error)' instead, and ignore the Value.
+//
+// 'this' value will always be set to 'undefined'.
+//
+// For a more low-level mechanism see AssertFunction().
+//
+// Map types
+//
+// An ES Map can be exported into a Go map type. If any exported key value is non-hashable, the operation panics
+// (as reflect.Value.SetMapIndex() would). Symbol.iterator is ignored.
+//
+// Exporting an ES Set into a map type results in the map being populated with (element) -> (zero value) key/value
+// pairs. If any value is non-hashable, the operation panics (as reflect.Value.SetMapIndex() would).
+// Symbol.iterator is ignored.
+//
+// Any other Object populates the map with own enumerable non-symbol properties.
+//
+// Slice types
+//
+// Exporting an ES Set into a slice type results in its elements being exported.
+//
+// Exporting any Object that implements the iterable protocol (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol)
+// into a slice type results in the slice being populated with the results of the iteration.
+//
+// Array is treated as iterable (i.e. overwriting Symbol.iterator affects the result).
+//
+// If an object has a 'length' property it is treated as array-like. The resulting slice will contain
+// obj[0], ... obj[length-1].
+//
+// Any other Object is treated as an array-like object with zero length and results in an empty slice.
+//
+// Proxy
+//
+// Proxy objects are treated the same way as if they were accessed from ES code in regard to their properties
+// (such as 'length' or [Symbol.iterator]). This means exporting them to slice types works, however
+// exporting a proxied Map into a map type does not produce its contents, because the Proxy is not recognised
+// as a Map. Same applies to a proxied Set.
+//
 func (r *Runtime) ExportTo(v Value, target interface{}) error {
 	tval := reflect.ValueOf(target)
 	if tval.Kind() != reflect.Ptr || tval.IsNil() {
@@ -2689,8 +2694,8 @@ func (r *Runtime) invoke(v Value, p unistring.String, args ...Value) Value {
 	return r.toCallable(o.self.getStr(p, nil))(FunctionCall{This: v, Arguments: args})
 }
 
-func (r *Runtime) iterableToList(items Value, method func(FunctionCall) Value) []Value {
-	iter := r.getIterator(items, method)
+func (r *Runtime) iterableToList(iterable Value, method func(FunctionCall) Value) []Value {
+	iter := r.getIterator(iterable, method)
 	var values []Value
 	iter.iterate(func(item Value) {
 		values = append(values, item)

+ 37 - 2
value.go

@@ -53,6 +53,21 @@ var (
 
 var intCache [256]Value
 
+// Value represents an ECMAScript value.
+//
+// Export returns a "plain" Go value which type depends on the type of the Value.
+//
+// For integer numbers it's int64.
+//
+// For any other numbers (including Infinities, NaN and negative zero) it's float64.
+//
+// For string it's a string. Note that unicode strings are converted into UTF-8 with invalid code points replaced with utf8.RuneError.
+//
+// For boolean it's bool.
+//
+// For null and undefined it's nil.
+//
+// For Object it depends on the Object type, see Object.Export() for more details.
 type Value interface {
 	ToInteger() int64
 	toString() valueString
@@ -731,8 +746,27 @@ func (o *Object) baseObject(*Runtime) *Object {
 	return o
 }
 
-// Export the Object to a plain Go type. The returned value will be map[string]interface{} unless
-// the Object is a wrapped Go value (created using ToValue()).
+// Export the Object to a plain Go type.
+// If the Object is a wrapped Go value (created using ToValue()) returns the original value.
+//
+// If the Object is a function, returns func(FunctionCall) Value. Note that exceptions thrown inside the function
+// result in panics, which can also leave the Runtime in an unusable state. Therefore, these values should only
+// be used inside another ES function implemented in Go. For calling a function from Go, use AssertFunction() or
+// Runtime.ExportTo() as described in the README.
+//
+// For a Map, returns the list of entries as [][2]interface{}.
+//
+// For a Set, returns the list of elements as []interface{}.
+//
+// For a Proxy, returns Proxy.
+//
+// For a Promise, returns Promise.
+//
+// For a DynamicObject or a DynamicArray, returns the underlying handler.
+//
+// For an array, returns its items as []interface{}.
+//
+// In all other cases returns own enumerable non-symbol properties as map[string]interface{}.
 // This method will panic with an *Exception if a JavaScript exception is thrown in the process.
 func (o *Object) Export() (ret interface{}) {
 	o.runtime.tryPanic(func() {
@@ -742,6 +776,7 @@ func (o *Object) Export() (ret interface{}) {
 	return
 }
 
+// ExportType returns the type of the value that is returned by Export().
 func (o *Object) ExportType() reflect.Type {
 	return o.self.exportType()
 }