Skip to content

Make nil == null, js.Undefined == undefined a bijection. #240

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
Jun 9, 2015
Merged

Make nil == null, js.Undefined == undefined a bijection. #240

merged 5 commits into from
Jun 9, 2015

Conversation

dmitshur
Copy link
Member

@dmitshur dmitshur commented Jun 7, 2015

This PR is a work in progress.

Fixes #237.

  • Add failing test for nil, undefined equality.
  • Run gopherjs tests with both GopherJS and Go.

@dmitshur
Copy link
Member Author

dmitshur commented Jun 7, 2015

Ok, good, the tests I've added are indeed failing now, which is expected:

https://circleci.com/gh/gopherjs/gopherjs/453

@dmitshur
Copy link
Member Author

dmitshur commented Jun 7, 2015

Woohoo, everything works! Tests pass, including old tests, and newly added tests.

Now that's the case, next step would be to see if the code can be refactored to be simpler/more direct/better. @neelance, please take a look and review.

@@ -372,7 +372,7 @@ var $internalAppend = function(slice, array, offset, length) {

var $equal = function(a, b, type) {
if (type === $jsObjectPtr) {
return a === b;
return a === b || (a.hasOwnProperty("object") && a.object === undefined && b.hasOwnProperty("object") && b.object === undefined);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to factor out this "if both a and b are undefined, return true" check out into $interfaceIsEqual, since that's where "if both a and b are nil interfaces, return true" happens. I wasn't sure which is better.

Also, I am unsure about the way "if both a and b are undefined" is being checked. It seems to work, but maybe there's a much better JavaScript way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I've written in the other comments, the *js.Object type holds the JS value itself. So when comparing two undefined, the JS value in a would be undefined and b would also be undefined. So a === b holds.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I change something else elsewhere, a === b did not hold. That's why I needed to add the "or" block. If you remove it, tests will not pass.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this necessary? Which test was failing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is really not the correct way. If something fails, then we'll have to look for the bug somewhere else. ;-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you reviewed #237? This PR is to fix the issue described there, and it uses the solution I proposed. I should make sure you agree with that proposal first, before we discuss its implementation details.

Why was this necessary? Which test was failing?

The test for #237 that I added.

Namely, calling js.Global.Call("eval", "(foo(null))")) would print expected output, but calling js.Global.Call("eval", "(foo(undefined))") would print:

x: undefined
x == nil: false
x == js.Undefined: false

Instead of:

x: undefined
x == nil: false
x == js.Undefined: true

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the x == js.Undefined case, a and b on this line should end up being the JS value undefined. But this does not happen. The issue is in $interfaceIsEqual, which does not correctly unwrap the JS value. It already shows with this simple test:

var i interface{} = js.Undefined
println(i == js.Undefined)

So $interfaceIsEqual needs to be fixed. I think it does not even need to call $equal in this case, because this just boils down to a === b which can be inlined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like the way to go. How should I unwrap the undefined value in $interfaceIsEqual?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just get the object field. Check for the type first, then you don't need hasOwnProperty.

@neelance
Copy link
Member

neelance commented Jun 7, 2015

The tests in github.com/gopherjs/gopherjs/tests only use Go features, but not the js package. That's why they can be run by both, normal Go and GopherJS. Tests for the js package are at https://github.com/gopherjs/gopherjs/blob/master/js/js_test.go and they are guarded by // +build js because the js package is not intended to work with pure Go (it could provide features to create mocks for testing, but that functionality is not implemented yet).

I am grateful that you wanted to tackle this bug, but it seems to me like you are not aware about how *js.Object works internally (and it is not really documented, sorry). The basic idea is that whenever something has the type *js.Object (with pointer), then it will directly contain a native JS object at runtime. This is why a === b works if the type which is being checked for equality is *js.Object. No need to change that. But this only applies to *js.Object. For js.Object (without pointer) and interfaces, the normal js.Object Go struct is used, which is recursive: It has a *js.Object field which, according to the rule above, can hold the JS object. Voila! For example take a look at https://github.com/gopherjs/gopherjs/blob/master/compiler/expressions.go#L1092

@dmitshur
Copy link
Member Author

dmitshur commented Jun 8, 2015

I am grateful that you wanted to tackle this bug, but it seems to me like you are not aware about how *js.Object works internally (and it is not really documented, sorry).

No problem, it's true that I am poorly familiar with internal details of GopherJS compiler, in fact this was my first time trying to make a PR for it, so I expected that even if I got it to work and pass tests, there's still a high chance my entire approach is wrong or there's a simpler, better, more direct way to accomplish it.

In addition to try to help fix this issue, it was meant to be a learning experience so I could make better contributions in the future, which is why I appreciate the explanations.

the js package is not intended to work with pure Go

Is this documented somewhere? I thought the js package did not provide functionality beyond the current mocks, but I didn't realize it was completely not intended to be used by normal Go compiler. If that's the case, what are mocks for? Are they just for docs?

Note that I initially wrote the if guard like this:

if runtime.GOARCH == "js" { ... }

But then changed it. Anyway, I thought importing js package was okay to do without hiding it behind js build tag.

@neelance
Copy link
Member

neelance commented Jun 8, 2015

The mocks are there for type checking, go/types needs something to work on. It is also quite good that you can compile it with pure Go without any errors. Just when using it at runtime, don't expect any meaningful thing to happen. That's why I'd say that printing js.Undefined right in pure Go is not a requirement. All other stuff of the js package doesn't work either with pure Go.

@neelance
Copy link
Member

neelance commented Jun 8, 2015

In short: The behavior of the js package with pure Go is currently not defined at all.

@dmitshur
Copy link
Member Author

dmitshur commented Jun 8, 2015

Ok. How would you like to proceed? Do you want to give me actionable feedback to change this PR so it can be accepted, or do you want to do this yourself?

@neelance
Copy link
Member

neelance commented Jun 8, 2015

I would love to help you. I'll post some comments on the code.

@@ -4,7 +4,7 @@ machine:

dependencies:
pre:
- cd /usr/local && sudo rm -rf go && curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | sudo tar -xz && sudo chmod a+w go/src/path/filepath
- cd /usr/local && sudo rm -rf go && curl https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | sudo tar -xz && sudo chmod a+w go/src/path/filepath
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate commit pls.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a55786e.

@dmitshur
Copy link
Member Author

dmitshur commented Jun 8, 2015

I am grateful that you wanted to tackle this bug, but it seems to me like you are not aware about how *js.Object works internally (and it is not really documented, sorry).

Just to prove how little I knew when I began working on this PR, you can see my WIP commit here from when I just got it to work, before I cleaned up all the debugging/test code. :P

dmitshur added 4 commits June 7, 2015 20:41
Catch and handle the case of both values being js.Undefined in
$interfaceIsEqual. Place this check underneath the check for
(a.constructor !== b.constructor) as an optimization.

Related to #237.
@dmitshur
Copy link
Member Author

dmitshur commented Jun 8, 2015

I've addressed all your feedback, PTAL.

I've moved the Go 1.4.1 -> 1.4.2 update into a separate commit.

I've removed the github.com/gohperjs/gopherjs/js tests from github.com/gohperjs/gopherjs/tests package, so it's no longer testing it via Go compiler. Instead, it's only in github.com/gohperjs/gopherjs/js package tests (in js_test.go).

I've broken down the verbose tests into minimal previously-failing tests.

There are now two compiler commits:

All tests are passing. Please review again and let me know how this looks.

Commit cb553f3 fixes the issue for when both *js.Object were undefined.
This change makes that fix general and apply when the value of the two
*js.Object is anything.
@neelance
Copy link
Member

neelance commented Jun 9, 2015

LGTM

neelance added a commit that referenced this pull request Jun 9, 2015
Make nil == null, js.Undefined == undefined a bijection.
@neelance neelance merged commit 2ce233f into gopherjs:master Jun 9, 2015
@dmitshur dmitshur deleted the nil-undefined-equality branch June 9, 2015 20:23
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

Successfully merging this pull request may close these issues.

2 participants