You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Disclaimer: this is a very rough proposal not immediately planned for implementation. It's been bouncing around my head for a while, though, so I wanted to put it up for some discussion. In case it has some fatal flaws, it would be nice to discover them before I put any significant effort into prototyping.
High-level idea
Go types that with identical underlying types must have identical in-memory representation in GopherJS.
For example, objects backing s and q must be deeply equal from JS perspective:
Today, all GopherJS-created values (with the exception of wrapped types) carry an association with the Go type metadata. While this seems nice on paper, it leads to complications and edge cases, for example:
The need for distinction of wrapped and non-wrapped types for performance reasons, which introduces lots of special cases in the compiler, prelude and standard library. In particular, a lot of mental effort is required to track whether a wrapped type value is supposed to be wrapped or bare at any given time.
My best guess is that the current implementation was chosen so that calling methods on structs like s.Foo() is expressed as calling object's method in JS. This is nice and idiomatic from the JS perspective, but begins to break down when we need to call methods on non-struct types.
Neither is really a problem in the vanilla Go: any value is represented as a chunk of memory and it's totally up to the compiler how to interpret the value. Using the example above, conversion from *S to *Q is a no-op at runtime: it only changes which operations the compiler would permit against it, but no reshuffling of in-memory data is required.
While this requires a lot more detailed design, here are some key points:
Value initialization: values are initialized with a literal according to the underlying type, so something like s := S{i: 42} will compile to let s = {i: 42}, as opposed to today's let s = new S(42).
Calling methods: whenever static dispatch is possible (i.e. the compiler knows the concrete type), the method is called from the type object, for example: s.Foo() compiles into S.Foo(s). That is, the methods are not attached to the value instance, but to the type. Note that this conceptually matches the general Go's implementation of methods, where a method call is a syntax sugar for the Type.MethodName(receiver, arg1, argv2) syntax. There are two cases where dynamic dispatch is necessary: interfaces and reflection.
Interfaces at call site will look similar to static dispatch, except instead of using a concrete type S to access the method we'll use a dynamic reference to the type stored inside the interface value. For example for type I interface { Foo() } the call site will look like i.typ.Foo(i.val). This is conceptually closer (although not identical) to how interfaces are implemented in vanilla Go.
Reflection will work pretty much the same: reflect.Value will store a reference to the concrete type (which can be extracted from the interface value) it will dispatch calls to it.
Interoperation with JavaScript: for the most part, it should remain unchanged. Although the current semantics is not very well defined, I think by default when we externalize objects we perform a deep copy and don't export Go methods. js.MakeWrapper does export methods, but it already has to heavily instrument them to make sure arguments and return values can be properly converted, so attaching methods to the exported value won't be difficult.
js.Object implementation will become much less of a special case for the compiler. Since we no longer impose any metadata requirements on the Go value representation, an external JS object can be plausibly used as a receiver. In reality we will still want to inline most of the methods for performance reasons, but that should be a relatively narrow specialization.
Uses of js.InternalObject may get broken if people use it to fiddle with methods or type metadata, but since this isn't an API intended for public use, this should be okay.
An accidental, although welcome, side effect of this is that println() output of a Go value will become a lot more interpretable, since all the Go type metadata won't be dumped half the time.
The fuzziest part of this design is representing pointers and uintptr. The current implementation is not particularly consistent between different types and I haven't fully wrapped my mind around it. I think in general types pointers can keep their runtime interface of $get and $set methods, and unsafe.Pointer can wrap the underlying JS value without any Go metadata. That would make unsafe type casting actually work in GopherJS reliably. But, like I said, this needs more thought.
The text was updated successfully, but these errors were encountered:
Disclaimer: this is a very rough proposal not immediately planned for implementation. It's been bouncing around my head for a while, though, so I wanted to put it up for some discussion. In case it has some fatal flaws, it would be nice to discover them before I put any significant effort into prototyping.
High-level idea
Go types that with identical underlying types must have identical in-memory representation in GopherJS.
For example, objects backing
s
andq
must be deeply equal from JS perspective:Discussion
Today, all GopherJS-created values (with the exception of wrapped types) carry an association with the Go type metadata. While this seems nice on paper, it leads to complications and edge cases, for example:
My best guess is that the current implementation was chosen so that calling methods on structs like
s.Foo()
is expressed as calling object's method in JS. This is nice and idiomatic from the JS perspective, but begins to break down when we need to call methods on non-struct types.Neither is really a problem in the vanilla Go: any value is represented as a chunk of memory and it's totally up to the compiler how to interpret the value. Using the example above, conversion from
*S
to*Q
is a no-op at runtime: it only changes which operations the compiler would permit against it, but no reshuffling of in-memory data is required.While this requires a lot more detailed design, here are some key points:
s := S{i: 42}
will compile tolet s = {i: 42}
, as opposed to today'slet s = new S(42)
.s.Foo()
compiles intoS.Foo(s)
. That is, the methods are not attached to the value instance, but to the type. Note that this conceptually matches the general Go's implementation of methods, where a method call is a syntax sugar for theType.MethodName(receiver, arg1, argv2)
syntax. There are two cases where dynamic dispatch is necessary: interfaces and reflection.S
to access the method we'll use a dynamic reference to the type stored inside the interface value. For example fortype I interface { Foo() }
the call site will look likei.typ.Foo(i.val)
. This is conceptually closer (although not identical) to how interfaces are implemented in vanilla Go.js.MakeWrapper
does export methods, but it already has to heavily instrument them to make sure arguments and return values can be properly converted, so attaching methods to the exported value won't be difficult.js.Object
implementation will become much less of a special case for the compiler. Since we no longer impose any metadata requirements on the Go value representation, an external JS object can be plausibly used as a receiver. In reality we will still want to inline most of the methods for performance reasons, but that should be a relatively narrow specialization.Uses of
js.InternalObject
may get broken if people use it to fiddle with methods or type metadata, but since this isn't an API intended for public use, this should be okay.An accidental, although welcome, side effect of this is that
println()
output of a Go value will become a lot more interpretable, since all the Go type metadata won't be dumped half the time.The fuzziest part of this design is representing pointers and
uintptr
. The current implementation is not particularly consistent between different types and I haven't fully wrapped my mind around it. I think in general types pointers can keep their runtime interface of$get
and$set
methods, andunsafe.Pointer
can wrap the underlying JS value without any Go metadata. That would make unsafe type casting actually work in GopherJS reliably. But, like I said, this needs more thought.The text was updated successfully, but these errors were encountered: