Sfoglia il codice sorgente

Debugger improvements 3 (#852)

* Prevent recursive calls to DebugHandler Break/Step

* Reinstated Debugger StepMode tests for accessors

They are now relevant (and pass), after the improvements to call stack.
Fixed a mistake in previously skipped StepsOverSetAccessor test

* Removed DebugHandler call stack management - reuse Engine's call stack

DebugInformation now includes public representation of internal Engine callstack, rather than just list of Callable names

* Proper locations for debug call stack

* Fixed EngineTests broken by the now proper call stack size

* Early work on better debugger scopes

* Better ScopeTests, testing all scopes for non-existence of binding.

* Support for step/break on function return

Slight refactor of DebugInformation

* Scope chain resolution closer to Chromium

* Support for inspecting all frames in DebugCallStack

Lazily populated scope chains
New DebugInformation more closely aligned with devtools protocol
Currently this causes regression (and failing tests) due to lack of access to the ExecutionContext through JintCallStack

* Added CallingExecutionContext to JintCallStack

In order to allow generation of scope chains for all call frames in the call stack (for devtools), we need access to the current lexical environment all the way down the call stack.
Fixed tests checking in Block scope for const/let at the top level of functions - they're now collapsed to Local scope, mirroring the Chromium approach.

* Clean-up, tests and documentation for new Debug API

Scope Chain tests
More CallStack tests
Return point stepping test
Simplified function naming tests for CallStack (using TestHelpers)

* Simple review fixes

* New DebugScopes API

Dictionary access replaced with Bindings and BindingNames lists on DebugScope
Lazy binding value evaluation for scopes
Removed this from scope bindings - can be found with CallFrame.This
Function top-level block and function scopes are no longer combined - can be combined by caller using new IsTopLevel property on top-level block scopes.

* Simpler (and more efficient) scope bindings API

* Updated tests for new DebugScope API

* All debugger related API moved into DebugHandler

Removed LINQ evaluation of breakpoints.
Renamed BreakPoint properties and parameters to reflect the terminology of the rest of Jint (and Esprima)

* Moved GetThisEnvironment to ExecutionContext for reuse in CallFrame.This
Jither 4 anni fa
parent
commit
546f1e8960
30 ha cambiato i file con 1396 aggiunte e 514 eliminazioni
  1. 10 10
      Jint.Tests/Runtime/Debugger/BreakPointTests.cs
  2. 221 148
      Jint.Tests/Runtime/Debugger/CallStackTests.cs
  3. 52 0
      Jint.Tests/Runtime/Debugger/DebugHandlerTests.cs
  4. 336 42
      Jint.Tests/Runtime/Debugger/ScopeTests.cs
  5. 37 10
      Jint.Tests/Runtime/Debugger/StepModeTests.cs
  6. 26 2
      Jint.Tests/Runtime/Debugger/TestHelpers.cs
  7. 42 39
      Jint.Tests/Runtime/EngineTests.cs
  8. 6 68
      Jint/Engine.cs
  9. 1 1
      Jint/Native/Function/EvalFunctionInstance.cs
  10. 22 0
      Jint/Native/Function/ScriptFunctionInstance.cs
  11. 5 1
      Jint/Runtime/CallStack/CallStackElement.cs
  12. 3 0
      Jint/Runtime/CallStack/JintCallStack.cs
  13. 7 7
      Jint/Runtime/Debugger/BreakPoint.cs
  14. 99 0
      Jint/Runtime/Debugger/BreakPointCollection.cs
  15. 68 0
      Jint/Runtime/Debugger/CallFrame.cs
  16. 42 0
      Jint/Runtime/Debugger/DebugCallStack.cs
  17. 74 169
      Jint/Runtime/Debugger/DebugHandler.cs
  18. 47 7
      Jint/Runtime/Debugger/DebugInformation.cs
  19. 55 0
      Jint/Runtime/Debugger/DebugScope.cs
  20. 90 0
      Jint/Runtime/Debugger/DebugScopeType.cs
  21. 115 0
      Jint/Runtime/Debugger/DebugScopes.cs
  22. 3 1
      Jint/Runtime/Environments/DeclarativeEnvironmentRecord.cs
  23. 24 0
      Jint/Runtime/Environments/ExecutionContext.cs
  24. 2 2
      Jint/Runtime/Environments/LexicalEnvironment.cs
  25. 2 2
      Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs
  26. 1 1
      Jint/Runtime/Interpreter/Expressions/JintMemberExpression.cs
  27. 1 1
      Jint/Runtime/Interpreter/Expressions/JintSuperExpression.cs
  28. 3 1
      Jint/Runtime/Interpreter/JintStatementList.cs
  29. 1 1
      Jint/Runtime/Interpreter/Statements/JintDebuggerStatement.cs
  30. 1 1
      Jint/Runtime/Interpreter/Statements/JintTryStatement.cs

+ 10 - 10
Jint.Tests/Runtime/Debugger/BreakPointTests.cs

@@ -18,7 +18,7 @@ x++; y *= 2;
             var engine = new Engine(options => options.DebugMode());
 
             bool didBreak = false;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 Assert.Equal(4, info.CurrentStatement.Location.Start.Line);
                 Assert.Equal(5, info.CurrentStatement.Location.Start.Column);
@@ -26,7 +26,7 @@ x++; y *= 2;
                 return StepMode.None;
             };
 
-            engine.BreakPoints.Add(new BreakPoint(4, 5));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 5));
             engine.Execute(script);
             Assert.True(didBreak);
         }
@@ -50,10 +50,10 @@ test(z);";
 
             var engine = new Engine(options => { options.DebugMode(); });
             
-            engine.BreakPoints.Add(new BreakPoint("script2", 3, 0));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint("script2", 3, 0));
 
             bool didBreak = false;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 Assert.Equal("script2", info.CurrentStatement.Location.Source);
                 Assert.Equal(3, info.CurrentStatement.Location.Start.Line);
@@ -88,7 +88,7 @@ debugger;
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
 
             bool didBreak = false;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 didBreak = true;
                 return StepMode.None;
@@ -110,10 +110,10 @@ debugger;
                 .DebugMode()
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
 
-            engine.BreakPoints.Add(new BreakPoint(2, 0));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(2, 0));
 
             int breakTriggered = 0;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 breakTriggered++;
                 return StepMode.None;
@@ -138,11 +138,11 @@ test();";
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.BreakPoints.Add(new BreakPoint(4, 0));
-            engine.BreakPoints.Add(new BreakPoint(6, 0));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 0));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(6, 0));
 
             int step = 0;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 step++;
                 switch (step)

+ 221 - 148
Jint.Tests/Runtime/Debugger/CallStackTests.cs

@@ -1,4 +1,6 @@
-using Jint.Runtime.Debugger;
+using Esprima;
+using Esprima.Ast;
+using Jint.Runtime.Debugger;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger
@@ -6,218 +8,289 @@ namespace Jint.Tests.Runtime.Debugger
     public class CallStackTests
     {
         [Fact]
-        public void NamesRegularFunction()
+        public void IncludesFunctionNames()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
-
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            var script = @"
+            function foo()
             {
-                didBreak = true;
-                Assert.Equal("regularFunction", info.CallStack.Peek());
-                return StepMode.None;
-            };
+                debugger;
+            }
+            
+            function bar()
+            {
+                foo();
+            }
 
-            engine.Execute(
-                @"function regularFunction() { debugger; }
-                regularFunction()");
+            bar()";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CallStack,
+                    frame => Assert.Equal("foo", frame.FunctionName),
+                    frame => Assert.Equal("bar", frame.FunctionName),
+                    frame => Assert.Equal("(anonymous)", frame.FunctionName)
+                );
+            });
         }
 
         [Fact]
-        public void NamesFunctionExpression()
+        public void IncludesLocations()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            var script = @"
+function foo()
+{
+debugger;
+}
+            
+function bar()
+{
+foo();
+}
+
+bar()";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            TestHelpers.TestAtBreak(script, info =>
             {
-                didBreak = true;
-                Assert.Equal("functionExpression", info.CallStack.Peek());
-                return StepMode.None;
-            };
+                // The line numbers here may mislead - the positions are, as would be expected,
+                // at the position before the currently executing line, not the line after.
+                // Remember that Esprima (and hence Jint) line numbers are 1-based, not 0-based.
+                Assert.Collection(info.CallStack,
+                    // "debugger;"
+                    frame => Assert.Equal(new Position(4, 0), frame.Location.Start),
+                    // "foo();"
+                    frame => Assert.Equal(new Position(9, 0), frame.Location.Start),
+                    // "bar();"
+                    frame => Assert.Equal(new Position(12, 0), frame.Location.Start)
+                );
+            });
+        }
+
+        [Fact]
+        public void IncludesFunctionLocations()
+        {
+            var script = @"
+function foo()
+{
+debugger;
+}
+            
+function bar()
+{
+foo();
+}
 
-            engine.Execute(
-                @"const functionExpression = function() { debugger; }
-                functionExpression()");
+bar()";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                // Remember that Esprima (and hence Jint) line numbers are 1-based, not 0-based.
+                Assert.Collection(info.CallStack,
+                    // function foo()
+                    frame => Assert.Equal(new Position(2, 0), frame.FunctionLocation?.Start),
+                    // function bar()
+                    frame => Assert.Equal(new Position(7, 0), frame.FunctionLocation?.Start),
+                    // global - no function location
+                    frame => Assert.Equal(null, frame.FunctionLocation?.Start)
+                );
+            });
         }
 
         [Fact]
-        public void NamesNamedFunctionExpression()
+        public void HasReturnValue()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+            function foo()
+            {
+                return 'result';
+            }
+
+            foo();";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            var engine = new Engine(options => options.DebugMode());
+
+            bool atReturn = false;
+            bool didCheckReturn = false;
+
+            engine.DebugHandler.Step += (sender, info) =>
             {
-                didBreak = true;
-                Assert.Equal("namedFunction", info.CallStack.Peek());
-                return StepMode.None;
+                if (atReturn)
+                {
+                    Assert.NotNull(info.CurrentCallFrame.ReturnValue);
+                    Assert.Equal("result", info.CurrentCallFrame.ReturnValue.AsString());
+                    didCheckReturn = true;
+                    atReturn = false;
+                }
+
+                if (info.CurrentStatement is ReturnStatement)
+                {
+                    // Step one further, and we should have the return value
+                    atReturn = true;
+                }
+                return StepMode.Into;
             };
 
-            engine.Execute(
-                @"const functionExpression = function namedFunction() { debugger; }
-                functionExpression()");
+            engine.Execute(script);
 
-            Assert.True(didBreak);
+            Assert.True(didCheckReturn);
         }
 
         [Fact]
-        public void NamesArrowFunction()
+        public void HasThis()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+function Thing(name)
+{
+    this.name = name;
+}
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
-            {
-                didBreak = true;
-                Assert.Equal("arrowFunction", info.CallStack.Peek());
-                return StepMode.None;
-            };
+Thing.prototype.test = function()
+{
+    debugger;
+}
 
-            engine.Execute(
-                @"const arrowFunction = () => { debugger; }
-                arrowFunction()");
+var car = new Thing('car');
+car.test();
+";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, (engine, info) =>
+            {
+                Assert.Collection(info.CallStack,
+                    frame => Assert.Equal(engine.Global.Get("car"), frame.This),
+                    frame => Assert.Equal(engine.Global, frame.This)
+                );
+            });
         }
 
         [Fact]
-        public void NamesNewFunction()
+        public void NamesRegularFunction()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+            function regularFunction() { debugger; }
+            regularFunction();";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            TestHelpers.TestAtBreak(script, info =>
             {
-                didBreak = true;
-                // Ideally, this should be "(anonymous)", but FunctionConstructor sets the "anonymous" name.
-                Assert.Equal("anonymous", info.CallStack.Peek());
-                return StepMode.None;
-            };
+                Assert.Equal("regularFunction", info.CurrentCallFrame.FunctionName);
+            });
+        }
 
-            engine.Execute(
-                @"const newFunction = new Function('debugger;');
-                newFunction()");
+        [Fact]
+        public void NamesFunctionExpression()
+        {
+            string script = @"
+            const functionExpression = function() { debugger; }
+            functionExpression()";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Equal("functionExpression", info.CurrentCallFrame.FunctionName);
+            });
         }
 
         [Fact]
-        public void NamesMemberFunction()
+        public void NamesNamedFunctionExpression()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+            const functionExpression = function namedFunction() { debugger; }
+            functionExpression()";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            TestHelpers.TestAtBreak(script, info =>
             {
-                didBreak = true;
-                Assert.Equal("memberFunction", info.CallStack.Peek());
-                return StepMode.None;
-            };
+                Assert.Equal("namedFunction", info.CurrentCallFrame.FunctionName);
+            });
+        }
 
-            engine.Execute(
-                @"const obj = { memberFunction() { debugger; } };
-                obj.memberFunction()");
+        [Fact]
+        public void NamesArrowFunction()
+        {
+            string script = @"
+            const arrowFunction = () => { debugger; }
+            arrowFunction()";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Equal("arrowFunction", info.CurrentCallFrame.FunctionName);
+            });
         }
 
         [Fact]
-        public void NamesAnonymousFunction()
+        public void NamesNewFunction()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+            const newFunction = new Function('debugger;');
+            newFunction()";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            TestHelpers.TestAtBreak(script, info =>
             {
-                didBreak = true;
-                Assert.Equal("(anonymous)", info.CallStack.Peek());
-                return StepMode.None;
-            };
+                // Ideally, this should be "(anonymous)", but FunctionConstructor sets the "anonymous" name.
+                Assert.Equal("anonymous", info.CurrentCallFrame.FunctionName);
+            });
+        }
 
-            engine.Execute(
-                @"(function()
-                {
-                    debugger;
-                }());");
+        [Fact]
+        public void NamesMemberFunction()
+        {
+            string script = @"
+            const obj = { memberFunction() { debugger; } };
+            obj.memberFunction()";
 
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Equal("memberFunction", info.CurrentCallFrame.FunctionName);
+            });
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
-        public void NamesGetAccessor()
+        [Fact]
+        public void NamesAnonymousFunction()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
+            string script = @"
+            (function()
+            {
+                debugger;
+            }());";
 
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
+            TestHelpers.TestAtBreak(script, info =>
             {
-                didBreak = true;
-                Assert.Equal("get accessor", info.CallStack.Peek());
-                return StepMode.None;
+                Assert.Equal("(anonymous)", info.CurrentCallFrame.FunctionName);
+            });
+        }
+
+        [Fact]
+        public void NamesGetAccessor()
+        {
+            string script = @"
+            const obj = {
+                get accessor()
+                {
+                    debugger;
+                    return 'test';
+                }
             };
+            const x = obj.accessor;";
 
-            engine.Execute(
-                @"
-                const obj = {
-                    get accessor()
-                    {
-                        debugger;
-                        return 'test';
-                    }
-                };
-                const x = obj.accessor;");
-
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Equal("get accessor", info.CurrentCallFrame.FunctionName);
+            });
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
+        [Fact]
         public void NamesSetAccessor()
         {
-            var engine = new Engine(options => options
-                .DebugMode()
-                .DebuggerStatementHandling(DebuggerStatementHandling.Script));
-
-            bool didBreak = false;
-            engine.Break += (sender, info) =>
-            {
-                didBreak = true;
-                Assert.Equal("set accessor", info.CallStack.Peek());
-                return StepMode.None;
+            string script = @"
+            const obj = {
+                set accessor(value)
+                {
+                    debugger;
+                    this.value = value;
+                }
             };
+            obj.accessor = 42;";
 
-            engine.Execute(
-                @"
-                const obj = {
-                    set accessor(value)
-                    {
-                        debugger;
-                        this.value = value;
-                    }
-                };
-                obj.accessor = 42;");
-
-            Assert.True(didBreak);
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Equal("set accessor", info.CurrentCallFrame.FunctionName);
+            });
         }
     }
 }

+ 52 - 0
Jint.Tests/Runtime/Debugger/DebugHandlerTests.cs

@@ -0,0 +1,52 @@
+using Jint.Native.Object;
+using Jint.Runtime.Debugger;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jint.Tests.Runtime.Debugger
+{
+    public class DebugHandlerTests
+    {
+        [Fact]
+        public void AvoidsPauseRecursion()
+        {
+            // While the DebugHandler is in a paused state, it shouldn't relay further OnStep calls to Break/Step.
+            // Such calls would occur e.g. if Step/Break event handlers evaluate accessors. Failing to avoid
+            // reentrance in a multithreaded environment (e.g. using ManualResetEvent(Slim)) would cause
+            // a deadlock.
+            string script = @"
+                const obj = { get name() { 'fail'; return 'Smith'; } };
+                'target';
+            ";
+
+            var engine = new Engine(options => options.DebugMode());
+
+            bool didPropertyAccess = false;
+
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                // We should never reach "fail", because the only way it's executed is from
+                // within this Step handler
+                Assert.False(info.ReachedLiteral("fail"));
+
+                if (info.ReachedLiteral("target"))
+                {
+                    var obj = info.CurrentScopeChain.Global.GetBindingValue("obj") as ObjectInstance;
+                    var prop = obj.GetOwnProperty("name");
+                    // This is where reentrance would occur:
+                    var value = prop.Get.Invoke();
+                    didPropertyAccess = true;
+                }
+                return StepMode.Into;
+            };
+
+            engine.Execute(script);
+
+            Assert.True(didPropertyAccess);
+        }
+    }
+}

+ 336 - 42
Jint.Tests/Runtime/Debugger/ScopeTests.cs

@@ -1,12 +1,36 @@
 using Jint.Native;
+using Jint.Runtime.Debugger;
+using System.Linq;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger
 {
     public class ScopeTests
     {
+        private static JsValue AssertOnlyScopeContains(DebugScopes scopes, string name, DebugScopeType scopeType)
+        {
+            var containingScope = Assert.Single(scopes, s => s.ScopeType == scopeType && s.BindingNames.Contains(name));
+            Assert.DoesNotContain(scopes, s => s != containingScope && s.BindingNames.Contains(name));
+
+            return containingScope.GetBindingValue(name);
+        }
+
+        private static void AssertScope(DebugScope actual, DebugScopeType expectedType, params string[] expectedBindingNames)
+        {
+            Assert.Equal(expectedType, actual.ScopeType);
+            // Global scope will have a number of intrinsic bindings that are outside the scope [no pun] of these tests
+            if (actual.ScopeType != DebugScopeType.Global)
+            {
+                Assert.Equal(expectedBindingNames.Length, actual.BindingNames.Count);
+            }
+            foreach (string expectedName in expectedBindingNames)
+            {
+                Assert.Contains(expectedName, actual.BindingNames);
+            }
+        }
+
         [Fact]
-        public void GlobalsAndLocalsIncludeGlobalConst()
+        public void GlobalScopeIncludesGlobalConst()
         {
             string script = @"
                 const globalConstant = 'test';
@@ -15,16 +39,13 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Globals, g => g.Key == "globalConstant");
-                Assert.Equal("test", variable.Value.AsString());
-
-                variable = Assert.Single(info.Locals, g => g.Key == "globalConstant");
-                Assert.Equal("test", variable.Value.AsString());
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalConstant", DebugScopeType.Global);
+                Assert.Equal("test", value.AsString());
             });
         }
 
         [Fact]
-        public void GlobalsAndLocalsIncludeGlobalLet()
+        public void GlobalScopeIncludesGlobalLet()
         {
             string script = @"
                 let globalLet = 'test';
@@ -32,16 +53,13 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Globals, g => g.Key == "globalLet");
-                Assert.Equal("test", variable.Value.AsString());
-
-                variable = Assert.Single(info.Locals, g => g.Key == "globalLet");
-                Assert.Equal("test", variable.Value.AsString());
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalLet", DebugScopeType.Global);
+                Assert.Equal("test", value.AsString());
             });
         }
 
         [Fact]
-        public void GlobalsAndLocalsIncludeGlobalVar()
+        public void GlobalScopeIncludesGlobalVar()
         {
             string script = @"
                 var globalVar = 'test';
@@ -49,16 +67,13 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Globals, g => g.Key == "globalVar");
-                Assert.Equal("test", variable.Value.AsString());
-
-                variable = Assert.Single(info.Locals, g => g.Key == "globalVar");
-                Assert.Equal("test", variable.Value.AsString());
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalVar", DebugScopeType.Global);
+                Assert.Equal("test", value.AsString());
             });
         }
 
         [Fact]
-        public void OnlyLocalsIncludeLocalConst()
+        public void TopLevelBlockScopeIsIdentified()
         {
             string script = @"
                 function test()
@@ -70,33 +85,75 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Locals, g => g.Key == "localConst");
-                Assert.Equal("test", variable.Value.AsString());
-                Assert.DoesNotContain(info.Globals, g => g.Key == "localConst");
+                Assert.Equal(3, info.CurrentScopeChain.Count);
+                Assert.Equal(DebugScopeType.Block, info.CurrentScopeChain[0].ScopeType);
+                Assert.True(info.CurrentScopeChain[0].IsTopLevel);
             });
         }
 
         [Fact]
-        public void OnlyLocalsIncludeLocalLet()
+        public void NonTopLevelBlockScopeIsIdentified()
         {
             string script = @"
                 function test()
                 {
-                    let localLet = 'test';
-                    debugger;
+                    {
+                        const localConst = 'test';
+                        debugger;
+                    }
                 }
                 test();";
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Locals, g => g.Key == "localLet");
-                Assert.Equal("test", variable.Value.AsString());
-                Assert.DoesNotContain(info.Globals, g => g.Key == "localLet");
+                // We only have 3 scopes, because the function top level block scope is empty.
+                Assert.Equal(3, info.CurrentScopeChain.Count);
+                Assert.Equal(DebugScopeType.Block, info.CurrentScopeChain[0].ScopeType);
+                Assert.False(info.CurrentScopeChain[0].IsTopLevel);
             });
         }
 
         [Fact]
-        public void OnlyLocalsIncludeLocalVar()
+        public void BlockScopeIncludesLocalConst()
+        {
+            string script = @"
+                function test()
+                {
+                    {
+                        const localConst = 'test';
+                        debugger;
+                    }
+                }
+                test();";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "localConst", DebugScopeType.Block);
+                Assert.Equal("test", value.AsString());
+            });
+        }
+        [Fact]
+        public void BlockScopeIncludesLocalLet()
+        {
+            string script = @"
+                function test()
+                {
+                    {
+                        let localLet = 'test';
+                        debugger;
+                    }
+                }
+                test();";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "localLet", DebugScopeType.Block);
+                Assert.Equal("test", value.AsString());
+            });
+        }
+
+        [Fact]
+        public void LocalScopeIncludesLocalVar()
         {
             string script = @"
                 function test()
@@ -108,26 +165,26 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                var variable = Assert.Single(info.Locals, g => g.Key == "localVar");
-                Assert.Equal("test", variable.Value.AsString());
-                Assert.DoesNotContain(info.Globals, g => g.Key == "localVar");
+                AssertOnlyScopeContains(info.CurrentScopeChain, "localVar", DebugScopeType.Local);
             });
         }
 
         [Fact]
-        public void BlockScopedVariablesAreInvisibleOutsideBlock()
+        public void LocalScopeIncludesBlockVar()
         {
             string script = @"
-            debugger;
-            {
-                let blockLet = 'block';
-                const blockConst = 'block';
-            }";
+                function test()
+                {
+                    debugger;
+                    {
+                        var localVar = 'test';
+                    }
+                }
+                test();";
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                Assert.DoesNotContain(info.Locals, g => g.Key == "blockLet");
-                Assert.DoesNotContain(info.Locals, g => g.Key == "blockConst");
+                AssertOnlyScopeContains(info.CurrentScopeChain, "localVar", DebugScopeType.Local);
             });
         }
 
@@ -143,7 +200,8 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                Assert.Single(info.Locals, c => c.Key == "blockConst" && c.Value == JsUndefined.Undefined);
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "blockConst", DebugScopeType.Block);
+                Assert.Equal(JsUndefined.Undefined, value);
             });
         }
 
@@ -159,7 +217,243 @@ namespace Jint.Tests.Runtime.Debugger
 
             TestHelpers.TestAtBreak(script, info =>
             {
-                Assert.Single(info.Locals, v => v.Key == "blockLet" && v.Value.AsString() == "block");
+                AssertOnlyScopeContains(info.CurrentScopeChain, "blockLet", DebugScopeType.Block);
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForFunction()
+        {
+            string script = @"
+            function add(a, b)
+            {
+                debugger;
+                return a + b;
+            }
+            const x = 1;
+            const y = 2;
+            const z = add(x, y);";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "x", "y", "z", "add"));
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForNestedFunction()
+        {
+            string script = @"
+            function add(a, b)
+            {
+                function power(a)
+                {
+                    debugger;
+                    return a * a;
+                }
+                return power(a) + b;
+            }
+            const x = 1;
+            const y = 2;
+            const z = add(x, y);";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a"),
+                    scope => AssertScope(scope, DebugScopeType.Closure, "b", "power"), // a, this, arguments shadowed by local
+                    scope => AssertScope(scope, DebugScopeType.Global, "x", "y", "z", "add"));
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForBlock()
+        {
+            string script = @"
+            function add(a, b)
+            {
+                if (a > 0)
+                {
+                    const y = b / a;
+                    debugger;
+                }
+                return a + b;
+            }
+            const x = 1;
+            const y = 2;
+            const z = add(x, y);";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Block, "y"),
+                    scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "x", "z", "add")); // y shadowed
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForNestedBlock()
+        {
+            string script = @"
+            function add(a, b)
+            {
+                if (a > 0)
+                {
+                    const y = b / a;
+                    if (y > 0)
+                    {
+                        const x = b / y;
+                        debugger;
+                    }
+                }
+                return a + b;
+            }
+            const x = 1;
+            const y = 2;
+            const z = add(x, y);";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Block, "x"),
+                    scope => AssertScope(scope, DebugScopeType.Block, "y"),
+                    scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "z", "add")); // x, y shadowed
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForCatch()
+        {
+            string script = @"
+            function func()
+            {
+                let a = 1;
+                try
+                {
+                    throw new Error('test');
+                }
+                catch (error)
+                {
+                    debugger;
+                }
+            }
+            func();";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Catch, "error"),
+                    scope => AssertScope(scope, DebugScopeType.Block, "a"),
+                    scope => AssertScope(scope, DebugScopeType.Local, "arguments"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "func"));
+            });
+        }
+
+        [Fact]
+        public void HasCorrectScopeChainForWith()
+        {
+            string script = @"
+            const obj = { a: 2, b: 4 };
+            with (obj)
+            {
+                const x = a;
+                debugger;
+            };";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Block, "x"),
+                    scope => AssertScope(scope, DebugScopeType.With, "a", "b"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "obj"));
+            });
+        }
+
+        [Fact]
+        public void ScopeChainIncludesNonEmptyScopes()
+        {
+            string script = @"
+            const x = 2;
+            if (x > 0)
+            {
+                const y = x;
+                if (x > 1)
+                {
+                    const z = x;
+                    debugger;
+                }
+            }";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Block, "z"),
+                    scope => AssertScope(scope, DebugScopeType.Block, "y"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "x"));
+            });
+        }
+
+        [Fact]
+        public void ScopeChainExcludesEmptyScopes()
+        {
+            string script = @"
+            const x = 2;
+            if (x > 0)
+            {
+                if (x > 1)
+                {
+                    const z = x;
+                    debugger;
+                }
+            }";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CurrentScopeChain,
+                    scope => AssertScope(scope, DebugScopeType.Block, "z"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "x"));
+            });
+        }
+
+        [Fact]
+        public void ResolvesScopeChainsUpTheCallStack()
+        {
+            string script = @"
+            const x = 1;
+            function foo(a, c)
+            {
+                debugger;
+            }
+            
+            function bar(b)
+            {
+                foo(b, 2);
+            }
+
+            bar(x);";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                Assert.Collection(info.CallStack,
+                    frame => Assert.Collection(frame.ScopeChain,
+                        // in foo()
+                        scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "c"),
+                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                    ),
+                    frame => Assert.Collection(frame.ScopeChain,
+                        // in bar()
+                        scope => AssertScope(scope, DebugScopeType.Local, "arguments", "b"),
+                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                    ),
+                    frame => Assert.Collection(frame.ScopeChain,
+                        // in global
+                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                    )
+                );
             });
         }
     }

+ 37 - 10
Jint.Tests/Runtime/Debugger/StepModeTests.cs

@@ -13,7 +13,7 @@ namespace Jint.Tests.Runtime.Debugger
         /// <param name="script">Script used as basis for test</param>
         /// <param name="stepMode">StepMode to use from source to target</param>
         /// <returns>Number of steps from source to target</returns>
-        private int StepsFromSourceToTarget(string script, StepMode stepMode)
+        private static int StepsFromSourceToTarget(string script, StepMode stepMode)
         {
             var engine = new Engine(options => options
                 .DebugMode()
@@ -22,7 +22,7 @@ namespace Jint.Tests.Runtime.Debugger
             int steps = 0;
             bool sourceReached = false;
             bool targetReached = false;
-            engine.Step += (sender, info) =>
+            engine.DebugHandler.Step += (sender, info) =>
             {
                 if (sourceReached)
                 {
@@ -210,7 +210,7 @@ namespace Jint.Tests.Runtime.Debugger
             Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
+        [Fact]
         public void StepsOverGetAccessor()
         {
             var script = @"
@@ -227,7 +227,7 @@ namespace Jint.Tests.Runtime.Debugger
             Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
+        [Fact]
         public void StepsOutOfGetAccessor()
         {
             var script = @"
@@ -262,7 +262,7 @@ namespace Jint.Tests.Runtime.Debugger
             Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
+        [Fact]
         public void StepsOverSetAccessor()
         {
             var script = @"
@@ -276,10 +276,10 @@ namespace Jint.Tests.Runtime.Debugger
                 obj.test = 37;
                 'target';";
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Over));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
         }
 
-        [Fact(Skip = "Debugger has no accessor awareness yet")]
+        [Fact]
         public void StepsOutOfSetAccessor()
         {
             var script = @"
@@ -297,6 +297,33 @@ namespace Jint.Tests.Runtime.Debugger
             Assert.Equal(1, StepsFromSourceToTarget(script, StepMode.Out));
         }
 
+        [Fact]
+        public void ReturnPointIsAStep()
+        {
+            var script = @"
+                function test()
+                {
+                    'source';
+                }
+                test();
+                'target';";
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Over));
+        }
+
+        [Fact]
+        public void ReturnStatementIsAStep()
+        {
+            var script = @"
+                function test()
+                {
+                    'source';
+                    return 'result';
+                }
+                test();
+                'target';";
+            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Over));
+        }
+
         [Fact]
         public void StepOutOnlyStepsOutOneStackLevel()
         {
@@ -319,7 +346,7 @@ namespace Jint.Tests.Runtime.Debugger
 
             var engine = new Engine(options => options.DebugMode());
             int step = 0;
-            engine.Step += (sender, info) =>
+            engine.DebugHandler.Step += (sender, info) =>
             {
                 switch (step)
                 {
@@ -360,12 +387,12 @@ namespace Jint.Tests.Runtime.Debugger
 
             bool stepping = false;
 
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 stepping = true;
                 return StepMode.Over;
             };
-            engine.Step += (sender, info) =>
+            engine.DebugHandler.Step += (sender, info) =>
             {
                 if (stepping)
                 {

+ 26 - 2
Jint.Tests/Runtime/Debugger/TestHelpers.cs

@@ -39,7 +39,7 @@ namespace Jint.Tests.Runtime.Debugger
             );
 
             bool didBreak = false;
-            engine.Break += (sender, info) =>
+            engine.DebugHandler.Break += (sender, info) =>
             {
                 didBreak = true;
                 breakHandler(info);
@@ -48,9 +48,33 @@ namespace Jint.Tests.Runtime.Debugger
 
             engine.Execute(script);
 
-            Assert.True(didBreak);
+            Assert.True(didBreak, "Test script did not break (e.g. didn't reach debugger statement)");
         }
 
+        /// <summary>
+        /// Initializes engine in debugmode and executes script until debugger statement,
+        /// before calling stepHandler for assertions. Also asserts that a break was triggered.
+        /// </summary>
+        /// <param name="script">Script that is basis for testing</param>
+        /// <param name="breakHandler">Handler for assertions</param>
+        public static void TestAtBreak(string script, Action<Engine, DebugInformation> breakHandler)
+        {
+            var engine = new Engine(options => options
+                .DebugMode()
+                .DebuggerStatementHandling(DebuggerStatementHandling.Script)
+            );
+
+            bool didBreak = false;
+            engine.DebugHandler.Break += (sender, info) =>
+            {
+                didBreak = true;
+                breakHandler(engine, info);
+                return StepMode.None;
+            };
 
+            engine.Execute(script);
+
+            Assert.True(didBreak, "Test script did not break (e.g. didn't reach debugger statement)");
+        }
     }
 }

+ 42 - 39
Jint.Tests/Runtime/EngineTests.cs

@@ -1663,15 +1663,15 @@ var prep = function (fn) { fn(); };
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.Break += EngineStep;
+            engine.DebugHandler.Break += EngineStep;
 
-            engine.BreakPoints.Add(new BreakPoint(1, 1));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(1, 1));
 
             engine.Execute(@"var local = true;
                 if (local === true)
                 {}");
 
-            engine.Break -= EngineStep;
+            engine.DebugHandler.Break -= EngineStep;
 
             Assert.Equal(1, countBreak);
         }
@@ -1684,13 +1684,13 @@ var prep = function (fn) { fn(); };
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.Step += EngineStep;
+            engine.DebugHandler.Step += EngineStep;
 
             engine.Execute(@"var local = true;
                 var creatingSomeOtherLine = 0;
                 var lastOneIPromise = true");
 
-            engine.Step -= EngineStep;
+            engine.DebugHandler.Step -= EngineStep;
 
             Assert.Equal(3, countBreak);
         }
@@ -1702,14 +1702,14 @@ var prep = function (fn) { fn(); };
             stepMode = StepMode.Into;
 
             var engine = new Engine(options => options.DebugMode());
-            engine.BreakPoints.Add(new BreakPoint(1, 1));
-            engine.Step += EngineStep;
-            engine.Break += EngineStep;
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(1, 1));
+            engine.DebugHandler.Step += EngineStep;
+            engine.DebugHandler.Break += EngineStep;
 
             engine.Execute(@"var local = true;");
 
-            engine.Step -= EngineStep;
-            engine.Break -= EngineStep;
+            engine.DebugHandler.Step -= EngineStep;
+            engine.DebugHandler.Break -= EngineStep;
 
             Assert.Equal(1, countBreak);
         }
@@ -1731,8 +1731,8 @@ var prep = function (fn) { fn(); };
             stepMode = StepMode.None;
 
             var engine = new Engine(options => options.DebugMode());
-            engine.BreakPoints.Add(new BreakPoint(5, 0));
-            engine.Break += EngineStepVerifyDebugInfo;
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(5, 0));
+            engine.DebugHandler.Break += EngineStepVerifyDebugInfo;
 
             engine.Execute(@"var global = true;
                             function func1()
@@ -1742,7 +1742,7 @@ var prep = function (fn) { fn(); };
                             }
                             func1();");
 
-            engine.Break -= EngineStepVerifyDebugInfo;
+            engine.DebugHandler.Break -= EngineStepVerifyDebugInfo;
 
             Assert.Equal(1, countBreak);
         }
@@ -1755,16 +1755,19 @@ var prep = function (fn) { fn(); };
 
             Assert.NotNull(debugInfo.CallStack);
             Assert.NotNull(debugInfo.CurrentStatement);
-            Assert.NotNull(debugInfo.Locals);
-
-            Assert.Single(debugInfo.CallStack);
-            Assert.Equal("func1", debugInfo.CallStack.Peek());
-            Assert.Contains(debugInfo.Globals, kvp => kvp.Key.Equals("global", StringComparison.Ordinal) && kvp.Value.AsBoolean() == true);
-            // Globals no longer contain local variables
-            //Assert.Contains(debugInfo.Globals, kvp => kvp.Key.Equals("local", StringComparison.Ordinal) && kvp.Value.AsBoolean() == false);
-            Assert.Contains(debugInfo.Locals, kvp => kvp.Key.Equals("local", StringComparison.Ordinal) && kvp.Value.AsBoolean() == false);
-            Assert.DoesNotContain(debugInfo.Locals, kvp => kvp.Key.Equals("global", StringComparison.Ordinal));
-
+            Assert.NotNull(debugInfo.CurrentScopeChain);
+            Assert.NotNull(debugInfo.CurrentScopeChain.Global);
+            Assert.NotNull(debugInfo.CurrentScopeChain.Local);
+
+            Assert.Equal(2, debugInfo.CallStack.Count);
+            Assert.Equal("func1", debugInfo.CurrentCallFrame.FunctionName);
+            var globalScope = debugInfo.CurrentScopeChain.Global;
+            var localScope = debugInfo.CurrentScopeChain.Local;
+            Assert.Contains("global", globalScope.BindingNames);
+            Assert.Equal(true, globalScope.GetBindingValue("global").AsBoolean());
+            Assert.Contains("local", localScope.BindingNames);
+            Assert.Equal(false, localScope.GetBindingValue("local").AsBoolean());
+            Assert.DoesNotContain("global", localScope.BindingNames);
             countBreak++;
             return stepMode;
         }
@@ -1777,10 +1780,10 @@ var prep = function (fn) { fn(); };
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.Break += EngineStep;
+            engine.DebugHandler.Break += EngineStep;
 
-            engine.BreakPoints.Add(new BreakPoint(5, 16, "condition === true"));
-            engine.BreakPoints.Add(new BreakPoint(6, 16, "condition === false"));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(5, 16, "condition === true"));
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(6, 16, "condition === false"));
 
             engine.Execute(@"var local = true;
                 var condition = true;
@@ -1790,7 +1793,7 @@ var prep = function (fn) { fn(); };
                 ;
                 }");
 
-            engine.Break -= EngineStep;
+            engine.DebugHandler.Break -= EngineStep;
 
             Assert.Equal(1, countBreak);
         }
@@ -1803,7 +1806,7 @@ var prep = function (fn) { fn(); };
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.Step += EngineStep;
+            engine.DebugHandler.Step += EngineStep;
 
             engine.Execute(@"function func() // first step - then stepping out
                 {
@@ -1813,7 +1816,7 @@ var prep = function (fn) { fn(); };
                 func(); // shall not step
                 ; // shall not step ");
 
-            engine.Step -= EngineStep;
+            engine.DebugHandler.Step -= EngineStep;
 
             Assert.Equal(1, countBreak);
         }
@@ -1825,7 +1828,7 @@ var prep = function (fn) { fn(); };
 
             var engine = new Engine(options => options.DebugMode());
 
-            engine.Step += EngineStepOutWhenInsideFunction;
+            engine.DebugHandler.Step += EngineStepOutWhenInsideFunction;
 
             engine.Execute(@"function func() // first step
                 {
@@ -1835,7 +1838,7 @@ var prep = function (fn) { fn(); };
                 func(); // second step
                 ; // fourth step ");
 
-            engine.Step -= EngineStepOutWhenInsideFunction;
+            engine.DebugHandler.Step -= EngineStepOutWhenInsideFunction;
 
             Assert.Equal(4, countBreak);
         }
@@ -1847,7 +1850,7 @@ var prep = function (fn) { fn(); };
             Assert.NotNull(debugInfo);
 
             countBreak++;
-            if (debugInfo.CallStack.Count > 0)
+            if (debugInfo.CallStack.Count > 1) // CallStack always has at least one element
                 return StepMode.Out;
 
             return StepMode.Into;
@@ -1860,8 +1863,8 @@ var prep = function (fn) { fn(); };
             stepMode = StepMode.None;
 
             var engine = new Engine(options => options.DebugMode());
-            engine.BreakPoints.Add(new BreakPoint(4, 33));
-            engine.Break += EngineStep;
+            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 33));
+            engine.DebugHandler.Break += EngineStep;
 
             engine.Execute(@"var global = true;
                             function func1()
@@ -1871,7 +1874,7 @@ var prep = function (fn) { fn(); };
                             }
                             func1();");
 
-            engine.Break -= EngineStep;
+            engine.DebugHandler.Break -= EngineStep;
 
             Assert.Equal(1, countBreak);
         }
@@ -1884,7 +1887,7 @@ var prep = function (fn) { fn(); };
             var engine = new Engine(options => options.DebugMode());
 
             stepMode = StepMode.Over;
-            engine.Step += EngineStep;
+            engine.DebugHandler.Step += EngineStep;
 
             engine.Execute(@"function func() // first step
                 {
@@ -1894,7 +1897,7 @@ var prep = function (fn) { fn(); };
                 func(); // second step
                 ; // third step ");
 
-            engine.Step -= EngineStep;
+            engine.DebugHandler.Step -= EngineStep;
 
             Assert.Equal(3, countBreak);
         }
@@ -1907,7 +1910,7 @@ var prep = function (fn) { fn(); };
             var engine = new Engine(options => options.DebugMode());
 
             stepMode = StepMode.Over;
-            engine.Step += EngineStep;
+            engine.DebugHandler.Step += EngineStep;
 
             engine.Execute(@"var step1 = 1; // first step
                 var step2 = 2; // second step
@@ -1916,7 +1919,7 @@ var prep = function (fn) { fn(); };
                     ; // fifth step
                 }");
 
-            engine.Step -= EngineStep;
+            engine.DebugHandler.Step -= EngineStep;
 
             Assert.Equal(5, countBreak);
         }

+ 6 - 68
Jint/Engine.cs

@@ -66,11 +66,10 @@ namespace Jint
         private ErrorConstructor _typeError;
         private ErrorConstructor _uriError;
         private DebugHandler _debugHandler;
-        private List<BreakPoint> _breakPoints;
 
         // cached access
         private readonly List<IConstraint> _constraints;
-        private readonly bool _isDebugMode;
+        internal readonly bool _isDebugMode;
         internal readonly bool _isStrict;
         internal readonly IReferenceResolver _referenceResolver;
         internal readonly ReferencePool _referencePool;
@@ -242,26 +241,7 @@ namespace Jint
 
         internal Options Options { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }
 
-        #region Debugger
-        public delegate StepMode DebugStepDelegate(object sender, DebugInformation e);
-        public delegate StepMode BreakDelegate(object sender, DebugInformation e);
-        public event DebugStepDelegate Step;
-        public event BreakDelegate Break;
-
-        internal DebugHandler DebugHandler => _debugHandler ??= new DebugHandler(this);
-
-        public List<BreakPoint> BreakPoints => _breakPoints ??= new List<BreakPoint>();
-
-        internal StepMode? InvokeStepEvent(DebugInformation info)
-        {
-            return Step?.Invoke(this, info);
-        }
-
-        internal StepMode? InvokeBreakEvent(DebugInformation info)
-        {
-            return Break?.Invoke(this, info);
-        }
-        #endregion
+        public DebugHandler DebugHandler => _debugHandler ??= new DebugHandler(this);
 
         public ExecutionContext EnterExecutionContext(
             LexicalEnvironment lexicalEnvironment,
@@ -724,38 +704,16 @@ namespace Jint
         internal JsValue GetNewTarget(EnvironmentRecord thisEnvironment = null)
         {
             // we can take as argument if caller site has already determined the value, otherwise resolve
-            thisEnvironment ??= GetThisEnvironment();
+            thisEnvironment ??= ExecutionContext.GetThisEnvironment();
             return thisEnvironment.NewTarget;
         }
         
-        /// <summary>
-        /// https://tc39.es/ecma262/#sec-getthisenvironment
-        /// </summary>
-        internal EnvironmentRecord GetThisEnvironment()
-        {
-            // The loop will always terminate because the list of environments always
-            // ends with the global environment which has a this binding.
-            var lex = ExecutionContext.LexicalEnvironment;
-            while (true)
-            {
-                var envRec = lex._record;
-                var exists = envRec.HasThisBinding();
-                if (exists)
-                {
-                    return envRec;
-                }
-
-                var outer = lex._outer;
-                lex = outer;
-            }
-        }
-
         /// <summary>
         /// https://tc39.es/ecma262/#sec-resolvethisbinding
         /// </summary>
         internal JsValue ResolveThisBinding()
         {
-            var envRec = GetThisEnvironment();
+            var envRec = ExecutionContext.GetThisEnvironment();
             return envRec.GetThisBinding();
         }
         
@@ -1256,7 +1214,7 @@ namespace Jint
             JsValue[] arguments,
             JintExpression expression)
         {
-            var callStackElement = new CallStackElement(functionInstance, expression);
+            var callStackElement = new CallStackElement(functionInstance, expression, ExecutionContext);
             var recursionDepth = CallStack.Push(callStackElement);
 
             if (recursionDepth > Options.MaxRecursionDepth)
@@ -1266,18 +1224,8 @@ namespace Jint
                 ExceptionHelper.ThrowRecursionDepthOverflowException(CallStack, callStackElement.ToString());
             }
 
-            if (_isDebugMode)
-            {
-                DebugHandler.AddToDebugCallStack(functionInstance);
-            }
-
             var result = functionInstance.Call(thisObject, arguments);
 
-            if (_isDebugMode)
-            {
-                DebugHandler.PopDebugCallStack();
-            }
-
             CallStack.Pop();
 
             return result;
@@ -1289,7 +1237,7 @@ namespace Jint
             JsValue newTarget,
             JintExpression expression)
         {
-            var callStackElement = new CallStackElement(functionInstance, expression);
+            var callStackElement = new CallStackElement(functionInstance, expression, ExecutionContext);
             var recursionDepth = CallStack.Push(callStackElement);
 
             if (recursionDepth > Options.MaxRecursionDepth)
@@ -1299,18 +1247,8 @@ namespace Jint
                 ExceptionHelper.ThrowRecursionDepthOverflowException(CallStack, callStackElement.ToString());
             }
 
-            if (_isDebugMode)
-            {
-                DebugHandler.AddToDebugCallStack(functionInstance);
-            }
-
             var result = ((IConstructor) functionInstance).Construct(arguments, newTarget);
 
-            if (_isDebugMode)
-            {
-                DebugHandler.PopDebugCallStack();
-            }
-
             CallStack.Pop();
 
             return result;

+ 1 - 1
Jint/Native/Function/EvalFunctionInstance.cs

@@ -40,7 +40,7 @@ namespace Jint.Native.Function
 
             if (direct)
             {
-                var thisEnvRec = _engine.GetThisEnvironment();
+                var thisEnvRec = _engine.ExecutionContext.GetThisEnvironment();
                 if (thisEnvRec is FunctionEnvironmentRecord functionEnvironmentRecord)
                 {
                     var F = functionEnvironmentRecord._functionObject;

+ 22 - 0
Jint/Native/Function/ScriptFunctionInstance.cs

@@ -72,6 +72,17 @@ namespace Jint.Native.Function
                         ExceptionHelper.ThrowJavaScriptException(_engine, result.Value, result);
                     }
 
+                    // The DebugHandler needs the current execution context before the return for stepping through the return point
+                    if (_engine._isDebugMode)
+                    {
+                        // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
+                        // the function body:
+                        _engine.DebugHandler.OnReturnPoint(
+                            _functionDefinition.Function.Body,
+                            result.Type == CompletionType.Normal ? Undefined : result.Value
+                        );
+                    }
+
                     if (result.Type == CompletionType.Return)
                     {
                         return result.Value;
@@ -120,6 +131,17 @@ namespace Jint.Native.Function
                 {
                     var result = OrdinaryCallEvaluateBody(arguments, calleeContext);
 
+                    // The DebugHandler needs the current execution context before the return for stepping through the return point
+                    if (_engine._isDebugMode && result.Type != CompletionType.Throw)
+                    {
+                        // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
+                        // the function body:
+                        _engine.DebugHandler.OnReturnPoint(
+                            _functionDefinition.Function.Body,
+                            result.Type == CompletionType.Normal ? thisArgument : result.Value
+                        );
+                    }
+
                     if (result.Type == CompletionType.Return)
                     {
                         if (result.Value is ObjectInstance oi)

+ 5 - 1
Jint/Runtime/CallStack/CallStackElement.cs

@@ -3,6 +3,7 @@
 using Esprima;
 using Esprima.Ast;
 using Jint.Native.Function;
+using Jint.Runtime.Environments;
 using Jint.Runtime.Interpreter.Expressions;
 
 namespace Jint.Runtime.CallStack
@@ -11,14 +12,17 @@ namespace Jint.Runtime.CallStack
     {
         public CallStackElement(
             FunctionInstance function,
-            JintExpression expression)
+            JintExpression expression,
+            ExecutionContext callingExecutionContext)
         {
             Function = function;
             Expression = expression;
+            CallingExecutionContext = callingExecutionContext;
         }
 
         public readonly FunctionInstance Function;
         public readonly JintExpression? Expression;
+        public readonly ExecutionContext CallingExecutionContext;
 
         public Location Location
         {

+ 3 - 0
Jint/Runtime/CallStack/JintCallStack.cs

@@ -15,6 +15,9 @@ namespace Jint.Runtime.CallStack
         private readonly RefStack<CallStackElement> _stack = new();
         private readonly Dictionary<CallStackElement, int>? _statistics;
 
+        // Internal for use by DebugHandler
+        internal RefStack<CallStackElement> Stack => _stack;
+
         public JintCallStack(bool trackRecursionDepth)
         {
             if (trackRecursionDepth)

+ 7 - 7
Jint/Runtime/Debugger/BreakPoint.cs

@@ -2,31 +2,31 @@
 {
     public sealed class BreakPoint
     {
-        public BreakPoint(int line, int character)
+        public BreakPoint(int line, int column)
         {
             Line = line;
-            Char = character;
+            Column = column;
         }
 
-        public BreakPoint(int line, int character, string condition)
-            : this(line, character)
+        public BreakPoint(int line, int column, string condition)
+            : this(line, column)
         {
             Condition = condition;
         }
 
-        public BreakPoint(string source, int line, int character) : this(line, character)
+        public BreakPoint(string source, int line, int column) : this(line, column)
         {
             Source = source;
         }
 
-        public BreakPoint(string source, int line, int character, string condition) : this(source, line, character)
+        public BreakPoint(string source, int line, int column, string condition) : this(source, line, column)
         {
             Condition = condition;
         }
 
         public string Source { get; }
         public int Line { get; }
-        public int Char { get; }
+        public int Column { get; }
         public string Condition { get; }
     }
 }

+ 99 - 0
Jint/Runtime/Debugger/BreakPointCollection.cs

@@ -0,0 +1,99 @@
+using Esprima;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Jint.Runtime.Debugger
+{
+    public class BreakPointCollection : ICollection<BreakPoint>
+    {
+        private readonly List<BreakPoint> _breakPoints = new List<BreakPoint>();
+
+        public BreakPointCollection()
+        {
+        }
+
+        public int Count => _breakPoints.Count;
+
+        public bool IsReadOnly => false;
+
+        public void Add(BreakPoint breakPoint)
+        {
+            _breakPoints.Add(breakPoint);
+        }
+
+        public bool Remove(BreakPoint breakPoint)
+        {
+            return _breakPoints.Remove(breakPoint);
+        }
+
+        public void Clear()
+        {
+            _breakPoints.Clear();
+        }
+
+        public bool Contains(BreakPoint item)
+        {
+            return _breakPoints.Contains(item);
+        }
+
+        public void CopyTo(BreakPoint[] array, int arrayIndex)
+        {
+            _breakPoints.CopyTo(array, arrayIndex);
+        }
+
+        public IEnumerator<BreakPoint> GetEnumerator()
+        {
+            return _breakPoints.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return _breakPoints.GetEnumerator();
+        }
+
+        internal BreakPoint FindMatch(Engine engine, Location location)
+        {
+            foreach (var breakPoint in _breakPoints)
+            {
+                if (breakPoint.Source != null)
+                {
+                    if (breakPoint.Source != location.Source)
+                    {
+                        continue;
+                    }
+                }
+
+                bool afterStart = breakPoint.Line == location.Start.Line &&
+                                 breakPoint.Column >= location.Start.Column;
+
+                if (!afterStart)
+                {
+                    continue;
+                }
+
+                bool beforeEnd = breakPoint.Line < location.End.Line
+                            || (breakPoint.Line == location.End.Line &&
+                                breakPoint.Column <= location.End.Column);
+
+                if (!beforeEnd)
+                {
+                    continue;
+                }
+
+                if (!string.IsNullOrEmpty(breakPoint.Condition))
+                {
+                    var completionValue = engine.Execute(breakPoint.Condition).GetCompletionValue();
+
+                    if (!completionValue.AsBoolean())
+                    {
+                        continue;
+                    }
+                }
+
+                return breakPoint;
+            }
+
+            return null;
+        }
+    }
+}

+ 68 - 0
Jint/Runtime/Debugger/CallFrame.cs

@@ -0,0 +1,68 @@
+using System;
+using Esprima;
+using Esprima.Ast;
+using Jint.Native;
+using Jint.Runtime.CallStack;
+using Jint.Runtime.Environments;
+
+namespace Jint.Runtime.Debugger
+{
+    public sealed class CallFrame
+    {
+        private readonly ExecutionContext _context;
+        private readonly CallStackElement? _element;
+        private readonly Lazy<DebugScopes> _scopeChain;
+
+        internal CallFrame(CallStackElement? element, ExecutionContext context, Location location, JsValue returnValue)
+        {
+            _element = element;
+            _context = context;
+            Location = location;
+            ReturnValue = returnValue;
+
+            _scopeChain = new Lazy<DebugScopes>(() => new DebugScopes(Environment));
+        }
+
+        private LexicalEnvironment Environment => _context.LexicalEnvironment;
+
+        // TODO: CallFrameId
+        /// <summary>
+        /// Name of the function of this call frame. For global scope, this will be "(anonymous)".
+        /// </summary>
+        public string FunctionName => _element?.ToString() ?? "(anonymous)";
+
+        /// <summary>
+        /// Source location of function of this call frame.
+        /// </summary>
+        /// <remarks>For top level (global) call frames, as well as functions not defined in script, this will be null.</remarks>
+        public Location? FunctionLocation => (_element?.Function._functionDefinition?.Function as Node)?.Location;
+
+        /// <summary>
+        /// Currently executing source location in this call frame.
+        /// </summary>
+        public Location Location { get; }
+
+        /// <summary>
+        /// The scope chain of this call frame.
+        /// </summary>
+        public DebugScopes ScopeChain => _scopeChain.Value;
+
+        /// <summary>
+        /// The value of <c>this</c> in this call frame.
+        /// </summary>
+        public JsValue This
+        {
+            get
+            {
+                var environment = _context.GetThisEnvironment();
+                return environment.GetThisBinding();
+            }
+        }
+
+        /// <summary>
+        /// The return value of this call frame. Will be null for call frames that aren't at the top of the stack,
+        /// as well as if execution is not at a return point.
+        /// </summary>
+        public JsValue ReturnValue { get; }
+    }
+}

+ 42 - 0
Jint/Runtime/Debugger/DebugCallStack.cs

@@ -0,0 +1,42 @@
+using System.Collections;
+using System.Collections.Generic;
+using Esprima;
+using Jint.Native;
+using Jint.Runtime.CallStack;
+
+namespace Jint.Runtime.Debugger
+{
+    public sealed class DebugCallStack : IReadOnlyList<CallFrame>
+    {
+        private readonly List<CallFrame> _stack;
+
+        internal DebugCallStack(Engine engine, Location location, JintCallStack callStack, JsValue returnValue)
+        {
+            _stack = new List<CallFrame>(callStack.Count + 1);
+            var executionContext = engine.ExecutionContext;
+            foreach (var element in callStack.Stack)
+            {
+                _stack.Add(new CallFrame(element, executionContext, location, returnValue));
+                location = element.Location;
+                returnValue = null;
+                executionContext = element.CallingExecutionContext;
+            }
+            // Add root location
+            _stack.Add(new CallFrame(null, executionContext, location, returnValue: null));
+        }
+
+        public CallFrame this[int index] => _stack[index];
+
+        public int Count => _stack.Count;
+
+        public IEnumerator<CallFrame> GetEnumerator()
+        {
+            return _stack.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 74 - 169
Jint/Runtime/Debugger/DebugHandler.cs

@@ -1,227 +1,132 @@
-using System.Collections.Generic;
-using System.Linq;
+using System;
+using Esprima;
 using Esprima.Ast;
 using Jint.Native;
-using Jint.Native.Function;
-using Jint.Runtime.Descriptors;
-using Jint.Runtime.Environments;
-using Jint.Runtime.Interop;
 
 namespace Jint.Runtime.Debugger
 {
-    internal class DebugHandler
+    public class DebugHandler
     {
-        private readonly Stack<string> _debugCallStack;
-        private int _steppingDepth;
-        private readonly Engine _engine;
+        public delegate StepMode DebugStepDelegate(object sender, DebugInformation e);
+        public delegate StepMode BreakDelegate(object sender, DebugInformation e);
 
-        public DebugHandler(Engine engine)
+        private enum PauseType
         {
-            _engine = engine;
-            _debugCallStack = new Stack<string>();
-            _steppingDepth = int.MaxValue;
+            Step,
+            Break
         }
 
-        internal void AddToDebugCallStack(JsValue function)
-        {
-            string name = GetCalleeName(function);
+        private readonly Engine _engine;
+        private bool _paused;
+        private int _steppingDepth;
 
-            _debugCallStack.Push(name);
-        }
+        public event DebugStepDelegate Step;
+        public event BreakDelegate Break;
 
-        internal void PopDebugCallStack()
+        internal DebugHandler(Engine engine)
         {
-            if (_debugCallStack.Count > 0)
-            {
-                _debugCallStack.Pop();
-            }
+            _engine = engine;
+            _steppingDepth = int.MaxValue;
         }
 
-        private string GetCalleeName(JsValue function)
+        public BreakPointCollection BreakPoints { get; } = new BreakPointCollection();
+
+        internal void OnStep(Statement statement)
         {
-            switch (function)
+            // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
+            if (_paused)
             {
-                case DelegateWrapper _:
-                    return "(native code)";
-
-                case FunctionInstance instance:
-                    PropertyDescriptor nameDescriptor = instance.GetOwnProperty(CommonProperties.Name);
-                    JsValue nameValue = nameDescriptor != null ? instance.UnwrapJsValue(nameDescriptor) : JsString.Empty;
-                    return !nameValue.IsUndefined() ? TypeConverter.ToString(nameValue) : "(anonymous)";
-
-                default:
-                    return "(unknown)";
+                return;
             }
-        }
 
-        internal void OnStep(Statement statement)
-        {
-            BreakPoint breakpoint = _engine.BreakPoints.FirstOrDefault(breakPoint => BpTest(statement, breakPoint));
+            Location location = statement.Location;
+            BreakPoint breakpoint = BreakPoints.FindMatch(_engine, location);
 
             if (breakpoint != null)
             {
-                Break(statement);
+                Pause(PauseType.Break, statement);
             }
-            else if (_debugCallStack.Count <= _steppingDepth)
+            else if (_engine.CallStack.Count <= _steppingDepth)
             {
-                Step(statement);
+                Pause(PauseType.Step, statement);
             }
         }
 
-        private void Step(Statement statement)
-        {
-            DebugInformation info = CreateDebugInformation(statement);
-            StepMode? result = _engine.InvokeStepEvent(info);
-            HandleNewStepMode(result);
-        }
-
-        internal void Break(Statement statement)
+        internal void OnReturnPoint(Node functionBody, JsValue returnValue)
         {
-            DebugInformation info = CreateDebugInformation(statement);
-            StepMode? result = _engine.InvokeBreakEvent(info);
-            HandleNewStepMode(result);
-        }
-
-        private void HandleNewStepMode(StepMode? newStepMode)
-        {
-            if (newStepMode != null)
+            // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
+            if (_paused)
             {
-                switch (newStepMode)
-                {
-                    case StepMode.Over:
-                        // Resume stepping when we're back at this level of the call stack
-                        _steppingDepth = _debugCallStack.Count;
-                        break;
-                    case StepMode.Out:
-                        // Resume stepping when we've popped the call stack
-                        _steppingDepth = _debugCallStack.Count - 1;
-                        break;
-                    case StepMode.None:
-                        // Never step
-                        _steppingDepth = int.MinValue;
-                        break;
-                    default:
-                        // Always step
-                        _steppingDepth = int.MaxValue;
-                        break;
-                }
+                return;
             }
-        }
 
-        private bool BpTest(Statement statement, BreakPoint breakpoint)
-        {
-            if (breakpoint.Source != null)
-            {
-                if (breakpoint.Source != statement.Location.Source)
-                {
-                    return false;
-                }
-            }
+            var bodyLocation = functionBody.Location;
+            var functionBodyEnd = bodyLocation.End;
+            var location = new Location(functionBodyEnd, functionBodyEnd, bodyLocation.Source);
 
-            bool afterStart, beforeEnd;
-
-            afterStart = (breakpoint.Line == statement.Location.Start.Line &&
-                             breakpoint.Char >= statement.Location.Start.Column);
-
-            if (!afterStart)
-            {
-                return false;
-            }
+            BreakPoint breakpoint = BreakPoints.FindMatch(_engine, location);
 
-            beforeEnd = breakpoint.Line < statement.Location.End.Line
-                        || (breakpoint.Line == statement.Location.End.Line &&
-                            breakpoint.Char <= statement.Location.End.Column);
-
-            if (!beforeEnd)
+            if (breakpoint != null)
             {
-                return false;
+                Pause(PauseType.Break, statement: null, location, returnValue);
             }
-
-            if (!string.IsNullOrEmpty(breakpoint.Condition))
+            else if (_engine.CallStack.Count <= _steppingDepth)
             {
-                var completionValue = _engine.Execute(breakpoint.Condition).GetCompletionValue();
-                return ((JsBoolean) completionValue)._value;
+                Pause(PauseType.Step, statement: null, location, returnValue);
             }
-
-            return true;
         }
 
-        private DebugInformation CreateDebugInformation(Statement statement)
+        private void Pause(PauseType type, Statement statement = null, Location? location = null, JsValue returnValue = null)
         {
-            var info = new DebugInformation
+            _paused = true;
+            
+            DebugInformation info = CreateDebugInformation(statement, location ?? statement.Location, returnValue);
+            
+            StepMode? result = type switch
             {
-                CurrentStatement = statement,
-                CallStack = _debugCallStack,
-                CurrentMemoryUsage = _engine.CurrentMemoryUsage
+                // Conventionally, sender should be DebugHandler - but Engine is more useful
+                PauseType.Step => Step?.Invoke(_engine, info),
+                PauseType.Break => Break?.Invoke(_engine, info),
+                _ => throw new ArgumentException("Invalid pause type", nameof(type))
             };
-
-            info.Locals = GetLocalVariables(_engine.ExecutionContext);
-            info.Globals = GetGlobalVariables(_engine.ExecutionContext);
-
-            return info;
+            
+            _paused = false;
+            
+            HandleNewStepMode(result);
         }
 
-        private static Dictionary<string, JsValue> GetLocalVariables(ExecutionContext context)
+        internal void OnBreak(Statement statement)
         {
-            Dictionary<string, JsValue> locals = new Dictionary<string, JsValue>();
-
-            // Local variables are the union of function scope (VariableEnvironment)
-            // and any current block scope (LexicalEnvironment)
-            if (!ReferenceEquals(context.VariableEnvironment?._record, null))
-            {
-                AddRecordsFromEnvironment(context.VariableEnvironment, locals);
-            }
-            if (!ReferenceEquals(context.LexicalEnvironment?._record, null))
+            // Don't reenter if we're already paused
+            if (_paused)
             {
-                AddRecordsFromEnvironment(context.LexicalEnvironment, locals);
+                return;
             }
-            return locals;
+
+            Pause(PauseType.Break, statement);
         }
 
-        private static Dictionary<string, JsValue> GetGlobalVariables(ExecutionContext context)
+        private void HandleNewStepMode(StepMode? newStepMode)
         {
-            Dictionary<string, JsValue> globals = new Dictionary<string, JsValue>();
-            
-            // Unless we're in the global scope (_outer is null), don't include function local variables.
-            // The function local variables are in the variable environment (function scope) and any current
-            // lexical environment (block scope), which will be a "child" of that VariableEnvironment.
-            // Hence, we should only use the VariableEnvironment's outer environment for global scope. This
-            // also means that block scoped variables will never be included - they'll be listed as local variables.
-            LexicalEnvironment tempLex = context.VariableEnvironment._outer ?? context.VariableEnvironment;
-
-            while (!ReferenceEquals(tempLex?._record, null))
+            if (newStepMode != null)
             {
-                AddRecordsFromEnvironment(tempLex, globals);
-                tempLex = tempLex._outer;
+                _steppingDepth = newStepMode switch
+                {
+                    StepMode.Over => _engine.CallStack.Count,// Resume stepping when we're back at this level of the call stack
+                    StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the call stack
+                    StepMode.None => int.MinValue,// Never step
+                    _ => int.MaxValue,// Always step
+                };
             }
-            return globals;
         }
 
-        private static void AddRecordsFromEnvironment(LexicalEnvironment lex, Dictionary<string, JsValue> locals)
+        private DebugInformation CreateDebugInformation(Statement statement, Location? currentLocation, JsValue returnValue)
         {
-            var bindings = lex._record.GetAllBindingNames();
-            foreach (var binding in bindings)
-            {
-                if (!locals.ContainsKey(binding))
-                {
-                    var jsValue = lex._record.GetBindingValue(binding, false);
-
-                    switch (jsValue)
-                    {
-                        case ICallable _:
-                            // TODO: Callables aren't added - but maybe they should be.
-                            break;
-                        case null:
-                            // Uninitialized consts in scope are shown as "undefined" in e.g. Chromium debugger.
-                            // Uninitialized lets aren't displayed.
-                            // TODO: Check if null result from GetBindingValue is only true for uninitialized const/let.
-                            break;
-                        default:
-                            locals.Add(binding, jsValue);
-                            break;
-                    }
-                }
-            }
+            return new DebugInformation(
+                statement,
+                new DebugCallStack(_engine, currentLocation ?? statement.Location, _engine.CallStack, returnValue),
+                _engine.CurrentMemoryUsage
+            );
         }
     }
 }

+ 47 - 7
Jint/Runtime/Debugger/DebugInformation.cs

@@ -1,16 +1,56 @@
 using System;
-using System.Collections.Generic;
+using Esprima;
 using Esprima.Ast;
 using Jint.Native;
 
 namespace Jint.Runtime.Debugger
 {
-    public class DebugInformation : EventArgs
+    public sealed class DebugInformation : EventArgs
     {
-        public Stack<string> CallStack { get; set; }
-        public Statement CurrentStatement { get; set; }
-        public long CurrentMemoryUsage { get; set; }
-        public Dictionary<string, JsValue> Locals { get; set; }
-        public Dictionary<string, JsValue> Globals { get; set; }
+        internal DebugInformation(Statement currentStatement, DebugCallStack callStack, long currentMemoryUsage)
+        {
+            CurrentStatement = currentStatement;
+            CallStack = callStack;
+            CurrentMemoryUsage = currentMemoryUsage;
+        }
+
+        /// <summary>
+        /// The current call stack.
+        /// </summary>
+        /// <remarks>This will always include at least a call frame for the global environment.</remarks>
+        public DebugCallStack CallStack { get; set; }
+
+        /// <summary>
+        /// The Statement that will be executed on next step.
+        /// Note that this will be null when execution is at a return point.
+        /// </summary>
+        public Statement CurrentStatement { get; }
+
+        /// <summary>
+        /// The current source Location.
+        /// For return points, this starts and ends at the end of the function body.
+        /// </summary>
+        public Location Location => CurrentCallFrame.Location;
+
+        /// <summary>
+        /// Not implemented. Will always return 0.
+        /// </summary>
+        public long CurrentMemoryUsage { get; }
+
+        /// <summary>
+        /// The currently executing call frame.
+        /// </summary>
+        public CallFrame CurrentCallFrame => CallStack[0];
+
+        /// <summary>
+        /// The scope chain of the currently executing call frame.
+        /// </summary>
+        public DebugScopes CurrentScopeChain => CurrentCallFrame.ScopeChain;
+
+        /// <summary>
+        /// The return value of the currently executing call frame.
+        /// This is null if execution is not at a return point. 
+        /// </summary>
+        public JsValue ReturnValue => CurrentCallFrame.ReturnValue;
     }
 }

+ 55 - 0
Jint/Runtime/Debugger/DebugScope.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Jint.Native;
+using Jint.Runtime.Environments;
+
+namespace Jint.Runtime.Debugger
+{
+    /// <summary>
+    /// Scope information, bindings, and values for a single scope in the scope chain
+    /// </summary>
+    public sealed class DebugScope
+    {
+        private readonly EnvironmentRecord _record;
+        private readonly List<string> _bindingNames;
+
+        internal DebugScope(DebugScopeType type, EnvironmentRecord record, List<string> bindingNames, bool isTopLevel)
+        {
+            ScopeType = type;
+            _record = record;
+            _bindingNames = bindingNames;
+            IsTopLevel = isTopLevel;
+        }
+
+        /// <summary>
+        /// The type of scope. Scope types are the same as defined by Chrome devtools protocol.
+        /// </summary>
+        public DebugScopeType ScopeType { get; }
+
+        /// <summary>
+        /// For <see cref="DebugScopeType.Block">block</see> scopes, indicates whether this scope is at the top level of a containing function.
+        /// </summary>
+        /// <remarks>
+        /// Block scopes at the top level of a function are combined with Local scope in Chromium and devtools protocol.
+        /// This property facilitates implementing the same "flattening" in e.g. a UI. Because empty scopes are excluded in the scope chain,
+        /// top level cannot be determined from the scope chain order alone.
+        /// </remarks>
+        public bool IsTopLevel { get; }
+
+        /// <summary>
+        /// Names of all non-shadowed bindings in the scope.
+        /// </summary>
+        public IReadOnlyList<string> BindingNames => _bindingNames;
+
+        /// <summary>
+        /// Retrieves the value of a specific binding. Note that some bindings (e.g. uninitialized let) may return null.
+        /// </summary>
+        /// <param name="name">Binding name</param>
+        /// <returns>Value of the binding</returns>
+        public JsValue GetBindingValue(string name)
+        {
+            return _record.GetBindingValue(name, strict: false);
+        }
+    }
+}

+ 90 - 0
Jint/Runtime/Debugger/DebugScopeType.cs

@@ -0,0 +1,90 @@
+namespace Jint.Runtime.Debugger
+{
+    /// <summary>
+    /// Variable scope type.
+    /// These mirror <see href="https://chromedevtools.github.io/devtools-protocol/tot/Debugger/#type-Scope">Chrome DevTools Protocol</see>.
+    /// </summary>
+    public enum DebugScopeType
+    {
+        /// <summary>
+        /// Global scope bindings.
+        /// </summary>
+        /// <remarks>
+        /// In Jint, this scope also includes bindings that would normally be part of <see cref="Script"/>.
+        /// A scope chain will only include one scope of this type.
+        /// </remarks>
+        Global,
+
+        /// <summary>
+        /// Block scope bindings (let/const) defined at top level.
+        /// </summary>
+        /// <remarks>Not currently implemented by Jint. These bindings are instead included in <see cref="Global" />.</remarks>
+        Script,
+        
+        /// <summary>
+        /// Function local bindings.
+        /// </summary>
+        /// <remarks>
+        /// Function scoped variables.
+        /// Note that variables in outer functions are in <see cref="Closure"/> scopes.
+        /// A scope chain will only include one scope of this type.
+        /// </remarks>
+        Local,
+
+        /// <summary>
+        /// Block scoped bindings.
+        /// </summary>
+        /// <remarks>
+        /// This scope is not used for block scoped variables (let/const) declared at the top level of the <see cref="Global">global</see> scope.
+        /// Unlike Chromium V8, it *is* used as a separate scope for block scoped variables declared at the top level of a function.
+        /// A scope chain may include more than one scope of this type.
+        /// </remarks>
+        Block,
+
+        /// <summary>
+        /// Catch scope bindings.
+        /// </summary>
+        /// <remarks>
+        /// This scope only includes the argument of a <c>catch</c> clause in a <c>try/catch</c> statement.
+        /// A scope chain may include more than one scope of this type.
+        /// </remarks>
+        Catch,
+
+        /// <summary>
+        /// Function scope bindings in outer functions.
+        /// </summary>
+        /// <remarks>
+        /// Unlike Chromium V8, which will optimize variables out that aren't referenced from the inner scope,
+        /// Jint includes local variables from the outer function in this scope.
+        /// A scope chain may include more than one scope of this type.
+        /// </remarks>
+        Closure,
+
+        /// <summary>
+        /// With scope bindings.
+        /// </summary>
+        /// <remarks>
+        /// Includes the bindings created from properties of object used as argument to a <c>with</c> statement.
+        /// A scope chain may include more than one scope of this type.
+        /// </remarks>
+        With,
+
+        /// <summary>
+        /// Eval scope bindings.
+        /// </summary>
+        /// <remarks>Variables declared in an evaluated string. Not implemented.</remarks>
+        Eval,
+
+        /// <summary>
+        /// Module scope bindings.
+        /// </summary>
+        /// <remarks>Not currently implemented by Jint.</remarks>
+        Module,
+
+        /// <summary>
+        /// WebAssembly expression stack bindings.
+        /// </summary>
+        /// <remarks>Not currently implemented by Jint.</remarks>
+        WasmExpressionStack
+    }
+}

+ 115 - 0
Jint/Runtime/Debugger/DebugScopes.cs

@@ -0,0 +1,115 @@
+using Jint.Native;
+using Jint.Runtime.Environments;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Jint.Runtime.Debugger
+{
+    public sealed class DebugScopes : IReadOnlyList<DebugScope>
+    {
+        private readonly HashSet<string> _foundBindings = new HashSet<string>();
+        private readonly List<DebugScope> _scopes = new List<DebugScope>();
+
+        internal DebugScopes(LexicalEnvironment environment)
+        {
+            Populate(environment);
+        }
+
+        /// <summary>
+        /// Shortcut to Global scope
+        /// </summary>
+        public DebugScope Global { get; private set; }
+
+        /// <summary>
+        /// Shortcut to Local scope. Note that this is only present inside functions, and only includes
+        /// function scope bindings.
+        /// </summary>
+        public DebugScope Local { get; private set; }
+
+        public DebugScope this[int index] => _scopes[index];
+        public int Count => _scopes.Count;
+
+        private void Populate(LexicalEnvironment environment)
+        {
+            bool inLocalScope = true;
+            while (environment != null)
+            {
+                EnvironmentRecord record = environment._record;
+                switch (record)
+                {
+                    case GlobalEnvironmentRecord:
+                        AddScope(DebugScopeType.Global, record);
+                        break;
+                    case FunctionEnvironmentRecord:
+                        AddScope(inLocalScope ? DebugScopeType.Local : DebugScopeType.Closure, record);
+                        // We're now in closure territory
+                        inLocalScope = false;
+                        break;
+                    case ObjectEnvironmentRecord:
+                        // If an ObjectEnvironmentRecord is not a GlobalEnvironmentRecord, it's With
+                        AddScope(DebugScopeType.With, record);
+                        break;
+                    case DeclarativeEnvironmentRecord der:
+                        if (der._catchEnvironment)
+                        {
+                            AddScope(DebugScopeType.Catch, record);
+                        }
+                        else
+                        {
+                            bool isTopLevel = environment._outer?._record is FunctionEnvironmentRecord;
+                            AddScope(DebugScopeType.Block, record, isTopLevel);
+                        }
+                        break;
+                }
+
+                environment = environment._outer;
+            }
+        }
+
+        private void AddScope(DebugScopeType type, EnvironmentRecord record, bool isTopLevel = false)
+        {
+            var bindings = new List<string>();
+            PopulateBindings(bindings, record);
+
+            if (bindings.Count > 0)
+            {
+                var scope = new DebugScope(type, record, bindings, isTopLevel);
+                _scopes.Add(scope);
+                switch (type)
+                {
+                    case DebugScopeType.Global:
+                        Global = scope;
+                        break;
+                    case DebugScopeType.Local:
+                        Local = scope;
+                        break;
+                }
+            }
+        }
+
+        private void PopulateBindings(List<string> bindings, EnvironmentRecord record)
+        {
+            var bindingNames = record.GetAllBindingNames();
+
+            foreach (var name in bindingNames)
+            {
+                // Only add non-shadowed bindings
+                if (!_foundBindings.Contains(name))
+                {
+                    bindings.Add(name);
+                    _foundBindings.Add(name);
+                }
+            }
+        }
+
+        public IEnumerator<DebugScope> GetEnumerator()
+        {
+            return _scopes.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return _scopes.GetEnumerator();
+        }
+    }
+}

+ 3 - 1
Jint/Runtime/Environments/DeclarativeEnvironmentRecord.cs

@@ -12,9 +12,11 @@ namespace Jint.Runtime.Environments
     {
         internal readonly HybridDictionary<Binding> _dictionary = new HybridDictionary<Binding>();
         internal bool _hasBindings;
+        internal bool _catchEnvironment;
 
-        public DeclarativeEnvironmentRecord(Engine engine) : base(engine)
+        public DeclarativeEnvironmentRecord(Engine engine, bool catchEnvironment = false) : base(engine)
         {
+            _catchEnvironment = catchEnvironment;
         }
 
         public sealed override bool HasBinding(string name)

+ 24 - 0
Jint/Runtime/Environments/ExecutionContext.cs

@@ -22,5 +22,29 @@
         {
             return new ExecutionContext(LexicalEnvironment, variableEnvironment);
         }
+
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-getthisenvironment
+        /// </summary>
+        internal EnvironmentRecord GetThisEnvironment()
+        {
+            // The loop will always terminate because the list of environments always
+            // ends with the global environment which has a this binding.
+            var lex = LexicalEnvironment;
+            while (true)
+            {
+                var envRec = lex._record;
+                var exists = envRec.HasThisBinding();
+                if (exists)
+                {
+                    return envRec;
+                }
+
+                var outer = lex._outer;
+                lex = outer;
+            }
+        }
+
+
     }
 }

+ 2 - 2
Jint/Runtime/Environments/LexicalEnvironment.cs

@@ -58,11 +58,11 @@ namespace Jint.Runtime.Environments
             return false;
         }
 
-        public static LexicalEnvironment NewDeclarativeEnvironment(Engine engine, LexicalEnvironment outer = null)
+        public static LexicalEnvironment NewDeclarativeEnvironment(Engine engine, LexicalEnvironment outer = null, bool catchEnvironment = false)
         {
             var environment = new LexicalEnvironment(engine, null, null)
             {
-                _record = new DeclarativeEnvironmentRecord(engine),
+                _record = new DeclarativeEnvironmentRecord(engine, catchEnvironment),
                 _outer = outer
             };
 

+ 2 - 2
Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs

@@ -76,7 +76,7 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         private object SuperCall()
         {
-            var thisEnvironment = (FunctionEnvironmentRecord) _engine.GetThisEnvironment();
+            var thisEnvironment = (FunctionEnvironmentRecord) _engine.ExecutionContext.GetThisEnvironment();
             var newTarget = _engine.GetNewTarget(thisEnvironment);
             var func = GetSuperConstructor(thisEnvironment);
             if (!func.IsConstructor)
@@ -86,7 +86,7 @@ namespace Jint.Runtime.Interpreter.Expressions
 
             var argList = ArgumentListEvaluation();
             var result = ((IConstructor) func).Construct(argList, newTarget);
-            var thisER = (FunctionEnvironmentRecord) _engine.GetThisEnvironment();
+            var thisER = (FunctionEnvironmentRecord) _engine.ExecutionContext.GetThisEnvironment();
             return thisER.BindThisValue(result);
         }
 

+ 1 - 1
Jint/Runtime/Interpreter/Expressions/JintMemberExpression.cs

@@ -65,7 +65,7 @@ namespace Jint.Runtime.Interpreter.Expressions
             }
             else if (_objectExpression is JintSuperExpression)
             {
-                var env = (FunctionEnvironmentRecord) _engine.GetThisEnvironment();
+                var env = (FunctionEnvironmentRecord) _engine.ExecutionContext.GetThisEnvironment();
                 actualThis = env.GetThisBinding();
                 baseValue = env.GetSuperBase();
             }

+ 1 - 1
Jint/Runtime/Interpreter/Expressions/JintSuperExpression.cs

@@ -11,7 +11,7 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         protected override object EvaluateInternal()
         {
-            var envRec = (FunctionEnvironmentRecord) _engine.GetThisEnvironment();
+            var envRec = (FunctionEnvironmentRecord) _engine.ExecutionContext.GetThisEnvironment();
             var activeFunction = envRec._functionObject;
             var superConstructor = activeFunction.GetPrototypeOf();
             return superConstructor;

+ 3 - 1
Jint/Runtime/Interpreter/JintStatementList.cs

@@ -37,7 +37,8 @@ namespace Jint.Runtime.Interpreter
                 jintStatements[i] = new Pair
                 {
                     Statement = JintStatement.Build(_engine, esprimaStatement),
-                    Value = JintStatement.FastResolve(esprimaStatement)
+                    // When in debug mode, don't do FastResolve: Stepping requires each statement to be actually executed.
+                    Value = _engine._isDebugMode ? null : JintStatement.FastResolve(esprimaStatement)
                 };
             }
             _jintStatements = jintStatements;
@@ -69,6 +70,7 @@ namespace Jint.Runtime.Interpreter
                 {
                     s = pair.Statement;
                     c = pair.Value ?? s.Execute();
+
                     if (c.Type != CompletionType.Normal)
                     {
                         return new Completion(

+ 1 - 1
Jint/Runtime/Interpreter/Statements/JintDebuggerStatement.cs

@@ -22,7 +22,7 @@ namespace Jint.Runtime.Interpreter.Statements
                     System.Diagnostics.Debugger.Break();
                     break;
                 case DebuggerStatementHandling.Script:
-                    _engine.DebugHandler?.Break(_statement);
+                    _engine.DebugHandler?.OnBreak(_statement);
                     break;
                 case DebuggerStatementHandling.Ignore:
                     break;

+ 1 - 1
Jint/Runtime/Interpreter/Statements/JintTryStatement.cs

@@ -49,7 +49,7 @@ namespace Jint.Runtime.Interpreter.Statements
                     }
                     var c = b.Value;
                     var oldEnv = _engine.ExecutionContext.LexicalEnvironment;
-                    var catchEnv = LexicalEnvironment.NewDeclarativeEnvironment(_engine, oldEnv);
+                    var catchEnv = LexicalEnvironment.NewDeclarativeEnvironment(_engine, oldEnv, catchEnvironment: true);
                     var catchEnvRecord = (DeclarativeEnvironmentRecord) catchEnv._record;
                     catchEnvRecord.CreateMutableBindingAndInitialize(_catchParamName, canBeDeleted: false, c);