Skip to content

Complete support for slice-to-array conversion. #1057

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 5 commits into from
Sep 13, 2021
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
26 changes: 20 additions & 6 deletions compiler/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression {

switch t.Underlying().(type) {
case *types.Struct, *types.Array:
// JavaScript's pass-by-reference semantics makes passing array's or
// struct's object semantically equivalent to passing a pointer
// TODO(nevkontakte): Evaluate if performance gain justifies complexity
// introduced by the special case.
return fc.translateExpr(e.X)
}

Expand Down Expand Up @@ -446,11 +450,22 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression {

case *ast.IndexExpr:
switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) {
case *types.Array, *types.Pointer:
case *types.Pointer:
if _, ok := t.Elem().Underlying().(*types.Array); !ok {
// Should never happen in type-checked code.
panic(fmt.Errorf("non-array pointers can't be used with index expression"))
}
// Rewrite arrPtr[i] → (*arrPtr)[i] to concentrate array dereferencing
// logic in one place.
x := &ast.StarExpr{
Star: e.X.Pos(),
X: e.X,
}
astutil.SetType(fc.pkgCtx.Info.Info, t.Elem(), x)
e.X = x
return fc.translateExpr(e)
case *types.Array:
pattern := rangeCheck("%1e[%2f]", fc.pkgCtx.Types[e.Index].Value != nil, true)
if _, ok := t.(*types.Pointer); ok { // check pointer for nix (attribute getter causes a panic)
pattern = `(%1e.nilCheck, ` + pattern + `)`
}
return fc.formatExpr(pattern, e.X, e.Index)
case *types.Slice:
return fc.formatExpr(rangeCheck("%1e.$array[%1e.$offset + %2f]", fc.pkgCtx.Types[e.Index].Value != nil, false), e.X, e.Index)
Expand Down Expand Up @@ -819,6 +834,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression {

recv := fc.translateImplicitConversionWithCloning(x, methodsRecvType)
if isWrapped(recvType) {
// Wrap JS-native value to have access to the Go type's methods.
recv = fc.formatExpr("new %s(%s)", fc.typeName(methodsRecvType), recv)
}
return recv
Expand Down Expand Up @@ -1074,8 +1090,6 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type
switch ptrElType := t.Elem().Underlying().(type) {
case *types.Array: // (*[N]T)(expr) — converting expr to a pointer to an array.
if _, ok := exprType.Underlying().(*types.Slice); ok {
// GopherJS interprets pointer to an array as the array object itself
// due to its reference semantics, so the bellow coversion is correct.
return fc.formatExpr("$sliceToGoArray(%e, %s)", expr, fc.typeName(desiredType))
}
// TODO(nevkontakte): Is this just for aliased types (e.g. `type a [4]byte`)?
Expand Down
18 changes: 9 additions & 9 deletions compiler/gopherjspkg/fs_vfsdata.go

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions compiler/natives/fs_vfsdata.go

Large diffs are not rendered by default.

34 changes: 31 additions & 3 deletions compiler/natives/src/reflect/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -747,16 +747,22 @@ func cvtDirect(v Value, typ Type) Value {
slice.Set("$capacity", srcVal.Get("$capacity"))
val = js.Global.Call("$newDataPointer", slice, jsType(PtrTo(typ)))
case Ptr:
if typ.Elem().Kind() == Struct {
switch typ.Elem().Kind() {
case Struct:
if typ.Elem() == v.typ.Elem() {
val = srcVal
break
}
val = jsType(typ).New()
copyStruct(val, srcVal, typ.Elem())
break
case Array:
// Unlike other pointers, array pointers are "wrapped" types (see
// isWrapped() in the compiler package), and are represented by a native
// javascript array object here.
val = srcVal
default:
val = jsType(typ).New(srcVal.Get("$get"), srcVal.Get("$set"))
}
val = jsType(typ).New(srcVal.Get("$get"), srcVal.Get("$set"))
case Struct:
val = jsType(typ).Get("ptr").New()
copyStruct(val, srcVal, typ)
Expand Down Expand Up @@ -1264,6 +1270,28 @@ func getJsTag(tag string) string {
return ""
}

// CanConvert reports whether the value v can be converted to type t. If
// v.CanConvert(t) returns true then v.Convert(t) will not panic.
//
// TODO(nevkontakte): this overlay can be removed after
// https://github.com/golang/go/pull/48346 is in the lastest stable Go release.
func (v Value) CanConvert(t Type) bool {
vt := v.Type()
if !vt.ConvertibleTo(t) {
return false
}
// Currently the only conversion that is OK in terms of type
// but that can panic depending on the value is converting
// from slice to pointer-to-array.
if vt.Kind() == Slice && t.Kind() == Ptr && t.Elem().Kind() == Array {
n := t.Elem().Len()
if n > v.Len() { // Avoiding use of unsafeheader.Slice here.
return false
}
}
return true
}

func (v Value) Index(i int) Value {
switch k := v.kind(); k {
case Array:
Expand Down
9 changes: 8 additions & 1 deletion compiler/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor
case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map:
size = sizes32.Sizeof(t)
}
if tPointer, ok := o.Type().Underlying().(*types.Pointer); ok {
if _, ok := tPointer.Elem().Underlying().(*types.Array); ok {
// Array pointers have non-default constructors to support wrapping
// of the native objects.
constructor = "$arrayPtrCtor()"
}
}
funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, lhs, size, typeKind(o.Type()), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor)
})
d.MethodListCode = funcCtx.CatchOutput(0, func() {
Expand Down Expand Up @@ -754,7 +761,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt,
if recv != nil && !isBlank(recv) {
this := "this"
if isWrapped(c.pkgCtx.TypeOf(recv)) {
this = "this.$val"
this = "this.$val" // Unwrap receiver value.
}
c.Printf("%s = %s;", c.translateExpr(recv), this)
}
Expand Down
3 changes: 3 additions & 0 deletions compiler/prelude/prelude.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ var $sliceToNativeArray = function(slice) {
};

// Convert Go slice to a pointer to an underlying Go array.
//
// Note that an array pointer can be represented by an "unwrapped" native array
// type, and it will be wrapped back into its Go type when necessary.
var $sliceToGoArray = function(slice, arrayPtrType) {
var arrayType = arrayPtrType.elem;
if (arrayType !== undefined && slice.$length < arrayType.len) {
Expand Down
2 changes: 1 addition & 1 deletion compiler/prelude/prelude_min.go

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions compiler/prelude/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ var $idKey = function(x) {
return String(x.$id);
};

// Creates constructor functions for array pointer types. Returns a new function
// instace each time to make sure each type is independent of the other.
var $arrayPtrCtor = function() {
return function(array) {
this.$get = function() { return array; };
this.$set = function(v) { typ.copy(this, v); };
this.$val = array;
}
}

var $newType = function(size, kind, string, named, pkg, exported, constructor) {
var typ;
switch(kind) {
Expand Down Expand Up @@ -132,11 +142,7 @@ var $newType = function(size, kind, string, named, pkg, exported, constructor) {
case $kindArray:
typ = function(v) { this.$val = v; };
typ.wrapped = true;
typ.ptr = $newType(4, $kindPtr, "*" + string, false, "", false, function(array) {
this.$get = function() { return array; };
this.$set = function(v) { typ.copy(this, v); };
this.$val = array;
});
typ.ptr = $newType(4, $kindPtr, "*" + string, false, "", false, $arrayPtrCtor());
typ.init = function(elem, len) {
typ.elem = elem;
typ.len = len;
Expand Down
30 changes: 30 additions & 0 deletions compiler/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,36 @@ func isBlank(expr ast.Expr) bool {
return false
}

// isWrapped returns true for types that may need to be boxed to access full
// functionality of the Go type.
//
// For efficiency or interoperability reasons certain Go types can be represented
// by JavaScript values that weren't constructed by the corresponding Go type
// constructor.
//
// For example, consider a Go type:
//
// type SecretInt int
// func (_ SecretInt) String() string { return "<secret>" }
//
// func main() {
// var i SecretInt = 1
// println(i.String())
// }
//
// For this example the compiler will generate code similar to the snippet below:
//
// SecretInt = $pkg.SecretInt = $newType(4, $kindInt, "main.SecretInt", true, "main", true, null);
// SecretInt.prototype.String = function() {
// return "<secret>";
// };
// main = function() {
// var i = 1;
// console.log(new SecretInt(i).String());
// };
//
// Note that the generated code assigns a primitive "number" value into i, and
// only boxes it into an object when it's necessary to access its methods.
func isWrapped(ty types.Type) bool {
switch t := ty.Underlying().(type) {
case *types.Basic:
Expand Down
78 changes: 78 additions & 0 deletions tests/arrays_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package tests

import (
"reflect"
"testing"
)

func TestArrayPointer(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var p1 *[1]int
if p1 != nil {
t.Errorf("Zero-value array pointer is not equal to nil: %v", p1)
}

var p2 *[1]int = nil
if p2 != nil {
t.Errorf("Nil array pointer is not equal to nil: %v", p2)
}

p3 := func() *[1]int { return nil }()
if p3 != nil {
t.Errorf("Nil array pointer returned from function is not equal to nil: %v", p3)
}

if p1 != p3 || p1 != p2 || p2 != p3 {
t.Errorf("Nil pointers are not equal to each other: %v %v %v", p1, p2, p3)
}

if v := reflect.ValueOf(p1); !v.IsNil() {
t.Errorf("reflect.Value.IsNil() is false for a nil pointer: %v %v", p1, v)
}

type arr *[1]int
var p4 arr = nil

if v := reflect.ValueOf(p4); !v.IsNil() {
t.Errorf("reflect.Value.IsNil() is false for a nil pointer: %v %v", p4, v)
}
})

t.Run("pointer-dereference", func(t *testing.T) {
a1 := [1]int{42}
aPtr := &a1
a2 := *aPtr
if !reflect.DeepEqual(a1, a2) {
t.Errorf("Array after pointer dereferencing is not equal to the original: %v != %v", a1, a2)
t.Logf("Pointer: %v", aPtr)
}
})

t.Run("interface-and-back", func(t *testing.T) {
type arr *[1]int
tests := []struct {
name string
a arr
}{{
name: "not nil",
a: &[1]int{42},
}, {
name: "nil",
a: nil,
}}
for _, test := range tests {
a1 := test.a
i := interface{}(a1)
a2 := i.(arr)

if a1 != a2 {
t.Errorf("Array pointer is not equal to itself after interface conversion: %v != %v", a1, a2)
println(a1, a2)
}
}
})

t.Run("reflect.IsNil", func(t *testing.T) {

})
}