[go: up one dir, main page]

Page MenuHomePhabricator

Bug 1257088 - Add allowRedeclaringExistingLexicalBinding to allow Console let/const redeclaration r=nicolo-ribaudo
ClosedPublic

Authored by cemase on Sep 16 2026, 11:27 AM.
Referenced Files
F84384963: D326431.1792239609.diff
Fri, Oct 16, 12:20 PM
Unknown Object (File)
Wed, Oct 14, 11:30 AM
Unknown Object (File)
Wed, Oct 14, 11:30 AM
Unknown Object (File)
Fri, Oct 9, 12:33 PM
Unknown Object (File)
Fri, Oct 9, 12:25 PM
Unknown Object (File)
Fri, Oct 9, 12:25 PM
Unknown Object (File)
Fri, Oct 9, 11:09 AM
Unknown Object (File)
Fri, Oct 9, 10:06 AM
Subscribers

Details

Summary

Add an allowRedeclaringExistingLexicalBinding option to the Debugger
eval APIs. When you set a let or const binding in the global lexical
environment, it can be reinitialized by a later evaluation instead of
throwing a redeclaration error.

This only covers the top-level (global lexical) case.

Diff Detail

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
js/src/vm/EnvironmentObject.cpp
4178–4180

The comment should be unnecessary if the parameter name is clearer.

Also, what's "same-input duplicates" ?

This revision now requires changes to proceed.Fri, Sep 18, 12:32 PM

replMode name needs to be renamed to reflect the actual behavior.
Such as, allowRedeclaringExistingLexicalBinding or something. Otherwise, it's hard to figure out the intention and the effect in each occurrence of replMode name.

I think I was the one suggesting such name because we want to have other niceties for such "mode" (e.g. be able to evaluate top-level await expression)
But I guess we could have many options with explicit names rather than only one covering everything

replMode name needs to be renamed to reflect the actual behavior.
Such as, allowRedeclaringExistingLexicalBinding or something. Otherwise, it's hard to figure out the intention and the effect in each occurrence of replMode name.

I think I was the one suggesting such name because we want to have other niceties for such "mode" (e.g. be able to evaluate top-level await expression)
But I guess we could have many options with explicit names rather than only one covering everything

If we're to add many more special behavior, merging the option flags would make sense, but each parameter and variables in the internal should reflect the actual effect, in each specific scope.
For example, "allowing redeclaration" and "allowing top-level await" has completely different scope (where it's generated, stored, passed through, and used), and they shouldn't leak to different part of the code.

cemase updated this revision to Diff 1381827.
cemase marked 5 inline comments as done.

[…] we should add devtools test to make sure that we can redeclare let/const in the regular case, and check that it's still forbidden when we're paused in a function call in the debugger?

I don't think that this was addressed

We can probably add a new test in devtools/client/webconsole/test/browser/, and for the evaluation while paused, we can take some inspiration from devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js (or directly add a test case for it there)

[…] we should add devtools test to make sure that we can redeclare let/const in the regular case, and check that it's still forbidden when we're paused in a function call in the debugger?

I don't think that this was addressed

We can probably add a new test in devtools/client/webconsole/test/browser/, and for the evaluation while paused, we can take some inspiration from devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe.js (or directly add a test case for it there)

Hey Nicolas,

thanks for the pointer. I was working on that. I'll push it once I'm done.

arai requested changes to this revision.Mon, Sep 21, 11:53 AM

Still partial review, but there's a critical bug where the mutation is not reflected to JIT-compiled code.

The following test fails, by returning the original value 1, instead of the mutated value 2.

var g = newGlobal({ newCompartment: true });
var dbg = new Debugger;
var gw = dbg.addDebuggee(g);
gw.executeInGlobal(`
let x = 1;
function getX() {
  return x;
}
`);

// Ensure the function is JIT compiled.
gw.executeInGlobal(`
for (var i = 0; i < 2000; i++) {
  getX();
}
`);

// Mutate the binding.
gw.executeInGlobal(`let x = 2;`, {
  allowRedeclaringExistingLexicalBinding: true,
});

// Ensure that the mutation is reflected to the JIT-compiled function.
assertEq(gw.executeInGlobal(`getX();`).return, 2);

This is because the property access to x is optimized out when the getX is JIT compiled, and mutation in the redeclaration doesn't invalidate the JIT code,
and thus the subsequent call to getX returns the inlined value 1, instead of retrieving the modified binding value 2.

If you modify the testcase by replacing the mutation code to x = 2 assignment, it passes, because an assignment invalidates the JIT code.
The invalidation on the mutation is handled by js::Watchtower,
and the assignment code notifies it in NativeSetExistingDataProperty.

So, if we're to allow redeclaration in this way, it should be performed either in the following ways:

  • compile the declaration as an assignment, so that the regular assignment operation is performed and that invalidates JIT code if necessary
  • modify the declaration's initialization code, so that it performs the invalidation, but only when the redeclaration is allowed, so that it doesn't regress the performance in unrelated situation

and in either way, this will need major re-design and re-write.
Let me know if you need any help.

js/src/debugger/Debugger.cpp
391

Please update either Debugger.Frame.md or Debugger.Object.md to mention the option, with a description that covers:

  • which combination of binding kind this actually allows
    • existing var binding + new lexical binding combination is not allowed
  • when this option is allowed and when not allowed
    • This option is only for executeInGlobalMethod*, not for Frame's eval*.

Also, incompatible combination of the options should throw an error if any.
For example, this conflicts with useInnerBindings.

js/src/debugger/Debugger.h
430–431

Let's put the same types in the same group. It allows packing fields more efficiently, and also making the further modification easier.

Currently there's 2 bytes hole after bypassCSP_ field.
https://searchfox.org/firefox-main/query/default?q=field-layout%3A%27js%3A%3AEvalOptions%27

449–451

Please move this below the setBypassCSP method, to make the order consistent.

I'm sorry that the setUseInnerBindings doesn't follow the order and grouping.
It could be fixed in a separate bug.

453–455

Here too, please move this below the bypassCSP method, so that the getters and setters are grouped separately.

js/src/debugger/Frame.cpp
1117

So, the allowRedeclaringExistingLexicalBinding option is consumed only in Global and GlobalWithExtraOuterBindings kind.
Passing the option in other cases should throw an error, to avoid unexpected behavior.

js/src/jit-test/tests/debug/Object-executeInGlobal-10.js
16

Please pass the option only when actually performing. the redeclaration.
Here and other places.

16–209

Is there any reason for this rewrite?

62

Please add a testcase with block-level function declaration ({ function f() {} ) too.
It has a special behavior around the conflict.

71–83

Please add the following testcases:

  • an opposite situation where only the redeclaration closes over the binding
  • the first declaration is a const and it's closed over, and ensure that the modified value is reflected.
  • ensure the the mutation is reflected even after a JIT compilation

You can test the JIT situation by the following:

  • declare a function that closes over a lexical variable and returns it
  • call the function many times, so that the function hits the JIT compilation threshold (usually, just call the function 2000 times in a loop)
  • perform the redeclartion and mutate the value
  • call the function again, and verify the result reflects the mutation

As mentioned above, this JIT situation fails.

js/src/vm/EnvironmentObject.cpp
4000

The introduction of the above two JS::Rooted instances actually de-optimizes the code in two ways.

The first one is the removal of the RootedTuple.
The combination of the RootedTuple+RootedField is an optimization to merge multiple JS::Rooted handling.
See RootingAPI.h for more details.

The other one is that this patch moves the variable into a loop, which means, the constructor and destructor are called for each time.

Please revert the change to declRoots etc below, and let DefineLexicalBinding function take uninitialized and id.

4358
This revision now requires changes to proceed.Mon, Sep 21, 11:53 AM
cemase updated this revision to Diff 1383301.

Hey Nicolas,
I've added the tests, and they're passing.
I found that the flag was being dropped in
CompileGlobalScriptWithExtraBindings and its fallback recompile goes
through the CompileOptions copy constructor, which didn't carry the new
flag.

I've also exercised the paused-frame in CHrome, and it looks like the frame eval
creates a new scope, so let x = 3 shadows rather than redeclares.

(I'll address arai's latest review next)

Thanks, that looks good for the devtools/ changes

arai requested changes to this revision.Tue, Sep 22, 2:56 AM
arai added inline comments.
devtools/client/webconsole/test/browser/browser_jsterm_redeclare_let_const.js
46

Shouldn't this check the outer binding as well?
The info message implies that the outer binding is unmodified, but this testcase doesn't verify that.

This revision now requires changes to proceed.Tue, Sep 22, 2:56 AM
cemase added inline comments.
js/src/jit-test/tests/debug/Object-executeInGlobal-10.js
16–209

no strong reason actually, it's just the convention I've used in other jit-tests.
the "throw" in result form reads more explicitly than !!result.throw
(which relies on the thrown value being truthy). I can revert that if you prefer the original

js/src/jit-test/tests/debug/Object-executeInGlobal-10.js
16–209

in general it's better making the scope of the change small as much as possible,
so that the intention of the diff becomes clearer and easier to understand, especially when investigating the changes in future.

In this case, the semantics is slightly different between before/after code, and the testcase is now verifying slightly different thing (of course, the actual expectation is the same tho), so the diff may look like it's actually a bug fix or something where the previous code wasn't working, or something along that line.

A cleanup on existing code can be done in a separate followup bug or followup patch, with a description provided in the patch summary.

cemase updated this revision to Diff 1388141.
cemase marked 2 inline comments as done.

Nicolas,

I've trimmed the mochitest, the paused-frame test case is removed,
if you would like to take another look.

I filed a followup bug for that case:
https://bugzilla.mozilla.org/show_bug.cgi?id=2075356
We can add the frame test back once that's fixed.

cemase retitled this revision from Bug 1257088 - Add replMode to allow Console let/const redeclaration r=nicolo-ribaudo to Bug 1257088 - Add allowRedeclaringExistingLexicalBinding to allow Console let/const redeclaration r=nicolo-ribaudo.
cemase edited the summary of this revision. (Show Details)
arai requested changes to this revision.Fri, Sep 25, 12:50 PM

Thank you for updating.
There are some bugs in when to invalidate the JIT code, and also how the redeclaration between different lexical binding kind works.

js/src/debugger/Frame.cpp
1171

Theoretically, discarding the JIT script here is not sufficient.

A debugger hook onEnterFrame can run in between here and the JSOp::GlobalOrEvalDeclInstantiation opcode handling (which calls InitGlobalOrEvalDeclarations), at the js::DebugAPI::onEnterFrame call.

Which means, you can JIT-compile the function after this line, and before setting the binding to JS_UNINITIALIZED_LEXICAL.

The following testcase fails, in the same way as the previous testcase, where the access to x is optimized away, and the redeclaration's value is not reflected.

var g = newGlobal({ newCompartment: true });
var dbg = new Debugger;
var gw = dbg.addDebuggee(g);
gw.executeInGlobal(`
let x = 1;
function getX() {
  return x;
}
`);

// When entering the `let x = 2;` script's frame, JIT compile getX.
let first = true;
dbg. => {
  if (!first) {
    return;
  }
  first = false;
  gw.executeInGlobal(`
for (var i = 0; i < 2000; i++) {
  r = getX();
}
`);
};

// Mutate the binding.
gw.executeInGlobal(`let x = 2;`, {
  allowRedeclaringExistingLexicalBinding: true,
});

// Ensure that the mutation is reflected to the JIT-compiled function.
assertEq(gw.executeInGlobal(`getX()`).return, 2);

Also, given that the allowRedeclaringExistingLexicalBinding option is stored into the script flag, handling the option only here is error-prone, where a new consumer can be overlooked.

My understanding is that, you need to discard JIT code after setting the binding to JS_UNINITIALIZED_LEXICAL, in JSOp::GlobalOrEvalDeclInstantiation, so that the property access results in ReferenceError for TDZ until the initialization, and thus there's no chance for JIT compiler to optimize away the property access until then.

The other possible place to discard the JIT code is JSOp::InitGLexical, which is the place where the actual assignment is performed, but given the above ReferenceError from TDZ, I assume that's not necessary and overly complex.

1174

Also, this basically makes the DevTools console evaluation invalidate the JIT code for every each input, even if the code doesn't contain redeclaration.
This should be performed only when there's any actual redeclaration.

js/src/doc/Debugger/Debugger.Object.md
569

in terms of accessibility, "below" is not a good link text, given that it doesn't provide sufficient meaning.
https://www.w3.org/WAI/tips/writing/#make-link-text-meaningful

This can be reworded to "the executeInGlobalWithBindings section" or something along that line.

617

Here too, but it's slightly different situation than the previous one.
The option section is near enough, so, you can just remove the "see ... for details" part here.

626–627

"has no effect" is not true.
Frame#eval throws an error.

js/src/frontend/BytecodeCompiler.cpp
648–649 ↗(On Diff #1388922)

The options should be copied by the copy constructor, in the same way as other fields.
JS::TransitiveCompileOptions::copyPODTransitiveOptions

js/src/jit-test/tests/debug/Object-executeInGlobal-10.js
7–8

The previous comment should be preserved.
and the "// executeInGlobal is strict by default." comment should be put immediately before the line 10, with an empty line before it

12–209

This block should be put immediately after the existing block, so that the "By contrast" makes sense.
(and also "the above" below makes sense)

So, please put the newly added tests at the end of the existing testcases.

74

The following testcase expects the method to throw, which is different than "not recognized".
It recognizes and throws.
(so, this contradicts with the document)

js/src/vm/EnvironmentObject.cpp
3992

This keeps the const-ness even if the redeclaration uses different declaration.

The following test fails, due to the x = 3 assignment fails:

var g = newGlobal({ newCompartment: true });
var dbg = new Debugger;
var gw = dbg.addDebuggee(g);
gw.executeInGlobal(`const x = 1;`, {
  allowRedeclaringExistingLexicalBinding: true,
});
gw.executeInGlobal(`let x = 2; x = 3`, {
  allowRedeclaringExistingLexicalBinding: true,
});
assertEq(gw.executeInGlobal(`x`).return, 3);

Same for the opposite way, where the assignment on const succeeds when the original declaration is let.

If we just want to allow re-running similar input again, we could reject redeclaration with different kind (let vs const), so that you don't have to worry about the assignment after the declaration.

This revision now requires changes to proceed.Fri, Sep 25, 12:50 PM
cemase updated this revision to Diff 1391131.
cemase marked 10 inline comments as done.
arai requested changes to this revision.Tue, Sep 29, 9:07 AM

Thank you for updating!
It's almost there :)

js/src/debugger/Debugger.h
437

This change is unnecessary. It was already in the correct position, where it matches the field's order.

js/src/doc/Debugger/Debugger.Object.md
644

Please mention the restriction where the redeclarations with different const-ness are not allowed.

js/src/jit-test/tests/debug/Object-executeInGlobal-10.js
12–209

Please move this block below line 10, so that the newly added tests are added after existing tests.
So that the "By contrast" makes sense.

16–17

The last sentence is unnecessary, given that the comment is explaining what the code does, which is obvious from the code.
If you want to add comment about the option, the comment should explain the reasoning why the code is doing so,

18

accepted, or allowed, or something.
"not recognize" usually means ignored.

js/src/vm/EnvironmentObject.cpp
3991

Please also assert that the all attributes match, especially the const-ness.

3996

(optional) This is performed for each redeclaration binding.
Assuming the watchGlobalLexicalRedeclaration is not costly operation when performed multiple times (given that the dependencies will be empty in js::ObjectFuse::invalidateAllDependentIonScripts on subsequent calls), performing it here would be okay.
But otherwise it would be nice to move the call after the loop.

This revision now requires changes to proceed.Tue, Sep 29, 9:07 AM
cemase updated this revision to Diff 1391469.
cemase marked 6 inline comments as done.

Looks good! Thank you!

Assuming the try access is still restricted, I'll push this to the try and post the URL shortly.

This revision is now accepted and ready to land.Tue, Sep 29, 10:53 AM

The try run shows some failures.
Some of them (e.g. bc) seem to be infra issue, but the failure in devtools tests may be real.
https://treeherder.mozilla.org/jobs?repo=try&group_state=expanded&revision=3c06576cc74ea12209824be25ac5eaeab8fa2ad0&selectedTaskRun=Iem4w0RqSeW2G_52zw3yGg.0

Can you check if the failure happens locally, and only with the patch applied?

I confirmed the failures only reproduce with my change applied,
and they were because preferConsoleCommandsOverLocalSymbols sets useInnerBindings,
which is not compatible with the new option. So, I've gated it on !preferConsoleCommandsOverLocalSymbols,
and all 7 devtools tests plus the jit-test pass now.