rails / execjs

Run JavaScript code from Ruby
MIT License
538 stars 282 forks source link

Add test for passing Symbol to JS and fix it for GraalJSRuntime #110

Closed eregon closed 2 years ago

eregon commented 2 years ago

@byroot Could you review and merge? :)

@bjfish noticed barber and/or Discourse is relying on this.

For curiosity I also tried what happens if we pass Object.new to JS and on miniracer & node it just passes it as a String with .to_s like JSON.dump does (e.g. "#<Object:0x0000000001d05718>"). On duktape it gives TypeError: cannot convert Object and on the Graal.js backend it gives TypeError: Unknown how to convert to JS: #<Object:0x1ad8>. I think the TypeError is good there, JSON just using to_s seems rather unexpected in general, so I think let's keep that as-is.

eregon commented 2 years ago

Rhino fails the new test, cc @josh.

And it seems all the conversion is done on the rhino gem side: https://github.com/rails/execjs/blob/39118e25f9c2ee40e475ac64653e66be6f4aecd0/lib/execjs/ruby_rhino_runtime.rb#L35

So we do we fix it with an explicit .map { |value| Symbol === value ? value.to_s : value } there, or do we skip this test on Rhino, or something else? We can also report it to https://github.com/rubyjs/therubyrhino

eregon commented 2 years ago

Upstream issue: https://github.com/rubyjs/therubyrhino/issues/43

casperisfine commented 2 years ago

So we do we fix it with an explicit .map { |value| Symbol === value ? value.to_s : value } there, or do we skip this test on Rhino, or something else?

ExecJS aims at maximum consistency between the various implementations, so yes I think the Rhino adapter should cast symbols to string. That's on us though, it's fine for Rhino to have support for Symbols if it wants to. So I'd close the upstream issue.

eregon commented 2 years ago

There are already two tests skipped on Rhino: test_call_with_this and test_babel. So I think better to skip it on Rhino for now (with the issue URL) and when a decision upstream is taken we can revisit (I think it makes sense to handle Symbols in therubyrhino, if we do it here it's less efficient and potentially redundant).

casperisfine commented 2 years ago

I think it makes sense to handle Symbols in therubyrhino

I doesn't no. ExecJS goal is to be a common denominator of all available JS implementations. It shouldn't allow you to use features that that vast majority of implementations don't support.

If people wish to use these features they should use the implementation directly, not ExecJS.

Now I'm ok with just skipping the test as Rhino is far from a popular backend, but it's not an official feature of ExecJS and we might remove it at any point.

eregon commented 2 years ago

It's your decision, are you OK with the skip or should I change the Rhino backend for now?

I didn't consider it but if Symbol is supported upstream we can remove the workaround here if there wasn't a release in between (and worst case it's redundant but wouldn't change behavior).

It shouldn't allow you to use features that that vast majority of implementations don't support.

All ExecJS backends except therubyrhino support it with this PR and my view is the therubyrhino backend will support it in the future, either from upstream or with the extra .map here.

casperisfine commented 2 years ago

are you OK with the skip or should I change the Rhino backend for now?

I'm OK with both, however if you decide to go with the skip, it shouldn't point to the upstream issue because it's not a Rhino issue, it's an ExecJS issue, so it's basically a TODO.

eregon commented 2 years ago

Alright, I'll just add the change in the backend, and link to the issue there saying it might not be necessary in the future anymore

casperisfine commented 2 years ago

it might not be necessary in the future anymore

I'm not sure we understand each others. Rhino like a few other JS implementation support passing ruby specific types to the JS engine. I very much doubt they'll see this as a bug, I'm 99% sure it's a feature.

The reason we don't want to allow this in ExecJS is because many adapters can't support it, e.g. the NodeJS adapter is limited to JSON types because it has to serialize the objects and pass them through STDIN / argv.

eregon commented 2 years ago

Right, I didn't think about it like that.

MiniRacer, all external runtimes (including node), duktape, Graal.js convert Symbol to String when passing them to JS (I can't run javascriptcore/jscript locally). I consider GraalVM to already have this behavior because TruffleRuby already converts Symbols to Strings when passing them to another language, at least conceptually since there is interop for strings but not symbols (few languages have those). So this seemed to me the most sensible behavior since that's what basically all backends do.

But it's true recent JS has Symbols nowadays (although with wildly different semantics, notably they are not unique for a given name), and Rhino doesn't support that and does something different which is to wrap it like any other Object.

In any case, I updated the PR to convert in the rhino backend, that seems best for compatibility between runtimes in ExecJS.

eregon commented 2 years ago

In general ExecJS follows the semantics of JSON.generate & JSON.parse, because that's what the external runtime need to do. (this also explains why functions are replaced by nil when going to Ruby, it's also what JSON.stringify() does in JS) In those semantics, the only choice for Symbols is to convert them to JSON strings or error (or null), and converting to JSON strings seems more useful and is already relied upon by ExecJS usages.