Files
v8/test/debugger/debug/debug-modules-set-variable-value.mjs
Simon Zünd e58e145c8c [debug] Walk outer lexical scopes via DebugScriptScope
Now that DebugScriptScopeInfo provides the full lexical scope tree for
every script, ScopeIterator no longer switches to context-only
iteration when it leaves the paused closure. Instead it keeps walking
DebugScriptScope into outer lexical scopes, including outer scopes
that don't need a context. Stack-allocated variables of such scopes
are reported as unavailable. Iteration falls back to the context chain
once it leaves the root of the scope tree (e.g. for eval and wrapped
functions).

Empty outer scopes are still skipped for now; a follow-up changes that.
[[Scopes]] is unaffected, since the inspector builds it from the runtime
context chain.

- GetFunctionDebugName() returns undefined for outer scopes whose
  enclosing function has no context, instead of using the name of an
  unrelated closure context.
- DebugEvaluate uses the closest runtime context when targeting an
  outer scope without a context (EvaluationContext()), and always wraps
  it for contextless target scopes so the scope's blocklist applies,
  even if the closest context is the native context. Otherwise
  evaluating `x` in a contextless scope that declares `x` would find a
  global `x`.

R=pfaffe@chromium.org

Bug: 545412300
Change-Id: Iaee6cfb7e1bee13c0bd6c3ff61d7e9c82a9b8491
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8460005
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Commit-Queue: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110376}
2026-10-06 21:52:25 -07:00

377 lines
9.1 KiB
JavaScript

// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --no-turbofan
// The first part of this file is copied over from debug-set-variable-value.js
// (a few tests were removed because they make no sense for modules). The second
// part is new.
var Debug = debug.Debug;
// Accepts a function/closure 'fun' that must have a debugger statement inside.
// A variable 'variable_name' must be initialized before debugger statement
// and returned after the statement. The test will alter variable value when
// on debugger statement and check that returned value reflects the change.
function RunPauseTest(scope_number, expected_old_result, variable_name,
new_value, expected_new_result, fun) {
var actual_old_result = fun();
assertEquals(expected_old_result, actual_old_result);
var listener_delegate;
var listener_called = false;
var exception = null;
function listener_delegate(exec_state) {
var scope = exec_state.frame(0).scope(scope_number);
scope.setVariableValue(variable_name, new_value);
}
function listener(event, exec_state, event_data, data) {
try {
if (event == Debug.DebugEvent.Break) {
listener_called = true;
listener_delegate(exec_state);
}
} catch (e) {
exception = e;
}
}
// Add the debug event listener.
Debug.setListener(listener);
var actual_new_result;
try {
actual_new_result = fun();
} finally {
Debug.setListener(null);
}
if (exception != null) {
assertUnreachable("Exception in listener\n" + exception.stack);
}
assertTrue(listener_called);
assertEquals(expected_new_result, actual_new_result);
}
function ClosureTestCase(scope_index, old_result, variable_name, new_value,
new_result, success_expected, factory) {
this.scope_index_ = scope_index;
this.old_result_ = old_result;
this.variable_name_ = variable_name;
this.new_value_ = new_value;
this.new_result_ = new_result;
this.success_expected_ = success_expected;
this.factory_ = factory;
}
ClosureTestCase.prototype.run_pause_test = function() {
var th = this;
var fun = this.factory_(true);
this.run_and_catch_(function() {
RunPauseTest(th.scope_index_ + 1, th.old_result_, th.variable_name_,
th.new_value_, th.new_result_, fun);
});
}
ClosureTestCase.prototype.run_and_catch_ = function(runnable) {
if (this.success_expected_) {
runnable();
} else {
assertThrows(runnable);
}
}
// Test scopes visible from closures.
var closure_test_cases = [
new ClosureTestCase(0, 'cat', 'v1', 5, 5, true,
function Factory(debug_stop) {
var v1 = 'cat';
return function() {
if (debug_stop) debugger;
return v1;
}
}),
new ClosureTestCase(0, 4, 't', 7, 9, true, function Factory(debug_stop) {
var t = 2;
var r = eval("t");
return function() {
if (debug_stop) debugger;
return r + t;
}
}),
new ClosureTestCase(0, 6, 't', 10, 13, true, function Factory(debug_stop) {
var t = 2;
var r = eval("t = 3");
return function() {
if (debug_stop) debugger;
return r + t;
}
}),
new ClosureTestCase(2, 'capybara', 'foo', 77, 77, true,
function Factory(debug_stop) {
var foo = "capybara";
return (function() {
var bar = "fish";
try {
throw {name: "test exception"};
} catch (e) {
return function() {
if (debug_stop) debugger;
bar = "beast";
return foo;
}
}
})();
}),
new ClosureTestCase(0, 'AlphaBeta', 'eee', 5, '5Beta', true,
function Factory(debug_stop) {
var foo = "Beta";
return (function() {
var bar = "fish";
try {
throw "Alpha";
} catch (eee) {
return function() {
if (debug_stop) debugger;
return eee + foo;
}
}
})();
})
];
for (var i = 0; i < closure_test_cases.length; i++) {
closure_test_cases[i].run_pause_test();
}
// Test local scope.
RunPauseTest(0, 'HelloYou', 'u', 'We', 'HelloWe', (function Factory() {
return function() {
var u = "You";
var v = "Hello";
debugger;
return v + u;
}
})());
RunPauseTest(0, 'Helloworld', 'p', 'GoodBye', 'HelloGoodBye',
(function Factory() {
function H(p) {
var v = "Hello";
debugger;
return v + p;
}
return function() {
return H("world");
}
})());
RunPauseTest(0, 'mouse', 'v1', 'dog', 'dog', (function Factory() {
return function() {
var v1 = 'cat';
eval("v1 = 'mouse'");
debugger;
return v1;
}
})());
// Check that we correctly update local variable that
// is referenced from an inner closure.
RunPauseTest(0, 'Blue', 'v', 'Green', 'Green', (function Factory() {
return function() {
function A() {
var v = "Blue";
function Inner() {
return void v;
}
debugger;
return v;
}
return A();
}
})());
// Check that we correctly update parameter, that is known to be stored
// both on stack and in heap.
RunPauseTest(0, 5, 'p', 2012, 2012, (function Factory() {
return function() {
function A(p) {
function Inner() {
return void p;
}
debugger;
return p;
}
return A(5);
}
})());
////////////////////////////////////////////////////////////////////////////////
// From here on we test the module scope.
////////////////////////////////////////////////////////////////////////////////
// Non-existing variable.
{
let exception;
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let module_scope = exec_state.frame().scope(1);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
try {
module_scope.setVariableValue('spargel', 42);
} catch(e) { exception = e; }
}
}
Debug.setListener(listener);
assertThrows(() => spargel, ReferenceError);
debugger;
assertThrows(() => spargel, ReferenceError);
assertTrue(exception !== undefined);
}
// Local (non-exported) variable.
let salat = 12;
{
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let module_scope = exec_state.frame().scope(1);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
module_scope.setVariableValue('salat', 42);
}
}
Debug.setListener(listener);
assertEquals(12, salat);
debugger;
assertEquals(42, salat);
}
// Local (non-exported) variable, nested access.
let salad = 12;
{
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let scope_count = exec_state.frame().scopeCount();
let module_scope = exec_state.frame().scope(2);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
module_scope.setVariableValue('salad', 42);
}
}
Debug.setListener(listener);
function foo() {
assertEquals(12, salad);
debugger;
assertEquals(42, salad);
};
foo();
}
// Exported variable.
export let salami = 1;
{
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let module_scope = exec_state.frame().scope(1);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
module_scope.setVariableValue('salami', 2);
}
}
Debug.setListener(listener);
assertEquals(1, salami);
debugger;
assertEquals(2, salami);
}
// Exported variable, nested access.
export let ham = 1;
{
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let scope_count = exec_state.frame().scopeCount();
let module_scope = exec_state.frame().scope(2);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
module_scope.setVariableValue('ham', 2);
}
}
Debug.setListener(listener);
function foo() {
assertEquals(1, ham);
debugger;
assertEquals(2, ham);
};
foo();
}
// Imported variable. Setting is currently not supported.
import { salami as wurst } from "./debug-modules-set-variable-value.mjs";
{
let exception;
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let module_scope = exec_state.frame().scope(1);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
try {
module_scope.setVariableValue('wurst', 3);
} catch(e) { exception = e; }
}
}
Debug.setListener(listener);
assertEquals(2, wurst);
debugger;
assertEquals(2, wurst);
assertTrue(exception !== undefined);
}
// Imported variable, nested access. Setting is currently not supported.
import { salami as wurstl } from "./debug-modules-set-variable-value.mjs";
{
let exception;
function listener(event, exec_state) {
if (event == Debug.DebugEvent.Break) {
let scope_count = exec_state.frame().scopeCount();
let module_scope = exec_state.frame().scope(2);
assertEquals(debug.ScopeType.Module, module_scope.scopeType());
try {
module_scope.setVariableValue('wurstl', 3);
} catch(e) { exception = e; }
}
}
Debug.setListener(listener);
function foo() {
assertEquals(2, wurstl);
debugger;
assertEquals(2, wurstl);
assertTrue(exception !== undefined);
};
foo();
}
Debug.setListener(null);