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}
328 lines
10 KiB
JavaScript
328 lines
10 KiB
JavaScript
// Copyright 2015 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-analyze-environment-liveness --no-experimental-reuse-locals-blocklists
|
|
|
|
// Test that debug-evaluate only resolves variables that are used by
|
|
// the function inside which we debug-evaluate. This is to avoid
|
|
// incorrect variable resolution when a context-allocated variable is
|
|
// shadowed by a stack-allocated variable.
|
|
|
|
Debug = debug.Debug
|
|
|
|
let test_name;
|
|
let listener_delegate;
|
|
let listener_called;
|
|
let exception;
|
|
let begin_test_count = 0;
|
|
let end_test_count = 0;
|
|
let break_count = 0;
|
|
|
|
// Debug event listener which delegates.
|
|
function listener(event, exec_state, event_data, data) {
|
|
try {
|
|
if (event == Debug.DebugEvent.Break) {
|
|
break_count++;
|
|
listener_called = true;
|
|
listener_delegate(exec_state);
|
|
}
|
|
} catch (e) {
|
|
exception = e;
|
|
print(e, e.stack);
|
|
}
|
|
}
|
|
Debug.setListener(listener);
|
|
|
|
function BeginTest(name) {
|
|
test_name = name;
|
|
listener_called = false;
|
|
exception = null;
|
|
begin_test_count++;
|
|
}
|
|
|
|
function EndTest() {
|
|
assertTrue(listener_called, "listener not called for " + test_name);
|
|
assertNull(exception, test_name + " / " + exception);
|
|
end_test_count++;
|
|
}
|
|
|
|
BeginTest("Check that 'x' resolves correctly and 'a' is written correctly");
|
|
var a = "a";
|
|
function f1() {
|
|
var x = 1; // context allocate x
|
|
(() => x);
|
|
var y = "y";
|
|
var z = "z";
|
|
(function () {
|
|
var x = 2; // stack allocate shadowing x
|
|
(function () {
|
|
y; // access y
|
|
debugger; // ReferenceError
|
|
})(); // 2
|
|
})(); // 1
|
|
return y;
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
for (var i = 0; i < exec_state.frameCount() - 1; i++) {
|
|
var frame = exec_state.frame(i);
|
|
var value;
|
|
try {
|
|
value = frame.evaluate("x").value();
|
|
} catch (e) {
|
|
value = e.name;
|
|
}
|
|
print(frame.sourceLineText());
|
|
var expected = frame.sourceLineText().match(/\/\/ (.*$)/)[1];
|
|
assertEquals(String(expected), String(value));
|
|
}
|
|
assertEquals("[object global]",
|
|
String(exec_state.frame(0).evaluate("this").value()));
|
|
assertEquals("y", exec_state.frame(0).evaluate("y").value());
|
|
assertEquals("a", exec_state.frame(0).evaluate("a").value());
|
|
exec_state.frame(0).evaluate("a = 'A'");
|
|
assertThrows(() => exec_state.frame(0).evaluate("z"), ReferenceError);
|
|
}
|
|
f1();
|
|
assertEquals("A", a);
|
|
a = "a";
|
|
EndTest();
|
|
|
|
BeginTest("Check that a context-allocated 'this' works")
|
|
function f2() {
|
|
var x = 1; // context allocate x
|
|
(() => x);
|
|
var y = "y";
|
|
var z = "z";
|
|
(function() {
|
|
var x = 2; // stack allocate shadowing x
|
|
(() => {
|
|
y;
|
|
a;
|
|
this; // context allocate receiver
|
|
debugger; // ReferenceError
|
|
})(); // 2
|
|
})(); // 1
|
|
return y;
|
|
};
|
|
|
|
// Uses the same listener delgate as for `f1`.
|
|
f2();
|
|
assertEquals("A", a);
|
|
EndTest();
|
|
|
|
BeginTest("Check that we don't get confused with nested scopes");
|
|
function f3() {
|
|
var x = 1; // context allocate x
|
|
(() => x);
|
|
(function() {
|
|
var x = 2; // stack allocate shadowing x
|
|
(function() {
|
|
{ // context allocate x in a nested scope
|
|
let x = 3;
|
|
(() => x);
|
|
}
|
|
debugger;
|
|
})();
|
|
})();
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
assertThrows(() => exec_state.frame(0).evaluate("x").value());
|
|
}
|
|
f3();
|
|
EndTest();
|
|
|
|
BeginTest("Check that stack-allocated variable is unavailable");
|
|
function f4() {
|
|
let a = 1;
|
|
let b = 2;
|
|
let c = 3;
|
|
() => a + c; // a and c are context-allocated
|
|
return function g() {
|
|
let a = 2; // a is stack-allocated
|
|
return function h() {
|
|
b; // b is allocated onto f's context.
|
|
debugger;
|
|
}
|
|
}
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
assertEquals(2, exec_state.frame(0).evaluate("b").value());
|
|
assertEquals(3, exec_state.frame(0).evaluate("c").value())
|
|
assertThrows(() => exec_state.frame(0).evaluate("a").value());
|
|
};
|
|
(f4())()();
|
|
EndTest();
|
|
|
|
BeginTest("Check that block lists on the closure boundary work as expected");
|
|
function f5() {
|
|
let a = 1;
|
|
() => a; // a is context-allocated
|
|
return function g() {
|
|
let a = 2; // a is stack-allocated
|
|
{
|
|
let b = 3;
|
|
return function h() {
|
|
debugger;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
assertThrows(() => exec_state.frame(0).evaluate("a").value());
|
|
};
|
|
(f5())()();
|
|
EndTest();
|
|
|
|
BeginTest("Check evaluating on outer closure scopes with shadowing");
|
|
function test_scopes_shadowing() {
|
|
let x = 'outer_x';
|
|
let y = 'outer_y';
|
|
() => [x, y]; // context allocate x and y in f7
|
|
return function g() {
|
|
let x = 'middle_x';
|
|
() => x; // context allocate x in g
|
|
return function h() {
|
|
let x = 'inner_x_stack'; // stack allocate x in h
|
|
debugger;
|
|
};
|
|
};
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
assertEquals('inner_x_stack', exec_state.frame(0).scope(0).evaluate("x").value());
|
|
assertEquals('outer_y', exec_state.frame(0).scope(0).evaluate("y").value());
|
|
|
|
assertEquals('middle_x', exec_state.frame(0).scope(1).evaluate("x").value());
|
|
assertEquals('outer_y', exec_state.frame(0).scope(1).evaluate("y").value());
|
|
|
|
assertEquals('outer_x', exec_state.frame(0).scope(2).evaluate("x").value());
|
|
assertEquals('outer_y', exec_state.frame(0).scope(2).evaluate("y").value());
|
|
};
|
|
(test_scopes_shadowing())()();
|
|
EndTest();
|
|
|
|
BeginTest("Check that outer functions also get the correct block list calculated");
|
|
// This test is important once we reuse block list info. The block list for `g`
|
|
// needs to be correctly calculated already when we stop on break_position 1.
|
|
|
|
let break_position;
|
|
function f6() {
|
|
let a = 1; // stack-allocated
|
|
return function g() { // g itself doesn't require a context.
|
|
if (break_position === 2) debugger;
|
|
let a = 2; (() => a); // context-allocated
|
|
return function h() {
|
|
if (break_position === 1) debugger;
|
|
}
|
|
}
|
|
}
|
|
|
|
listener_delegate = function (exec_state) {
|
|
assertEquals(2, exec_state.frame(0).evaluate("a").value());
|
|
}
|
|
break_position = 1;
|
|
(f6())()();
|
|
EndTest();
|
|
|
|
BeginTest("Check that outer functions also get the correct block list calculated (continued)");
|
|
listener_delegate = function (exec_state) {
|
|
assertThrows(() => exec_state.frame(0).evaluate("a").value());
|
|
}
|
|
break_position = 2;
|
|
(f6())()();
|
|
EndTest();
|
|
|
|
BeginTest("Check evaluating on outer closure scope with stack-allocated shadowed variable");
|
|
var x = 'global_x';
|
|
function test_outer_shadow() {
|
|
let x = 'outer_stack';
|
|
let closure_y = 'closure_y';
|
|
() => closure_y; // force context allocation for closure_y
|
|
return function inner() {
|
|
debugger;
|
|
};
|
|
}
|
|
listener_delegate = function(exec_state) {
|
|
assertThrows(() => exec_state.frame(0).scope(1).evaluate("x").value(), ReferenceError);
|
|
assertEquals('closure_y', exec_state.frame(0).scope(1).evaluate("closure_y").value());
|
|
};
|
|
(test_outer_shadow())();
|
|
EndTest();
|
|
|
|
BeginTest("Check evaluating on outer closure scopes when inner scope also has a context");
|
|
function test_outer_shadow_with_inner_context() {
|
|
let x = 'outer_stack';
|
|
let closure_y = 'closure_y';
|
|
() => closure_y; // force context allocation for test_outer_shadow_with_inner_context
|
|
return function inner() {
|
|
let inner_ctx = 'inner_ctx';
|
|
() => inner_ctx; // force context allocation for inner as well
|
|
debugger;
|
|
};
|
|
}
|
|
listener_delegate = function(exec_state) {
|
|
assertThrows(() => exec_state.frame(0).scope(1).evaluate("x").value(), ReferenceError);
|
|
assertEquals('closure_y', exec_state.frame(0).scope(1).evaluate("closure_y").value());
|
|
};
|
|
(test_outer_shadow_with_inner_context())();
|
|
EndTest();
|
|
|
|
BeginTest("Check that inner block locals do not leak into outer function block list");
|
|
var a_block_test = 'global_a';
|
|
var c_block_test = 'global_c';
|
|
function test_inner_block_isolation() {
|
|
let a_block_test = 42; // stack-allocated in f
|
|
let b_block_test = 21;
|
|
() => b_block_test; // force context-allocation so f has a context
|
|
{
|
|
const c_block_test = 'foo'; // stack-allocated in inner block
|
|
(function g() {
|
|
if (break_position === 1) debugger;
|
|
})();
|
|
}
|
|
return function h() {
|
|
if (break_position === 2) debugger;
|
|
};
|
|
}
|
|
|
|
listener_delegate = function(exec_state) {
|
|
// In `g` (inside the inner block), both `a_block_test` and `c_block_test` are shadowed.
|
|
assertThrows(() => exec_state.frame(0).scope(0).evaluate("a_block_test").value(), ReferenceError);
|
|
assertThrows(() => exec_state.frame(0).scope(0).evaluate("c_block_test").value(), ReferenceError);
|
|
assertEquals(21, exec_state.frame(0).scope(0).evaluate("b_block_test").value());
|
|
|
|
// The inner block (`scope(1)`) has no context, but still shadows both.
|
|
assertThrows(() => exec_state.frame(0).scope(1).evaluate("a_block_test").value(), ReferenceError);
|
|
assertThrows(() => exec_state.frame(0).scope(1).evaluate("c_block_test").value(), ReferenceError);
|
|
assertEquals(21, exec_state.frame(0).scope(1).evaluate("b_block_test").value());
|
|
|
|
// In `f` (`scope(2)`), `a_block_test` is shadowed, but `c_block_test` (from the inner block) is not.
|
|
assertThrows(() => exec_state.frame(0).scope(2).evaluate("a_block_test").value(), ReferenceError);
|
|
assertEquals('global_c', exec_state.frame(0).scope(2).evaluate("c_block_test").value());
|
|
assertEquals(21, exec_state.frame(0).scope(2).evaluate("b_block_test").value());
|
|
};
|
|
break_position = 1;
|
|
(test_inner_block_isolation())();
|
|
EndTest();
|
|
|
|
BeginTest("Check that inner block locals do not leak into sibling closure after reuse");
|
|
listener_delegate = function(exec_state) {
|
|
// In `h` (outside the inner block), `a_block_test` is shadowed, but `c_block_test` is not.
|
|
assertThrows(() => exec_state.frame(0).scope(0).evaluate("a_block_test").value(), ReferenceError);
|
|
assertEquals('global_c', exec_state.frame(0).scope(0).evaluate("c_block_test").value());
|
|
assertEquals(21, exec_state.frame(0).scope(0).evaluate("b_block_test").value());
|
|
};
|
|
break_position = 2;
|
|
(test_inner_block_isolation())();
|
|
EndTest();
|
|
|
|
assertEquals(begin_test_count, break_count,
|
|
'one or more tests did not enter the debugger');
|
|
assertEquals(begin_test_count, end_test_count,
|
|
'one or more tests did not have its result checked');
|