Skip to content

Conversation

@jakebailey
Copy link
Member

@jakebailey jakebailey commented Jan 6, 2025

The current Go port of the expression evaluator uses plain Go conversions; this turns out to be subtly incorrect for certain values as the conversion Go does is not the same one specified by ECMAScript. We didn't have this problem in JS because... we were already running in JS, so got its behavior for free.

This PR reimplements number operations following the ECMAScript spec, which involves a little more work than a conversion for out-of-range values, along with a load of tests which verify the implementation. Notably, it's possible to make a "fast" implementation that passes on amd64, but then fail on arm64, so it's good that macOS is now an arm64 platform.

Sidenotes:

  • The behavior of native amd64 conversion is more or less spec compliant, but the default behavior of the conversion in arm64 is to use FCVTZS, which explicitly has a different behavior. This matters enough that arm64 added FJCVTZS ("Floating-point Javascript Convert to Signed fixed-point, rounding toward Zero"), which is used by all major JS engines, but is unavailable to us in Go.
  • In looking at other implementations of ToInt32/ToUint32, I found that esbuild does it similarly to what I did (modulo... the modulo part), but goja's implementation is actually broken on arm64. Should report that.
  • The major JS engines (outside arm64) use bitwise operations to achieve the same thing, but are not super well documented as to why what they're doing works, which is extra annoying because they are all very different. Using a bitwise implementation would be faster, but I haven't been able to rationalize it. The speedup only actually benefits the "out of range" values, which we rarely evaluate anyway.
  • The major engines implement ToInt32 instead of ToUint32 since FJCVTZS converts to signed. This difference is not super important if you can't use FJCVTZS; the bits are identical so the name is just "what type I give to the result".

This issue was more or less caught in #201, which detected the truncation of potentially large inputs and led me down the rabbit hole.

@jakebailey jakebailey changed the title Implement bitwise operations per ECMAScript spect Implement bitwise operations per ECMAScript spec Jan 6, 2025
Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

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

This looks good, though admittedly it may be easy to forget to do this in other places. One way to mitigate this would be if values that we intend to be JS numbers were forced to be a separate type.

On that note, we have a internal/stringutil/convert.go. After this PR, would it make more sense for those functions to move into the jsnum package?

@jakebailey
Copy link
Member Author

This looks good, though admittedly it may be easy to forget to do this in other places. One way to mitigate this would be if values that we intend to be JS numbers were forced to be a separate type.

I thought about doing that, but I wasn't sure if it would scale very well. Worth a try quick just to see.

On that note, we have a internal/stringutil/convert.go. After this PR, would it make more sense for those functions to move into the jsnum package?

Yes, I think that is a good idea, given that is also "number stuff that differs from JS to Go".

@jakebailey
Copy link
Member Author

Yeah, so % and ** are also not implemented correctly, this time due to IEEE having changed after ECMAScript encoded some NaN-preserving behavior, so I'll have to fix that too.

@jakebailey jakebailey changed the title Implement bitwise operations per ECMAScript spec Implement number operations per ECMAScript spec Jan 6, 2025
@jakebailey
Copy link
Member Author

jakebailey commented Jan 6, 2025

Fixed rem/pow. I also have a change which switches all uses of float64 to a jsnum.Number type, but I think that should be a second PR.

@jakebailey jakebailey merged commit 30c5c71 into main Jan 6, 2025
12 checks passed
@jakebailey jakebailey deleted the jabaile/jsnum branch January 6, 2025 23:21
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