Skip to content

Mark (bigint|number).toString(radix) nosideeffects (fixes #4169)#4308

Open
mnsaglam wants to merge 1 commit intogoogle:masterfrom
mnsaglam:toStringRadix
Open

Mark (bigint|number).toString(radix) nosideeffects (fixes #4169)#4308
mnsaglam wants to merge 1 commit intogoogle:masterfrom
mnsaglam:toStringRadix

Conversation

@mnsaglam
Copy link
Copy Markdown
Contributor

  • Object.toString() was marked nosideeffects in AstAnalyzer through a special code path.
  • We extend this to (number|bigint).toString(radix) as well.

@mnsaglam
Copy link
Copy Markdown
Contributor Author

@brad4d Does this look about right?

@brad4d
Copy link
Copy Markdown
Contributor

brad4d commented Mar 26, 2026

@KimlikDAO-bot I think this is OK, but I'm having trouble importing it due to a completely unrelated problem that it's not worth explaining. Could you please rebase this PR to the current head? That should resolve the problem for me.

@mnsaglam
Copy link
Copy Markdown
Contributor Author

@brad4d It's synced to the head now. Let me know if the direction is right, if so, happy to add more tests.

@brad4d
Copy link
Copy Markdown
Contributor

brad4d commented Mar 27, 2026

Thanks, that resolved the problem.
I've imported the PR and will start testing and review.

@@ -452,4 +452,14 @@ public void testIgnoresRuntimeLibRequireStatementsOnly() {
srcs(SourceFile.fromCode("other/file.js", "'require base'")),
warning(CheckSideEffects.USELESS_CODE_ERROR));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What we're missing is a test case to confirm that the call will not be eliminated when the this object is not numerical.

I think we should have that before I try to run this through extensive testing.

@@ -3668,6 +3668,16 @@ public void testCallCache_propagatesSideEffects() {
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What we're missing is a test case to confirm that the call will not be considered pure when the this object is not numerical.

I think we should have that before I try to run this through extensive testing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just for learning: So you mean that if the compiler knows for sure that its numerical, the call can be removed. And if it does not know for sure (any type) its kept? Why wouldn't we say that toString will always only have the purpose to output a string and if anyone calls another function toString, its an idiot? Thanks.

Copy link
Copy Markdown
Contributor Author

@mnsaglam mnsaglam Mar 28, 2026

Choose a reason for hiding this comment

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

Right, we may be way overly cautious here. Instead of

      if (assumeKnownBuiltinsArePure
          && OBJECT_METHODS_WITHOUT_SIDEEFFECTS.contains(callee.getString())
          && (callNode.hasOneChild() || isNumericToStringCall(callee))) {

maybe one can decide the call to be pure if its a built-in with 0 arguments or toString(...) with any number of arguments, regardless of the node type. Something like

      if (assumeKnownBuiltinsArePure
          && OBJECT_METHODS_WITHOUT_SIDEEFFECTS.contains(callee.getString())
          && (callNode.hasOneChild() || "toString".equals(callee.getString()))) {

Whichever is the desired behavior, I can change the code to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, I did a bit of historical digging with the intent of finding out whether the callNode.hasOneChild() condition was added in response to the discovery of some bug.

The answer was "no". This condition was there when the logic was first added in 2010, and never had a comment attached to say why it was there. I assume it was just an extra signal to be sure that the valueOf or toString call didn't look suspiciously different from the standard usage.

I'll point out that OBJECT_METHODS_WITHOUT_SIDEEFFECTS has always had only valueOf and toString in it and is only used in this one location.

So, I'll suggest 3 options for you.

  1. Add the tests as I originally suggested.
  2. eliminate the OBJECT_METHODS_WITHOUT_SIDE_EFFECTS and make the logic explicitly check for either valueOf with no arguments or toString with at most 1 argument.
  3. drop all checks for number of arguments

Options 2 and 3 assume you would also remove the type-checking code, of course.

Number 3 keeps the logic the simplest, but adds a new risk of breaking code using user-written valueOf or toString that have side-effects. OTOH, it would enable elimination of all those calls when their results are unused.

Number 2 adds very little risk, so I'd be inclined to go with that one.

I think I prefer both of those options over number 1 now that I've had more time to reflect on it, because they're simpler and don't require type information. But, if you decide to add tests, I'm still OK with merging it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Awesome, thanks a lot! I'll get to this later today.

Let me research this better but I remember seeing toString() pure assumption in other tools as well (oxc, Uglify). If so, that may make 3 safer than it initially seems: most things that can be broken by this have already been broken & likely fixed.

Re: eliminating OBJECT_METHODS_WITHOUT_SIDE_EFFECTS , I've been thinking of ways to transform Object.keys(OBJECT_LITERAL) -> array literal (which would motivate keeping this map) but this is quite tricky in gcc due to prop renaming happening later. Let me understand this better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@brad4d I implemented Option 2 with minimal changes. Can you take another look?

Though, it could be nice to rearrange functionCallHasSideEffects() to have the following structure:

test callNode.isNoSideEffectsCall()
test callNode.isOnlyModifiesArgumentsCall() && NodeUtil.allArgsUnescapedLocal(callNode)
test callNode.isOnlyModifiesThisCall() && NodeUtil.evaluatesToLocalValue(callee.getFirstChild())
then test for built-ins

 - Object.toString() was marked nosideeffects in AstAnalyzer through a
   special code path.
 - We extend this to (number|bigint).toString(radix).
@brad4d
Copy link
Copy Markdown
Contributor

brad4d commented Apr 14, 2026

I've pulled in the latest version of this PR for testing and review.

FYI, I'll be on vacation April 15 - 27.
It is unlikely that the testing will complete fast enough for me to submit this before I leave.

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.

3 participants