Skip to content

Fields of embedded structs are not set correctly #640

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

Open
theclapp opened this issue May 4, 2017 · 10 comments
Open

Fields of embedded structs are not set correctly #640

theclapp opened this issue May 4, 2017 · 10 comments

Comments

@theclapp
Copy link

theclapp commented May 4, 2017

In the below I'd expect s.F1 = "foo" to compile to s.Object.t1.Object.f1 = "foo", but instead it seems to compile to s.Object.t1 = "foo". In the marked assignment ("error here"), running in Node I get "TypeError: Cannot create property 'f1' on string 'foo'"; in Chrome with similar code I get a similar error. (The playground behaves differently -- it doesn't error, but it doesn't get it right, either).

type T1 struct {
	*js.Object
	F1 string `js:"f1"`
}
s := &struct {
	*js.Object
	*T1 `js:"t1"`
}{Object: hvue.NewObject()}
s.T1 = &T1{Object: hvue.NewObject()}
s.F1 = "foo"
println("s.F1:", s.F1)
println("s.T1.F1:", s.T1.F1)
s.T1.F1 = "bar" // <- error here

This code compiles as

s = new structType.ptr(hvue.NewObject(), ptrType.nil);
s.Object.t1 = $externalize(new T1.ptr(hvue.NewObject(), ""), ptrType);
s.Object.t1 = $externalize("foo", $String); // <- This is wrong
console.log("s.F1:", $internalize(s.Object.t1, $String));
console.log("s.T1.F1:", $internalize($internalize(s.Object.t1, ptrType).Object.f1, $String));
$internalize(s.Object.t1, ptrType).Object.f1 = $externalize("bar", $String);

Am I doing something wrong and/or unsupported?

@myitcv
Copy link
Member

myitcv commented May 4, 2017

@theclapp there's two issues/bugs here that are related.

It appears that fields of embedded structs are not being correctly transpiled. Given:

package main

import (
	"github.com/gopherjs/gopherjs/js"
)

type T1 struct {
	*js.Object
	F1 string `js:"f1"`
}

type T2 struct {
	*js.Object
	*T1 `js:"t1"`
}

func main() {
	s := &T2{Object: js.Global.Get("Object").New()}
	s.T1 = &T1{Object: js.Global.Get("Object").New()}

	_ = s.F1
}

the transpiled result of the _ assignment is:

$unused($internalize(s.Object.t1, $String));

which is clearly wrong.

If we then change the assignment to explicitly dereference via the T1 field we see the second bug (which is similar issue to my conclusion in #639 (comment)):

$unused($internalize($internalize(s.Object.t1, ptrType).Object.f1, $String));

i.e. the superfluous $internalize(s.Object.t1, ptrType).Object (because it's equivalent to s.Object.t1)

So the workaround for now (again, this will be fixed in #617 and #633) is to explicitly dereference fields in a multi step process:

// EDIT: IGNORE THIS...

// t := s.T1
// _ = t.F1

which transpiles to:

// EDIT: IGNORE THIS

// t = $internalize(s.Object.t1, ptrType);
// $unused($internalize(t.Object.f1, $String));

The desired result here is (I think) which removes the redundant $internalize and associated allocation is that:

_ = s.F1

transpiles to:

$unused($internalize(s.Object.t1.f1, $String));

@theclapp
Copy link
Author

theclapp commented May 4, 2017

$unused($internalize($internalize(s.Object.t1, ptrType).Object.f1, $String));

i.e. the erroneous $internalize(s.Object.t1, ptrType)

I'll have to trust you that that's a bug. :) FWIW, it does seem to work.

So the workaround for now [...] is to explicitly dereference fields in a multi step process:

t := s.T1
_ = t.F1

I'm not sure I understand. t := s.T1.F1 (and s.T1.F1 = "something") seem to work just fine (even if they are suboptimal in having too many calls to $internalize().

this will be fixed in #617 and #633

So, do we have any ETA on those?

@theclapp
Copy link
Author

theclapp commented May 4, 2017

In case anyone's wondering (I was :), access of regular Go embedded fields works fine, e.g.

type T3 struct {
	F3 string
}
type T4 struct {
	T3
}
t4 := T4{T3{"foo"}}
println(t4.F3)

works fine and transpiles to

t4 = new T4.ptr(new T3.ptr("foo"));
console.log(t4.T3.F3);

As an aside, I've seen several recent issues saying js.Global.Get("console").Call("log",...) instead of println. Is println deprecated, or considered bad style, or something? I know it's more-or-less both in regular Go code, but frankly I've ignored that in GopherJS code.

@myitcv
Copy link
Member

myitcv commented May 4, 2017

I'm not sure I understand. t := s.T1.F1 (and s.T1.F1 = "something") seem to work just fine (even if they are suboptimal in having too many calls to $internalize().

You're right; to confirm, whilst suboptimal, this does work. I was conflating two things here (edited my comment above).

The point I failed to make was that in making this optimal we will also solve #639 when it comes to comparison of values of pointer to *js.Object-special struct types.

So, do we have any ETA on those?

Not a specific one from my side, no. But they are issues that are also blocking me in https://github.com/myitcv/react so I have incentive to work on them asap.

We have explicit agreement from @neelance and @shurcooL on the major points (and @flimzy has offered some helpful feedback too); I'll ping a few others to see if they have anything to add into the mix.

@theclapp
Copy link
Author

theclapp commented May 4, 2017

Thanks, @myitcv, for your comments and feedback.

I forgot to mention, re:

The desired result here is (I think) which removes the redundant $internalize and associated allocation is that: _ = s.F1 transpiles to: $unused($internalize(s.Object.t1.f1, $String));

This isn't quite right. We want it to transpile to ...s.Object.t1.Object.f1.... I think you know what we want and just left out the extra Object.

@myitcv
Copy link
Member

myitcv commented May 4, 2017

This isn't quite right. We want it to transpile to ...s.Object.t1.Object.f1.... I think you know what we want and just left out the extra Object.

Actually I think that's what I intended because:

s.Object.t1 === $internalize(s.Object.t1, ptrType).Object // true

(if you run https://gopherjs.github.io/playground/#/_9Zv0-b_lc locally and set a breakpoint on the $unused line, then evaluate the above)

The Object dereference is only required if you have $internalize-d a the Javascript object in field t1; in this case (and others) this $internalize is not necessary (in fact I think it's only required when invoking methods... but more thought required on that)

@j7b
Copy link

j7b commented Jul 3, 2017

Possibly related to this and other issues mentioned:

https://gopherjs.github.io/playground/#/J4t4vqRaj2

Seems to crash the playground, when gopherjs run node stack trace starts with

TypeError: Cannot read property 'apply' of undefined
    at Object.target.(anonymous function) [as GetRotation]

@theclapp
Copy link
Author

theclapp commented Jul 3, 2017

On re-reading my own initial bug report, I think this is, in fact, wrong (Edit: But see later comment):

type T1 struct {
	*js.Object
	F1 string `js:"f1"`
}
s := &struct {
	*js.Object
	*T1 `js:"t1"` // <- shouldn't have this `js:"t1"` tag
}{Object: hvue.NewObject()}

That should be

type T1 struct {
	*js.Object
	F1 string `js:"f1"`
}
s := &struct {
	*js.Object
	*T1 // <- no js tag here
}{Object: hvue.NewObject()}

with no js tag on *T1 in s. If I do it that way, then (I think) it works (in the playground). (I haven't tried it in my actual app and don't know what it transpiles to, though.)

I'm inclined to close this bug as "user error / don't do that then". (Edit: Again, see later comment.)

@theclapp
Copy link
Author

theclapp commented Jul 7, 2017

So I tried this again in my actual app, and it does seem like it should work and doesn't.

The core problem is that data4.Color = "red" gets transpiled as data4.Object.styleObject = $externalize("red", $String);, but should be transpiled the same as data4.StyleObject.Color = "blue", which is $internalize(data4.Object.styleObject, ptrType$3).Object.color = $externalize("blue", $String);. (This seems like it could be more efficient? Skipping the call to $internalize? But maybe not.)

Here's the full code and the full transpiled Javascript:

type StyleObject struct {
	*js.Object
	Color    string `js:"color"`
	FontSize string `js:"fontSize"`
}
data4 := &struct {
	*js.Object
	*StyleObject `js:"styleObject"`
}{Object: hvue.NewObject()}
data4.StyleObject = &StyleObject{Object: hvue.NewObject()}
println("assigning Color & FontSize")
data4.Color = "red"
data4.FontSize = "13px"
println("Color:", data4.Color)                                         // => Color: 13px
println("FontSize:", data4.FontSize)                                   // => FontSize: 13px
println("data4.StyleObject.Color:", data4.StyleObject.Color)           // => data4.StyleObject.Color: undefined
println("data4.StyleObject.FontSize:", data4.StyleObject.FontSize)     // => data4.StyleObject.FontSize: undefined
println("data4 json:", js.Global.Get("JSON").Call("stringify", data4)) // => data4 json: {"styleObject":"13px"}

data4.StyleObject = &StyleObject{Object: hvue.NewObject()}
data4.StyleObject.Color = "blue"
data4.StyleObject.FontSize = "14px"
println("reassinged")
println("Color:", data4.Color)                                     // => Color: [object Object]
println("FontSize:", data4.FontSize)                               // => FontSize: [object Object]
println("data4.StyleObject.Color:", data4.StyleObject.Color)       // => data4.StyleObject.Color: blue
println("data4.StyleObject.FontSize:", data4.StyleObject.FontSize) // => data4.StyleObject.FontSize: 14px

And here's the transpiled JS:

console.log("assigning Color & FontSize");
data4.Object.styleObject = $externalize("red", $String);
data4.Object.styleObject = $externalize("13px", $String);
console.log("Color:", $internalize(data4.Object.styleObject, $String));
console.log("FontSize:", $internalize(data4.Object.styleObject, $String));
console.log("data4.StyleObject.Color:", $internalize($internalize(data4.Object.styleObject, ptrType$3).Object.color, $String));
console.log("data4.StyleObject.FontSize:", $internalize($internalize(data4.Object.styleObject, ptrType$3).Object.fontSize, $String));
console.log("data4 json:", $global.JSON.stringify($externalize(data4, ptrType$4)));
data4.Object.styleObject = $externalize(new StyleObject.ptr(hvue.NewObject(), "", ""), ptrType$3);
$internalize(data4.Object.styleObject, ptrType$3).Object.color = $externalize("blue", $String);
$internalize(data4.Object.styleObject, ptrType$3).Object.fontSize = $externalize("14px", $String);
console.log("reassinged");
console.log("Color:", $internalize(data4.Object.styleObject, $String));
console.log("FontSize:", $internalize(data4.Object.styleObject, $String));
console.log("data4.StyleObject.Color:", $internalize($internalize(data4.Object.styleObject, ptrType$3).Object.color, $String));
console.log("data4.StyleObject.FontSize:", $internalize($internalize(data4.Object.styleObject, ptrType$3).Object.fontSize, $String));

@theclapp
Copy link
Author

theclapp commented Apr 9, 2021

As near as I can tell, this is still broken.

If you embed a struct type, and DO NOT give it a js tag (as in s.T1 below)

type T1 struct {
	*js.Object
	F1 string `js:"f1"`
}
s := &struct {
	*js.Object
	*T1 // <- no js tag here
}{Object: hvue.NewObject()}

then you can reference s.F1 directly, instead of saying s.T1.F1.

But only in Go. If you need to reference s.T1 (at all) in Javascript, you can't, since it has no js tag. So excluding the js tag for embedded struct fields is not a feasible solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants