Browse Source

Debugger improvements 4b (#1113)

* Added Parsed event to engine

* Split Global scope into Global (OER) and Script (DER) in debugger

BindingObject property on Global and With scopes (mainly for devtools for now, but may be useful for VSCode DebugAdapter)

* New BreakPoint implementation

Only allows a single breakpoint at a given location (source/line/column) - hence, API is now "Set" instead of "Add".
Requires the breakpoint to be set at *exactly* the correct line/column (this allows faster matching, column breakpoints, and more).
Fixed tests broken by addition of Script scope and new breakpoint semantics.
Breakpoints still need work on the API (and probably implementation) side.
Additional BreakPoint tests

* Let Scope resolve BindingObject

* Slight cleanup

* BreakLocation to record

* Sealed classes

* Fixed inspection of uninitialized block scoped bindings

* Member order

* Silently catch errors in breakpoint conditions

Test case for failing breakpoint conditions (currently they, and other evaluations during paused state) reset the call stack (and probably other things).
Documentation of SourceParsedEventArgs

* Distinguish between breakpoints and debugger statements

Renamed DebugHandler OnBreak event to clarify that it only handles debugger statements.
Added PauseType (Break/Step/DebuggerStatement) to DebugInformation to allow debuggers to distinguish between breaks and debugger statements.

* DebugHandler-specific script evaluation

* Fix: DebugHandler reentry when evaluating conditional breakpoints

* Stepping: Skip block statements, include loop expressions

Removed unused Source property from SourceParsedEventArgs

* Include relevant BreakPoint in DebugInformation

Removed redundant BreakPoint constructors

* DebugHandler Support for evaluating (preparsed) AST

* More logical Break/Step events

Option for initial StepMode - it's now StepMode.None by default to properly handle breakpoints from the start. Previously, it was StepMode.Into
Updated tests to reflect new initial StepMode

* Cleanup

* Don't react to debugger statement when stepping

Error messages for a few TypeErrors

* DebugHandler.CurrentLocation

DebugHandler cleanup and more documentation

* Evaluation context guard for DebugHandler Evaluate

* Tests for new DebugHandler functionality

* Removed Engine.Parsed event for now

Currently unused, and a better approach for getting AST for scripts is needed, in order to support Modules etc.

* Changes after review

#nullable enable and namespace statements on new files
Slight improvement of DebugEvaluationException
Got rid of DebugHandler references to _engine._isDebugMode (use context)
A few new tests

* Minor variable renaming for test clarity
Jither 3 years ago
parent
commit
3fa1f8343c
34 changed files with 1258 additions and 264 deletions
  1. 295 11
      Jint.Tests/Runtime/Debugger/BreakPointTests.cs
  2. 2 2
      Jint.Tests/Runtime/Debugger/CallStackTests.cs
  3. 2 2
      Jint.Tests/Runtime/Debugger/DebugHandlerTests.cs
  4. 126 0
      Jint.Tests/Runtime/Debugger/EvaluateTests.cs
  5. 62 14
      Jint.Tests/Runtime/Debugger/ScopeTests.cs
  6. 169 0
      Jint.Tests/Runtime/Debugger/StepFlowTests.cs
  7. 48 16
      Jint.Tests/Runtime/Debugger/StepModeTests.cs
  8. 8 28
      Jint.Tests/Runtime/Debugger/TestHelpers.cs
  9. 18 18
      Jint.Tests/Runtime/EngineTests.cs
  10. 8 3
      Jint/Engine.cs
  11. 1 1
      Jint/Native/Array/ArrayConstructor.cs
  12. 2 2
      Jint/Native/Function/ScriptFunctionInstance.cs
  13. 1 1
      Jint/Native/Symbol/SymbolConstructor.cs
  14. 9 0
      Jint/Options.Extensions.cs
  15. 5 0
      Jint/Options.cs
  16. 30 0
      Jint/Runtime/Debugger/BreakLocation.cs
  17. 16 27
      Jint/Runtime/Debugger/BreakPoint.cs
  18. 67 55
      Jint/Runtime/Debugger/BreakPointCollection.cs
  19. 19 0
      Jint/Runtime/Debugger/DebugEvaluationException.cs
  20. 168 58
      Jint/Runtime/Debugger/DebugHandler.cs
  21. 28 6
      Jint/Runtime/Debugger/DebugInformation.cs
  22. 25 3
      Jint/Runtime/Debugger/DebugScope.cs
  23. 4 3
      Jint/Runtime/Debugger/DebugScopeType.cs
  24. 14 6
      Jint/Runtime/Debugger/DebugScopes.cs
  25. 51 0
      Jint/Runtime/Debugger/OptionalSourceBreakLocationEqualityComparer.cs
  26. 12 0
      Jint/Runtime/Environments/DeclarativeEnvironmentRecord.cs
  27. 11 0
      Jint/Runtime/Environments/EnvironmentRecord.cs
  28. 10 2
      Jint/Runtime/Environments/GlobalEnvironmentRecord.cs
  29. 13 0
      Jint/Runtime/Environments/ObjectEnvironmentRecord.cs
  30. 1 1
      Jint/Runtime/Interpreter/Statements/JintDebuggerStatement.cs
  31. 6 1
      Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs
  32. 6 1
      Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
  33. 15 2
      Jint/Runtime/Interpreter/Statements/JintForStatement.cs
  34. 6 1
      Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs

+ 295 - 11
Jint.Tests/Runtime/Debugger/BreakPointTests.cs

@@ -1,12 +1,117 @@
 using Esprima;
 using Esprima;
 using Jint.Runtime.Debugger;
 using Jint.Runtime.Debugger;
+using System.Linq;
 using Xunit;
 using Xunit;
 
 
 namespace Jint.Tests.Runtime.Debugger
 namespace Jint.Tests.Runtime.Debugger
 {
 {
     public class BreakPointTests
     public class BreakPointTests
     {
     {
-       [Fact]
+        [Fact]
+        public void BreakLocationsCompareEqualityByValue()
+        {
+            var loc1 = new BreakLocation(42, 23);
+            var loc2 = new BreakLocation(42, 23);
+            var loc3 = new BreakLocation(17, 7);
+
+            Assert.Equal(loc1, loc2);
+            Assert.True(loc1 == loc2);
+            Assert.True(loc2 != loc3);
+            Assert.False(loc1 != loc2);
+            Assert.False(loc2 == loc3);
+        }
+
+        [Fact]
+        public void BreakLocationsWithSourceCompareEqualityByValue()
+        {
+            var loc1 = new BreakLocation("script1", 42, 23);
+            var loc2 = new BreakLocation("script1", 42, 23);
+            var loc3 = new BreakLocation("script2", 42, 23);
+
+            Assert.Equal(loc1, loc2);
+            Assert.True(loc1 == loc2);
+            Assert.True(loc2 != loc3);
+            Assert.False(loc1 != loc2);
+            Assert.False(loc2 == loc3);
+        }
+
+        [Fact]
+        public void BreakLocationsOptionalSourceEqualityComparer()
+        {
+            var script1 = new BreakLocation("script1", 42, 23);
+            var script2 = new BreakLocation("script2", 42, 23);
+            var script2b = new BreakLocation("script2", 44, 23);
+            var any = new BreakLocation(null, 42, 23);
+
+            var comparer = new OptionalSourceBreakLocationEqualityComparer();
+            Assert.True(comparer.Equals(script1, any));
+            Assert.True(comparer.Equals(script2, any));
+            Assert.False(comparer.Equals(script1, script2));
+            Assert.False(comparer.Equals(script2, script2b));
+            Assert.Equal(comparer.GetHashCode(script1), comparer.GetHashCode(any));
+            Assert.Equal(comparer.GetHashCode(script1), comparer.GetHashCode(script2));
+            Assert.NotEqual(comparer.GetHashCode(script2), comparer.GetHashCode(script2b));
+        }
+
+        [Fact]
+        public void BreakPointReplacesPreviousBreakPoint()
+        {
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
+            Assert.Collection(engine.DebugHandler.BreakPoints,
+                breakPoint =>
+                {
+                    Assert.Equal(4, breakPoint.Location.Line);
+                    Assert.Equal(5, breakPoint.Location.Column);
+                    Assert.Equal("i === 1", breakPoint.Condition);
+                });
+
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5));
+            Assert.Collection(engine.DebugHandler.BreakPoints,
+                breakPoint =>
+                {
+                    Assert.Equal(4, breakPoint.Location.Line);
+                    Assert.Equal(5, breakPoint.Location.Column);
+                    Assert.Equal(null, breakPoint.Condition);
+                });
+        }
+
+        [Fact]
+        public void BreakPointRemovesBasedOnLocationEquality()
+        {
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 6, "j === 2"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(10, 7, "x > 5"));
+            Assert.Equal(3, engine.DebugHandler.BreakPoints.Count);
+
+            engine.DebugHandler.BreakPoints.RemoveAt(new BreakLocation(null, 4, 5));
+            engine.DebugHandler.BreakPoints.RemoveAt(new BreakLocation(null, 10, 7));
+
+            Assert.Collection(engine.DebugHandler.BreakPoints,
+                breakPoint =>
+                {
+                    Assert.Equal(5, breakPoint.Location.Line);
+                    Assert.Equal(6, breakPoint.Location.Column);
+                    Assert.Equal("j === 2", breakPoint.Condition);
+                });
+        }
+
+        [Fact]
+        public void BreakPointContainsBasedOnLocationEquality()
+        {
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5, "i === 1"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 6, "j === 2"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(10, 7, "x > 5"));
+            Assert.True(engine.DebugHandler.BreakPoints.Contains(new BreakLocation(null, 5, 6)));
+            Assert.False(engine.DebugHandler.BreakPoints.Contains(new BreakLocation(null, 8, 9)));
+        }
+
+        [Fact]
         public void BreakPointBreaksAtPosition()
         public void BreakPointBreaksAtPosition()
         {
         {
             string script = @"let x = 1, y = 2;
             string script = @"let x = 1, y = 2;
@@ -20,13 +125,13 @@ x++; y *= 2;
             bool didBreak = false;
             bool didBreak = false;
             engine.DebugHandler.Break += (sender, info) =>
             engine.DebugHandler.Break += (sender, info) =>
             {
             {
-                Assert.Equal(4, info.CurrentStatement.Location.Start.Line);
-                Assert.Equal(5, info.CurrentStatement.Location.Start.Column);
+                Assert.Equal(4, info.Location.Start.Line);
+                Assert.Equal(5, info.Location.Start.Column);
                 didBreak = true;
                 didBreak = true;
                 return StepMode.None;
                 return StepMode.None;
             };
             };
 
 
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 5));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 5));
             engine.Execute(script);
             engine.Execute(script);
             Assert.True(didBreak);
             Assert.True(didBreak);
         }
         }
@@ -50,14 +155,14 @@ test(z);";
 
 
             var engine = new Engine(options => { options.DebugMode(); });
             var engine = new Engine(options => { options.DebugMode(); });
             
             
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint("script2", 3, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint("script2", 3, 0));
 
 
             bool didBreak = false;
             bool didBreak = false;
             engine.DebugHandler.Break += (sender, info) =>
             engine.DebugHandler.Break += (sender, info) =>
             {
             {
-                Assert.Equal("script2", info.CurrentStatement.Location.Source);
-                Assert.Equal(3, info.CurrentStatement.Location.Start.Line);
-                Assert.Equal(0, info.CurrentStatement.Location.Start.Column);
+                Assert.Equal("script2", info.Location.Source);
+                Assert.Equal(3, info.Location.Start.Line);
+                Assert.Equal(0, info.Location.Start.Column);
                 didBreak = true;
                 didBreak = true;
                 return StepMode.None;
                 return StepMode.None;
             };
             };
@@ -99,6 +204,81 @@ debugger;
             Assert.True(didBreak);
             Assert.True(didBreak);
         }
         }
 
 
+        [Fact]
+        public void DebuggerStatementDoesNotTriggerBreakWhenStepping()
+        {
+            string script = @"'dummy';
+debugger;
+'dummy';";
+
+            var engine = new Engine(options => options
+                .DebugMode()
+                .DebuggerStatementHandling(DebuggerStatementHandling.Script)
+                .InitialStepMode(StepMode.Into));
+
+            bool didBreak = false;
+            int stepCount = 0;
+            engine.DebugHandler.Break += (sender, info) =>
+            {
+                didBreak = true;
+                return StepMode.None;
+            };
+
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                stepCount++;
+                return StepMode.Into;
+            };
+
+            engine.Execute(script);
+            Assert.Equal(3, stepCount);
+            Assert.False(didBreak);
+        }
+
+        [Fact]
+        public void BreakPointDoesNotTriggerBreakWhenStepping()
+        {
+            string script = @"
+'first breakpoint';
+'dummy';
+'second breakpoint';";
+
+            var engine = new Engine(options => options
+                .DebugMode()
+                .InitialStepMode(StepMode.Into));
+
+            bool didStep = true;
+            bool didBreak = true;
+
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(2, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 0));
+
+            engine.DebugHandler.Break += (sender, info) =>
+            {
+                didBreak = true;
+                // first breakpoint shouldn't cause us to get here, because we're stepping,
+                // but when we reach the second, we're running:
+                Assert.True(TestHelpers.ReachedLiteral(info, "second breakpoint"));
+                return StepMode.None;
+            };
+
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                didStep = true;
+                if (TestHelpers.ReachedLiteral(info, "first breakpoint"))
+                {
+                    // Run from here
+                    return StepMode.None;
+                }
+                return StepMode.Into;
+            };
+
+            engine.Execute(script);
+
+            Assert.True(didStep);
+            Assert.True(didBreak);
+        }
+
         [Fact(Skip = "Non-source breakpoint is triggered before Statement, while debugger statement is now triggered by ExecuteInternal")]
         [Fact(Skip = "Non-source breakpoint is triggered before Statement, while debugger statement is now triggered by ExecuteInternal")]
         public void DebuggerStatementAndBreakpointTriggerSingleBreak()
         public void DebuggerStatementAndBreakpointTriggerSingleBreak()
         {
         {
@@ -110,7 +290,7 @@ debugger;
                 .DebugMode()
                 .DebugMode()
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
 
 
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(2, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(2, 0));
 
 
             int breakTriggered = 0;
             int breakTriggered = 0;
             engine.DebugHandler.Break += (sender, info) =>
             engine.DebugHandler.Break += (sender, info) =>
@@ -138,8 +318,8 @@ test();";
 
 
             var engine = new Engine(options => options.DebugMode());
             var engine = new Engine(options => options.DebugMode());
 
 
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 0));
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(6, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(6, 0));
 
 
             int step = 0;
             int step = 0;
             engine.DebugHandler.Break += (sender, info) =>
             engine.DebugHandler.Break += (sender, info) =>
@@ -160,5 +340,109 @@ test();";
 
 
             Assert.Equal(2, step);
             Assert.Equal(2, step);
         }
         }
+
+        [Fact]
+        public void ErrorInConditionalBreakpointLeavesCallStackAlone()
+        {
+            string script = @"
+function foo()
+{
+let x = 0;
+'before breakpoint';
+'breakpoint 1 here';
+'breakpoint 2 here';
+'after breakpoint';
+}
+
+foo();
+";
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));
+
+            int stepsReached = 0;
+            int breakpointsReached = 0;
+
+            // This breakpoint will be hit:
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(6, 0, "x == 0"));
+            // This condition is an error (y is not defined). DebugHandler will
+            // treat it as an unmatched breakpoint:
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(7, 0, "y == 0"));
+
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                if (info.ReachedLiteral("before breakpoint"))
+                {
+                    Assert.Equal(1, engine.CallStack.Count);
+                    stepsReached++;
+                    return StepMode.None;
+                }
+                else if (info.ReachedLiteral("after breakpoint"))
+                {
+                    Assert.Equal(1, engine.CallStack.Count);
+                    stepsReached++;
+                    return StepMode.None;
+                }
+                return StepMode.Into;
+            };
+
+            engine.DebugHandler.Break += (sender, info) =>
+            {
+                breakpointsReached++;
+                return StepMode.Into;
+            };
+
+            engine.Execute(script);
+
+            Assert.Equal(1, breakpointsReached);
+            Assert.Equal(2, stepsReached);
+        }
+
+        private class SimpleHitConditionBreakPoint : BreakPoint
+        {
+            public SimpleHitConditionBreakPoint(int line, int column, string condition = null,
+                int? hitCondition = null) : base(line, column, condition)
+            {
+                HitCondition = hitCondition;
+            }
+
+            public int HitCount { get; set; }
+            public int? HitCondition { get; set; }
+        }
+
+        [Fact]
+        public void BreakPointCanBeExtended()
+        {
+            // More of a documentation than a required test, this shows the usefulness of BreakPoint being
+            // extensible - as a test, at least it ensures that it is.
+            var script = @"
+for (let i = 0; i < 10; i++)
+{
+    'breakpoint';
+}
+";
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.None));
+
+            engine.DebugHandler.BreakPoints.Set(
+                new SimpleHitConditionBreakPoint(4, 4, condition: null, hitCondition: 5));
+
+            int numberOfBreaks = 0;
+            engine.DebugHandler.Break += (sender, info) =>
+            {
+                Assert.True(info.ReachedLiteral("breakpoint"));
+                var extendedBreakPoint = Assert.IsType<SimpleHitConditionBreakPoint>(info.BreakPoint);
+                extendedBreakPoint.HitCount++;
+                if (extendedBreakPoint.HitCount == extendedBreakPoint.HitCondition)
+                {
+                    // Here is where we would normally pause the execution.
+                    // the breakpoint is hit for the fifth time, when i is 4 (off by one)
+                    Assert.Equal(4, info.CurrentScopeChain[0].GetBindingValue("i").AsInteger());
+                    numberOfBreaks++;
+                }
+                return StepMode.None;
+            };
+
+            engine.Execute(script);
+
+            Assert.Equal(1, numberOfBreaks);
+        }
     }
     }
 }
 }

+ 2 - 2
Jint.Tests/Runtime/Debugger/CallStackTests.cs

@@ -106,7 +106,7 @@ bar()";
 
 
             foo();";
             foo();";
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));
 
 
             bool atReturn = false;
             bool atReturn = false;
             bool didCheckReturn = false;
             bool didCheckReturn = false;
@@ -121,7 +121,7 @@ bar()";
                     atReturn = false;
                     atReturn = false;
                 }
                 }
 
 
-                if (info.CurrentStatement is ReturnStatement)
+                if (info.CurrentNode is ReturnStatement)
                 {
                 {
                     // Step one further, and we should have the return value
                     // Step one further, and we should have the return value
                     atReturn = true;
                     atReturn = true;

+ 2 - 2
Jint.Tests/Runtime/Debugger/DebugHandlerTests.cs

@@ -16,11 +16,11 @@ namespace Jint.Tests.Runtime.Debugger
             // reentrance in a multithreaded environment (e.g. using ManualResetEvent(Slim)) would cause
             // reentrance in a multithreaded environment (e.g. using ManualResetEvent(Slim)) would cause
             // a deadlock.
             // a deadlock.
             string script = @"
             string script = @"
-                const obj = { get name() { 'fail'; return 'Smith'; } };
+                var obj = { get name() { 'fail'; return 'Smith'; } };
                 'target';
                 'target';
             ";
             ";
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));
 
 
             bool didPropertyAccess = false;
             bool didPropertyAccess = false;
 
 

+ 126 - 0
Jint.Tests/Runtime/Debugger/EvaluateTests.cs

@@ -0,0 +1,126 @@
+using Esprima;
+using Jint.Native;
+using Jint.Runtime;
+using Jint.Runtime.Debugger;
+using Xunit;
+
+namespace Jint.Tests.Runtime.Debugger
+{
+    public class EvaluateTests
+    {
+        [Fact]
+        public void EvalutesInCurrentContext()
+        {
+            var script = @"
+            function test(x)
+            {
+                x *= 10;
+                debugger;
+            }
+
+            test(5);
+            ";
+
+            TestHelpers.TestAtBreak(script, (engine, info) =>
+            {
+                var evaluated = engine.DebugHandler.Evaluate("x");
+                Assert.IsType<JsNumber>(evaluated);
+                Assert.Equal(50, evaluated.AsNumber());
+            });
+        }
+
+        [Fact]
+        public void ThrowsIfNoCurrentContext()
+        {
+            var engine = new Engine(options => options.DebugMode());
+            var exception = Assert.Throws<DebugEvaluationException>(() => engine.DebugHandler.Evaluate("let x = 1;"));
+            Assert.Null(exception.InnerException); // Not a JavaScript or parser exception
+        }
+
+        [Fact]
+        public void ThrowsOnRuntimeError()
+        {
+            var script = @"
+            function test(x)
+            {
+                x *= 10;
+                debugger;
+            }
+
+            test(5);
+            ";
+
+            TestHelpers.TestAtBreak(script, (engine, info) =>
+            {
+                var exception = Assert.Throws<DebugEvaluationException>(() => engine.DebugHandler.Evaluate("y"));
+                Assert.IsType<JavaScriptException>(exception.InnerException);
+            });
+        }
+
+        [Fact]
+        public void ThrowsOnExecutionError()
+        {
+            var script = @"
+            function test(x)
+            {
+                x *= 10;
+                debugger;
+            }
+
+            test(5);
+            ";
+
+            TestHelpers.TestAtBreak(script, (engine, info) =>
+            {
+                var exception = Assert.Throws<DebugEvaluationException>(() =>
+                    engine.DebugHandler.Evaluate("this is a syntax error"));
+                Assert.IsType<ParserException>(exception.InnerException);
+            });
+        }
+
+        [Fact]
+        public void RestoresStackAfterEvaluation()
+        {
+            var script = @"
+            function throws()
+            {
+                throw new Error('Take this!');
+            }
+
+            function test(x)
+            {
+                x *= 10;
+                debugger;
+            }
+
+            test(5);
+            ";
+
+            TestHelpers.TestAtBreak(script, (engine, info) =>
+            {
+                Assert.Equal(1, engine.CallStack.Count);
+                var frameBefore = engine.CallStack.Stack[0];
+
+                Assert.Throws<DebugEvaluationException>(() => engine.DebugHandler.Evaluate("throws()"));
+                Assert.Equal(1, engine.CallStack.Count);
+                var frameAfter = engine.CallStack.Stack[0];
+                // Stack frames and some of their properties are structs - can't check reference equality
+                // Besides, even if we could, it would be no guarantee. Neither is the following, but it'll do for now.
+                Assert.Equal(frameBefore.CallingExecutionContext.Function,
+                    frameAfter.CallingExecutionContext.Function);
+                Assert.Equal(frameBefore.CallingExecutionContext.LexicalEnvironment,
+                    frameAfter.CallingExecutionContext.LexicalEnvironment);
+                Assert.Equal(frameBefore.CallingExecutionContext.PrivateEnvironment,
+                    frameAfter.CallingExecutionContext.PrivateEnvironment);
+                Assert.Equal(frameBefore.CallingExecutionContext.VariableEnvironment,
+                    frameAfter.CallingExecutionContext.VariableEnvironment);
+                Assert.Equal(frameBefore.CallingExecutionContext.Realm, frameAfter.CallingExecutionContext.Realm);
+
+                Assert.Equal(frameBefore.Arguments, frameAfter.Arguments);
+                Assert.Equal(frameBefore.Expression, frameAfter.Expression);
+                Assert.Equal(frameBefore.Location, frameAfter.Location);
+                Assert.Equal(frameBefore.Function, frameAfter.Function);
+            });
+        }
+    }
+}

+ 62 - 14
Jint.Tests/Runtime/Debugger/ScopeTests.cs

@@ -30,7 +30,45 @@ namespace Jint.Tests.Runtime.Debugger
         }
         }
 
 
         [Fact]
         [Fact]
-        public void GlobalScopeIncludesGlobalConst()
+        public void AllowsInspectionOfUninitializedGlobalBindings()
+        {
+            string script = @"
+                debugger;
+                const globalConstant = 'test';
+                let globalLet = 'test';
+            ";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                // Uninitialized global block scoped ("script scoped") bindings return null (and, just as importantly, don't throw):
+                Assert.Null(info.CurrentScopeChain[0].GetBindingValue("globalConstant"));
+                Assert.Null(info.CurrentScopeChain[0].GetBindingValue("globalLet"));
+            });
+        }
+
+        [Fact]
+        public void AllowsInspectionOfUninitializedBlockBindings()
+        {
+            string script = @"
+                function test()
+                {
+                    debugger;
+                    const globalConstant = 'test';
+                    let globalLet = 'test';
+                }
+                test();
+            ";
+
+            TestHelpers.TestAtBreak(script, info =>
+            {
+                // Uninitialized block scoped bindings return null (and, just as importantly, don't throw):
+                Assert.Null(info.CurrentScopeChain[0].GetBindingValue("globalConstant"));
+                Assert.Null(info.CurrentScopeChain[0].GetBindingValue("globalLet"));
+            });
+        }
+
+        [Fact]
+        public void ScriptScopeIncludesGlobalConst()
         {
         {
             string script = @"
             string script = @"
                 const globalConstant = 'test';
                 const globalConstant = 'test';
@@ -39,13 +77,13 @@ namespace Jint.Tests.Runtime.Debugger
 
 
             TestHelpers.TestAtBreak(script, info =>
             TestHelpers.TestAtBreak(script, info =>
             {
             {
-                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalConstant", DebugScopeType.Global);
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalConstant", DebugScopeType.Script);
                 Assert.Equal("test", value.AsString());
                 Assert.Equal("test", value.AsString());
             });
             });
         }
         }
 
 
         [Fact]
         [Fact]
-        public void GlobalScopeIncludesGlobalLet()
+        public void ScriptScopeIncludesGlobalLet()
         {
         {
             string script = @"
             string script = @"
                 let globalLet = 'test';
                 let globalLet = 'test';
@@ -53,7 +91,7 @@ namespace Jint.Tests.Runtime.Debugger
 
 
             TestHelpers.TestAtBreak(script, info =>
             TestHelpers.TestAtBreak(script, info =>
             {
             {
-                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalLet", DebugScopeType.Global);
+                var value = AssertOnlyScopeContains(info.CurrentScopeChain, "globalLet", DebugScopeType.Script);
                 Assert.Equal("test", value.AsString());
                 Assert.Equal("test", value.AsString());
             });
             });
         }
         }
@@ -237,7 +275,8 @@ namespace Jint.Tests.Runtime.Debugger
             {
             {
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "x", "y", "z", "add"));
+                    scope => AssertScope(scope, DebugScopeType.Script, "x", "y", "z"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "add"));
             });
             });
         }
         }
 
 
@@ -263,7 +302,8 @@ namespace Jint.Tests.Runtime.Debugger
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a"),
                     scope => AssertScope(scope, DebugScopeType.Closure, "b", "power"), // a, this, arguments shadowed by local
                     scope => AssertScope(scope, DebugScopeType.Closure, "b", "power"), // a, this, arguments shadowed by local
-                    scope => AssertScope(scope, DebugScopeType.Global, "x", "y", "z", "add"));
+                    scope => AssertScope(scope, DebugScopeType.Script, "x", "y", "z"),
+                    scope => AssertScope(scope, DebugScopeType.Global, "add"));
             });
             });
         }
         }
 
 
@@ -289,7 +329,8 @@ namespace Jint.Tests.Runtime.Debugger
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "x", "z", "add")); // y shadowed
+                    scope => AssertScope(scope, DebugScopeType.Script, "x", "z"), // y shadowed
+                    scope => AssertScope(scope, DebugScopeType.Global, "add"));
             });
             });
         }
         }
 
 
@@ -320,7 +361,8 @@ namespace Jint.Tests.Runtime.Debugger
                     scope => AssertScope(scope, DebugScopeType.Block, "x"),
                     scope => AssertScope(scope, DebugScopeType.Block, "x"),
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
                     scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "b"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "z", "add")); // x, y shadowed
+                    scope => AssertScope(scope, DebugScopeType.Script, "z"), // x, y shadowed
+                    scope => AssertScope(scope, DebugScopeType.Global, "add"));
             });
             });
         }
         }
 
 
@@ -368,7 +410,8 @@ namespace Jint.Tests.Runtime.Debugger
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Block, "x"),
                     scope => AssertScope(scope, DebugScopeType.Block, "x"),
                     scope => AssertScope(scope, DebugScopeType.With, "a", "b"),
                     scope => AssertScope(scope, DebugScopeType.With, "a", "b"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "obj"));
+                    scope => AssertScope(scope, DebugScopeType.Script, "obj"),
+                    scope => AssertScope(scope, DebugScopeType.Global));
             });
             });
         }
         }
 
 
@@ -392,7 +435,8 @@ namespace Jint.Tests.Runtime.Debugger
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Block, "z"),
                     scope => AssertScope(scope, DebugScopeType.Block, "z"),
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
                     scope => AssertScope(scope, DebugScopeType.Block, "y"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "x"));
+                    scope => AssertScope(scope, DebugScopeType.Script, "x"),
+                    scope => AssertScope(scope, DebugScopeType.Global));
             });
             });
         }
         }
 
 
@@ -414,7 +458,8 @@ namespace Jint.Tests.Runtime.Debugger
             {
             {
                 Assert.Collection(info.CurrentScopeChain,
                 Assert.Collection(info.CurrentScopeChain,
                     scope => AssertScope(scope, DebugScopeType.Block, "z"),
                     scope => AssertScope(scope, DebugScopeType.Block, "z"),
-                    scope => AssertScope(scope, DebugScopeType.Global, "x"));
+                    scope => AssertScope(scope, DebugScopeType.Script, "x"),
+                    scope => AssertScope(scope, DebugScopeType.Global));
             });
             });
         }
         }
 
 
@@ -441,16 +486,19 @@ namespace Jint.Tests.Runtime.Debugger
                     frame => Assert.Collection(frame.ScopeChain,
                     frame => Assert.Collection(frame.ScopeChain,
                         // in foo()
                         // in foo()
                         scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "c"),
                         scope => AssertScope(scope, DebugScopeType.Local, "arguments", "a", "c"),
-                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                        scope => AssertScope(scope, DebugScopeType.Script, "x"),
+                        scope => AssertScope(scope, DebugScopeType.Global, "foo", "bar")
                     ),
                     ),
                     frame => Assert.Collection(frame.ScopeChain,
                     frame => Assert.Collection(frame.ScopeChain,
                         // in bar()
                         // in bar()
                         scope => AssertScope(scope, DebugScopeType.Local, "arguments", "b"),
                         scope => AssertScope(scope, DebugScopeType.Local, "arguments", "b"),
-                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                        scope => AssertScope(scope, DebugScopeType.Script, "x"),
+                        scope => AssertScope(scope, DebugScopeType.Global, "foo", "bar")
                     ),
                     ),
                     frame => Assert.Collection(frame.ScopeChain,
                     frame => Assert.Collection(frame.ScopeChain,
                         // in global
                         // in global
-                        scope => AssertScope(scope, DebugScopeType.Global, "x", "foo", "bar")
+                        scope => AssertScope(scope, DebugScopeType.Script, "x"),
+                        scope => AssertScope(scope, DebugScopeType.Global, "foo", "bar")
                     )
                     )
                 );
                 );
             });
             });

+ 169 - 0
Jint.Tests/Runtime/Debugger/StepFlowTests.cs

@@ -0,0 +1,169 @@
+using System.Collections.Generic;
+using Esprima.Ast;
+using Jint.Runtime.Debugger;
+using Xunit;
+
+namespace Jint.Tests.Runtime.Debugger
+{
+    public class StepFlowTests
+    {
+        private List<Node> CollectStepNodes(string script)
+        {
+            var engine = new Engine(options => options
+                .DebugMode()
+                .InitialStepMode(StepMode.Into));
+
+            var nodes = new List<Node>();
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                nodes.Add(info.CurrentNode);
+                return StepMode.Into;
+            };
+
+            engine.Execute(script);
+
+            return nodes;
+        }
+
+        [Fact]
+        public void StepsThroughWhileLoop()
+        {
+            string script = @"
+                let x = 0;
+                while (x < 2)
+                {
+                    x++;
+                }
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<VariableDeclaration>(node), // let x = 0;
+                node => Assert.IsType<WhileStatement>(node),      // while ...
+                node => Assert.IsType<BinaryExpression>(node),    // x < 2
+                node => Assert.IsType<ExpressionStatement>(node), // x++;
+                node => Assert.IsType<BinaryExpression>(node),    // x < 2
+                node => Assert.IsType<ExpressionStatement>(node), // x++;
+                node => Assert.IsType<BinaryExpression>(node)     // x < 2 (false)
+            );
+        }
+
+        [Fact]
+        public void StepsThroughDoWhileLoop()
+        {
+            string script = @"
+                let x = 0;
+                do
+                {
+                    x++;
+                }
+                while (x < 2)
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<VariableDeclaration>(node), // let x = 0;
+                node => Assert.IsType<DoWhileStatement>(node),    // do ...
+                node => Assert.IsType<ExpressionStatement>(node), // x++;
+                node => Assert.IsType<BinaryExpression>(node),    // x < 2
+                node => Assert.IsType<ExpressionStatement>(node), // x++;
+                node => Assert.IsType<BinaryExpression>(node)     // x < 2 (false)
+            );
+        }
+
+        [Fact]
+        public void StepsThroughForLoop()
+        {
+            string script = @"
+                for (let x = 0; x < 2; x++)
+                {
+                    'dummy';
+                }
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<ForStatement>(node),        // for ...
+                node => Assert.IsType<VariableDeclaration>(node), // let x = 0
+                node => Assert.IsType<BinaryExpression>(node),    // x < 2
+                node => Assert.IsType<ExpressionStatement>(node), // 'dummy';
+                node => Assert.IsType<UpdateExpression>(node),    // x++;
+                node => Assert.IsType<BinaryExpression>(node),    // x < 2
+                node => Assert.IsType<ExpressionStatement>(node), // 'dummy';
+                node => Assert.IsType<UpdateExpression>(node),    // x++;
+                node => Assert.IsType<BinaryExpression>(node)     // x < 2 (false)
+            );
+        }
+
+        [Fact]
+        public void StepsThroughForOfLoop()
+        {
+            string script = @"
+                const arr = [1, 2];
+                for (const item of arr)
+                {
+                    'dummy';
+                }
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<VariableDeclaration>(node), // let arr = [1, 2];
+                node => Assert.IsType<ForOfStatement>(node),      // for ...
+                node => Assert.IsType<VariableDeclaration>(node), // item
+                node => Assert.IsType<ExpressionStatement>(node), // 'dummy';
+                node => Assert.IsType<VariableDeclaration>(node), // item
+                node => Assert.IsType<ExpressionStatement>(node)  // 'dummy';
+            );
+        }
+
+        [Fact]
+        public void StepsThroughForInLoop()
+        {
+            string script = @"
+                const obj = { x: 1, y: 2 };
+                for (const key in obj)
+                {
+                    'dummy';
+                }
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<VariableDeclaration>(node), // let obj = { x: 1, y: 2 };
+                node => Assert.IsType<ForInStatement>(node),      // for ...
+                node => Assert.IsType<VariableDeclaration>(node), // key
+                node => Assert.IsType<ExpressionStatement>(node), // 'dummy';
+                node => Assert.IsType<VariableDeclaration>(node), // key
+                node => Assert.IsType<ExpressionStatement>(node)  // 'dummy';
+            );
+        }
+
+
+        [Fact]
+        public void SkipsFunctionBody()
+        {
+            string script = @"
+                function test()
+                {
+                    'dummy';
+                }
+                test();
+            ";
+
+            var nodes = CollectStepNodes(script);
+
+            Assert.Collection(nodes,
+                node => Assert.IsType<FunctionDeclaration>(node), // function(test) ...;
+                node => Assert.IsType<ExpressionStatement>(node), // test();
+                node => Assert.IsType<Directive>(node),           // 'dummy';
+                node => Assert.Null(node)                         // return point
+            );
+        }
+    }
+}

+ 48 - 16
Jint.Tests/Runtime/Debugger/StepModeTests.cs

@@ -1,4 +1,6 @@
-using Jint.Runtime.Debugger;
+using System.Collections.Generic;
+using Esprima.Ast;
+using Jint.Runtime.Debugger;
 using Xunit;
 using Xunit;
 
 
 namespace Jint.Tests.Runtime.Debugger
 namespace Jint.Tests.Runtime.Debugger
@@ -17,6 +19,7 @@ namespace Jint.Tests.Runtime.Debugger
         {
         {
             var engine = new Engine(options => options
             var engine = new Engine(options => options
                 .DebugMode()
                 .DebugMode()
+                .InitialStepMode(StepMode.Into)
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
                 .DebuggerStatementHandling(DebuggerStatementHandling.Script));
 
 
             int steps = 0;
             int steps = 0;
@@ -56,13 +59,13 @@ namespace Jint.Tests.Runtime.Debugger
         {
         {
             var script = @"
             var script = @"
                 'source';
                 'source';
-                test();
+                test(); // first step
                 function test()
                 function test()
                 {
                 {
-                    'target';
+                    'target'; // second step
                 }";
                 }";
 
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
         }
         }
 
 
         [Fact]
         [Fact]
@@ -103,13 +106,13 @@ namespace Jint.Tests.Runtime.Debugger
                 const obj = {
                 const obj = {
                     test()
                     test()
                     {
                     {
-                        'target';
+                        'target'; // second step
                     }
                     }
                 };
                 };
                 'source';
                 'source';
-                obj.test();";
+                obj.test(); // first step";
 
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
         }
         }
 
 
         [Fact]
         [Fact]
@@ -152,13 +155,13 @@ namespace Jint.Tests.Runtime.Debugger
             var script = @"
             var script = @"
                 function test()
                 function test()
                 {
                 {
-                    'target';
+                    'target'; // second step
                     return 42;
                     return 42;
                 }
                 }
                 'source';
                 'source';
-                const x = test();";
+                const x = test(); // first step";
 
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
         }
         }
 
 
         [Fact]
         [Fact]
@@ -200,14 +203,14 @@ namespace Jint.Tests.Runtime.Debugger
                 const obj = {
                 const obj = {
                     get test()
                     get test()
                     {
                     {
-                        'target';
+                        'target'; // second step
                         return 144;
                         return 144;
                     }
                     }
                 };
                 };
                 'source';
                 'source';
-                const x = obj.test;";
+                const x = obj.test; // first step";
 
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
         }
         }
 
 
         [Fact]
         [Fact]
@@ -252,14 +255,14 @@ namespace Jint.Tests.Runtime.Debugger
                 const obj = {
                 const obj = {
                     set test(value)
                     set test(value)
                     {
                     {
-                        'target';
+                        'target'; // second step
                         this.value = value;
                         this.value = value;
                     }
                     }
                 };
                 };
                 'source';
                 'source';
-                obj.test = 37;";
+                obj.test = 37; // first step";
 
 
-            Assert.Equal(3, StepsFromSourceToTarget(script, StepMode.Into));
+            Assert.Equal(2, StepsFromSourceToTarget(script, StepMode.Into));
         }
         }
 
 
         [Fact]
         [Fact]
@@ -403,5 +406,34 @@ namespace Jint.Tests.Runtime.Debugger
 
 
             engine.Execute(script);
             engine.Execute(script);
         }
         }
+
+        [Fact]
+        public void StepNotTriggeredWhenRunning()
+        {
+            string script = @"
+                test();
+
+                function test()
+                {
+                    'dummy';
+                    'øummy';
+                }";
+
+            var engine = new Engine(options => options
+                .DebugMode()
+                .InitialStepMode(StepMode.Into));
+
+            int stepCount = 0;
+            engine.DebugHandler.Step += (sender, info) =>
+            {
+                stepCount++;
+                // Start running after first step
+                return StepMode.None;
+            };
+
+            engine.Execute(script);
+
+            Assert.Equal(1, stepCount);
+        }
     }
     }
 }
 }

+ 8 - 28
Jint.Tests/Runtime/Debugger/TestHelpers.cs

@@ -7,9 +7,9 @@ namespace Jint.Tests.Runtime.Debugger
 {
 {
     public static class TestHelpers
     public static class TestHelpers
     {
     {
-        public static bool IsLiteral(this Statement statement, string requiredValue = null)
+        public static bool IsLiteral(this Node node, string requiredValue = null)
         {
         {
-            switch (statement)
+            switch (node)
             {
             {
                 case Directive directive:
                 case Directive directive:
                     return requiredValue == null || directive.Directiv == requiredValue;
                     return requiredValue == null || directive.Directiv == requiredValue;
@@ -22,7 +22,7 @@ namespace Jint.Tests.Runtime.Debugger
 
 
         public static bool ReachedLiteral(this DebugInformation info, string requiredValue)
         public static bool ReachedLiteral(this DebugInformation info, string requiredValue)
         {
         {
-            return info.CurrentStatement.IsLiteral(requiredValue);
+            return info.CurrentNode.IsLiteral(requiredValue);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -31,7 +31,7 @@ namespace Jint.Tests.Runtime.Debugger
         /// </summary>
         /// </summary>
         /// <param name="script">Script that is basis for testing</param>
         /// <param name="script">Script that is basis for testing</param>
         /// <param name="breakHandler">Handler for assertions</param>
         /// <param name="breakHandler">Handler for assertions</param>
-        public static void TestAtBreak(string script, Action<DebugInformation> breakHandler)
+        public static void TestAtBreak(string script, Action<Engine, DebugInformation> breakHandler)
         {
         {
             var engine = new Engine(options => options
             var engine = new Engine(options => options
                 .DebugMode()
                 .DebugMode()
@@ -42,7 +42,7 @@ namespace Jint.Tests.Runtime.Debugger
             engine.DebugHandler.Break += (sender, info) =>
             engine.DebugHandler.Break += (sender, info) =>
             {
             {
                 didBreak = true;
                 didBreak = true;
-                breakHandler(info);
+                breakHandler(sender as Engine, info);
                 return StepMode.None;
                 return StepMode.None;
             };
             };
 
 
@@ -51,30 +51,10 @@ namespace Jint.Tests.Runtime.Debugger
             Assert.True(didBreak, "Test script did not break (e.g. didn't reach debugger statement)");
             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)
+        /// <inheritdoc cref="TestAtBreak()"/>
+        public static void TestAtBreak(string script, Action<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)");
+            TestAtBreak(script, (engine, info) => breakHandler(info));
         }
         }
     }
     }
 }
 }

+ 18 - 18
Jint.Tests/Runtime/EngineTests.cs

@@ -1471,7 +1471,7 @@ var prep = function (fn) { fn(); };
 
 
             engine.DebugHandler.Break += EngineStep;
             engine.DebugHandler.Break += EngineStep;
 
 
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(1, 1));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(1, 0));
 
 
             engine.Evaluate(@"var local = true;
             engine.Evaluate(@"var local = true;
                 if (local === true)
                 if (local === true)
@@ -1488,7 +1488,7 @@ var prep = function (fn) { fn(); };
             countBreak = 0;
             countBreak = 0;
             stepMode = StepMode.Into;
             stepMode = StepMode.Into;
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(stepMode));
 
 
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Step += EngineStep;
 
 
@@ -1507,8 +1507,8 @@ var prep = function (fn) { fn(); };
             countBreak = 0;
             countBreak = 0;
             stepMode = StepMode.Into;
             stepMode = StepMode.Into;
 
 
-            var engine = new Engine(options => options.DebugMode());
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(1, 1));
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(stepMode));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(1, 1));
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Break += EngineStep;
             engine.DebugHandler.Break += EngineStep;
 
 
@@ -1537,7 +1537,7 @@ var prep = function (fn) { fn(); };
             stepMode = StepMode.None;
             stepMode = StepMode.None;
 
 
             var engine = new Engine(options => options.DebugMode());
             var engine = new Engine(options => options.DebugMode());
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(5, 0));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 0));
             engine.DebugHandler.Break += EngineStepVerifyDebugInfo;
             engine.DebugHandler.Break += EngineStepVerifyDebugInfo;
 
 
             engine.Evaluate(@"var global = true;
             engine.Evaluate(@"var global = true;
@@ -1560,7 +1560,7 @@ var prep = function (fn) { fn(); };
             Assert.NotNull(debugInfo);
             Assert.NotNull(debugInfo);
 
 
             Assert.NotNull(debugInfo.CallStack);
             Assert.NotNull(debugInfo.CallStack);
-            Assert.NotNull(debugInfo.CurrentStatement);
+            Assert.NotNull(debugInfo.CurrentNode);
             Assert.NotNull(debugInfo.CurrentScopeChain);
             Assert.NotNull(debugInfo.CurrentScopeChain);
             Assert.NotNull(debugInfo.CurrentScopeChain.Global);
             Assert.NotNull(debugInfo.CurrentScopeChain.Global);
             Assert.NotNull(debugInfo.CurrentScopeChain.Local);
             Assert.NotNull(debugInfo.CurrentScopeChain.Local);
@@ -1588,8 +1588,8 @@ var prep = function (fn) { fn(); };
 
 
             engine.DebugHandler.Break += EngineStep;
             engine.DebugHandler.Break += EngineStep;
 
 
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(5, 16, "condition === true"));
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(6, 16, "condition === false"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(5, 16, "condition === true"));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(6, 16, "condition === false"));
 
 
             engine.Evaluate(@"var local = true;
             engine.Evaluate(@"var local = true;
                 var condition = true;
                 var condition = true;
@@ -1610,7 +1610,7 @@ var prep = function (fn) { fn(); };
             countBreak = 0;
             countBreak = 0;
             stepMode = StepMode.Out;
             stepMode = StepMode.Out;
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));
 
 
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Step += EngineStep;
 
 
@@ -1632,7 +1632,7 @@ var prep = function (fn) { fn(); };
         {
         {
             countBreak = 0;
             countBreak = 0;
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into));
 
 
             engine.DebugHandler.Step += EngineStepOutWhenInsideFunction;
             engine.DebugHandler.Step += EngineStepOutWhenInsideFunction;
 
 
@@ -1669,7 +1669,7 @@ var prep = function (fn) { fn(); };
             stepMode = StepMode.None;
             stepMode = StepMode.None;
 
 
             var engine = new Engine(options => options.DebugMode());
             var engine = new Engine(options => options.DebugMode());
-            engine.DebugHandler.BreakPoints.Add(new BreakPoint(4, 33));
+            engine.DebugHandler.BreakPoints.Set(new BreakPoint(4, 32));
             engine.DebugHandler.Break += EngineStep;
             engine.DebugHandler.Break += EngineStep;
 
 
             engine.Evaluate(@"var global = true;
             engine.Evaluate(@"var global = true;
@@ -1689,10 +1689,10 @@ var prep = function (fn) { fn(); };
         public void ShouldNotStepInsideIfRequiredToStepOver()
         public void ShouldNotStepInsideIfRequiredToStepOver()
         {
         {
             countBreak = 0;
             countBreak = 0;
+            stepMode = StepMode.Over;
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(stepMode));
 
 
-            stepMode = StepMode.Over;
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Step += EngineStep;
 
 
             engine.Evaluate(@"function func() // first step
             engine.Evaluate(@"function func() // first step
@@ -1712,22 +1712,22 @@ var prep = function (fn) { fn(); };
         public void ShouldStepAllStatementsWithoutInvocationsIfStepOver()
         public void ShouldStepAllStatementsWithoutInvocationsIfStepOver()
         {
         {
             countBreak = 0;
             countBreak = 0;
+            stepMode = StepMode.Over;
 
 
-            var engine = new Engine(options => options.DebugMode());
+            var engine = new Engine(options => options.DebugMode().InitialStepMode(stepMode));
 
 
-            stepMode = StepMode.Over;
             engine.DebugHandler.Step += EngineStep;
             engine.DebugHandler.Step += EngineStep;
 
 
             engine.Evaluate(@"var step1 = 1; // first step
             engine.Evaluate(@"var step1 = 1; // first step
                 var step2 = 2; // second step
                 var step2 = 2; // second step
                 if (step1 !== step2) // third step
                 if (step1 !== step2) // third step
-                { // fourth step
-                    ; // fifth step
+                {
+                    ; // fourth step
                 }");
                 }");
 
 
             engine.DebugHandler.Step -= EngineStep;
             engine.DebugHandler.Step -= EngineStep;
 
 
-            Assert.Equal(5, countBreak);
+            Assert.Equal(4, countBreak);
         }
         }
 
 
         [Fact]
         [Fact]

+ 8 - 3
Jint/Engine.cs

@@ -147,7 +147,7 @@ namespace Jint
             private set;
             private set;
         }
         }
 
 
-        public DebugHandler DebugHandler => _debugHandler ??= new DebugHandler(this);
+        public DebugHandler DebugHandler => _debugHandler ??= new DebugHandler(this, Options.Debugger.InitialStepMode);
 
 
 
 
         internal ExecutionContext EnterExecutionContext(
         internal ExecutionContext EnterExecutionContext(
@@ -252,7 +252,12 @@ namespace Jint
             => Execute(source, DefaultParserOptions);
             => Execute(source, DefaultParserOptions);
 
 
         public Engine Execute(string source, ParserOptions parserOptions)
         public Engine Execute(string source, ParserOptions parserOptions)
-            => Execute(new JavaScriptParser(source, parserOptions).ParseScript());
+        {
+            var parser = new JavaScriptParser(source, parserOptions);
+            var script = parser.ParseScript();
+
+            return Execute(script);
+        }
 
 
         public Engine Execute(Script script)
         public Engine Execute(Script script)
         {
         {
@@ -357,7 +362,7 @@ namespace Jint
                 constraint.Check();
                 constraint.Check();
             }
             }
 
 
-            if (_isDebugMode && statement != null)
+            if (_isDebugMode && statement != null && statement.Type != Nodes.BlockStatement)
             {
             {
                 DebugHandler.OnStep(statement);
                 DebugHandler.OnStep(statement);
             }
             }

+ 1 - 1
Jint/Native/Array/ArrayConstructor.cs

@@ -442,7 +442,7 @@ namespace Jint.Native.Array
 
 
             if (!c.IsConstructor)
             if (!c.IsConstructor)
             {
             {
-                ExceptionHelper.ThrowTypeError(_realm);
+                ExceptionHelper.ThrowTypeError(_realm, $"{c} is not a constructor");
             }
             }
 
 
             return ((IConstructor) c).Construct(new JsValue[] { JsNumber.Create(length) }, c);
             return ((IConstructor) c).Construct(new JsValue[] { JsNumber.Create(length) }, c);

+ 2 - 2
Jint/Native/Function/ScriptFunctionInstance.cs

@@ -78,7 +78,7 @@ namespace Jint.Native.Function
                     }
                     }
 
 
                     // The DebugHandler needs the current execution context before the return for stepping through the return point
                     // The DebugHandler needs the current execution context before the return for stepping through the return point
-                    if (_engine._isDebugMode)
+                    if (context.DebugMode)
                     {
                     {
                         // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
                         // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
                         // the function body:
                         // the function body:
@@ -141,7 +141,7 @@ namespace Jint.Native.Function
                     var result = OrdinaryCallEvaluateBody(_engine._activeEvaluationContext, arguments, calleeContext);
                     var result = OrdinaryCallEvaluateBody(_engine._activeEvaluationContext, arguments, calleeContext);
 
 
                     // The DebugHandler needs the current execution context before the return for stepping through the return point
                     // The DebugHandler needs the current execution context before the return for stepping through the return point
-                    if (_engine._isDebugMode && result.Type != CompletionType.Throw)
+                    if (_engine._activeEvaluationContext.DebugMode && result.Type != CompletionType.Throw)
                     {
                     {
                         // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
                         // We don't have a statement, but we still need a Location for debuggers. DebugHandler will infer one from
                         // the function body:
                         // the function body:

+ 1 - 1
Jint/Native/Symbol/SymbolConstructor.cs

@@ -109,7 +109,7 @@ namespace Jint.Native.Symbol
 
 
         ObjectInstance IConstructor.Construct(JsValue[] arguments, JsValue newTarget)
         ObjectInstance IConstructor.Construct(JsValue[] arguments, JsValue newTarget)
         {
         {
-            ExceptionHelper.ThrowTypeError(_realm);
+            ExceptionHelper.ThrowTypeError(_realm, "Symbol is not a constructor");
             return null;
             return null;
         }
         }
 
 

+ 9 - 0
Jint/Options.Extensions.cs

@@ -49,6 +49,15 @@ namespace Jint
             return options;
             return options;
         }
         }
 
 
+        /// <summary>
+        /// Set initial step mode.
+        /// </summary>
+        public static Options InitialStepMode(this Options options, StepMode initialStepMode = StepMode.None)
+        {
+            options.Debugger.InitialStepMode = initialStepMode;
+            return options;
+        }
+
         /// <summary>
         /// <summary>
         /// Adds a <see cref="IObjectConverter"/> instance to convert CLR types to <see cref="JsValue"/>
         /// Adds a <see cref="IObjectConverter"/> instance to convert CLR types to <see cref="JsValue"/>
         /// </summary>
         /// </summary>

+ 5 - 0
Jint/Options.cs

@@ -200,6 +200,11 @@ namespace Jint
         /// Configures the statement handling strategy, defaults to Ignore.
         /// Configures the statement handling strategy, defaults to Ignore.
         /// </summary>
         /// </summary>
         public DebuggerStatementHandling StatementHandling { get; set; } = DebuggerStatementHandling.Ignore;
         public DebuggerStatementHandling StatementHandling { get; set; } = DebuggerStatementHandling.Ignore;
+
+        /// <summary>
+        /// Configures the step mode used when entering the script.
+        /// </summary>
+        public StepMode InitialStepMode { get; set; } = StepMode.None;
     }
     }
 
 
     public class InteropOptions
     public class InteropOptions

+ 30 - 0
Jint/Runtime/Debugger/BreakLocation.cs

@@ -0,0 +1,30 @@
+#nullable enable
+
+namespace Jint.Runtime.Debugger;
+
+/// <summary>
+/// BreakLocation is a combination of an Esprima position (line and column) and a source (path or identifier of script).
+/// Like Esprima, first column is 0 and first line is 1.
+/// </summary>
+public sealed record BreakLocation
+{
+    public BreakLocation(string? source, int line, int column)
+    {
+        Source = source;
+        Line = line;
+        Column = column;
+    }
+
+    public BreakLocation(int line, int column) : this(null, line, column)
+    {
+
+    }
+
+    public BreakLocation(string source, Esprima.Position position) : this(source, position.Line, position.Column)
+    {
+    }
+
+    public string? Source { get; }
+    public int Line { get; }
+    public int Column { get; }
+}

+ 16 - 27
Jint/Runtime/Debugger/BreakPoint.cs

@@ -1,32 +1,21 @@
-namespace Jint.Runtime.Debugger
-{
-    public sealed class BreakPoint
-    {
-        public BreakPoint(int line, int column)
-        {
-            Line = line;
-            Column = column;
-        }
+#nullable enable
 
 
-        public BreakPoint(int line, int column, string condition)
-            : this(line, column)
-        {
-            Condition = condition;
-        }
+namespace Jint.Runtime.Debugger;
 
 
-        public BreakPoint(string source, int line, int column) : this(line, column)
-        {
-            Source = source;
-        }
-
-        public BreakPoint(string source, int line, int column, string condition) : this(source, line, column)
-        {
-            Condition = condition;
-        }
+// BreakPoint is not sealed. It's useful to be able to add additional properties on a derived BreakPoint class (e.g. a breakpoint ID
+// or breakpoint type) but still let it be managed by Jint's breakpoint collection.
+public class BreakPoint
+{
+    public BreakPoint(string? source, int line, int column, string? condition = null)
+    {
+        Location = new BreakLocation(source, line, column);
+        Condition = condition;
+    }
 
 
-        public string Source { get; }
-        public int Line { get; }
-        public int Column { get; }
-        public string Condition { get; }
+    public BreakPoint(int line, int column, string? condition = null) : this(null, line, column, condition)
+    {
     }
     }
+
+    public BreakLocation Location { get; }
+    public string? Condition { get; }
 }
 }

+ 67 - 55
Jint/Runtime/Debugger/BreakPointCollection.cs

@@ -1,98 +1,110 @@
-using Esprima;
+using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 
 
 namespace Jint.Runtime.Debugger
 namespace Jint.Runtime.Debugger
 {
 {
-    public class BreakPointCollection : ICollection<BreakPoint>
+    /// <summary>
+    /// Collection of breakpoints.
+    /// </summary>
+    /// <remarks>
+    /// Only allows a single breakpoint at the same location (source, column and line).
+    /// Adding a new breakpoint at the same location <i>replaces</i> the old one - this allows replacing e.g. a 
+    /// conditional breakpoint with a new condition (or remove the condition).
+    /// </remarks>
+    public sealed class BreakPointCollection : IEnumerable<BreakPoint>
     {
     {
-        private readonly List<BreakPoint> _breakPoints = new List<BreakPoint>();
+        private readonly Dictionary<BreakLocation, BreakPoint> _breakPoints = new(new OptionalSourceBreakLocationEqualityComparer());
 
 
         public BreakPointCollection()
         public BreakPointCollection()
         {
         {
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets whether breakpoints are activated. When false, all breakpoints will fail to match (and be skipped by the debugger).
+        /// </summary>
+        public bool Active { get; set; } = true;
+
         public int Count => _breakPoints.Count;
         public int Count => _breakPoints.Count;
 
 
         public bool IsReadOnly => false;
         public bool IsReadOnly => false;
 
 
-        public void Add(BreakPoint breakPoint)
+        /// <summary>
+        /// Sets a new breakpoint. Note that this will replace any breakpoint at the same location (source/column/line).
+        /// </summary>
+        public void Set(BreakPoint breakPoint)
         {
         {
-            _breakPoints.Add(breakPoint);
+            _breakPoints[breakPoint.Location] = breakPoint;
         }
         }
 
 
-        public bool Remove(BreakPoint breakPoint)
-        {
-            return _breakPoints.Remove(breakPoint);
-        }
-
-        public void Clear()
+        /// <summary>
+        /// Removes breakpoint with the given location (source/column/line).
+        /// Note that a null source matches <i>any</i> source.
+        /// </summary>
+        public bool RemoveAt(BreakLocation location)
         {
         {
-            _breakPoints.Clear();
+            return _breakPoints.Remove(location);
         }
         }
 
 
-        public bool Contains(BreakPoint item)
+        /// <summary>
+        /// Checks whether collection contains a breakpoint at the given location (source/column/line).
+        /// Note that a null source matches <i>any</i> source.
+        /// </summary>
+        public bool Contains(BreakLocation location)
         {
         {
-            return _breakPoints.Contains(item);
+            return _breakPoints.ContainsKey(location);
         }
         }
 
 
-        public void CopyTo(BreakPoint[] array, int arrayIndex)
+        /// <summary>
+        /// Removes all breakpoints.
+        /// </summary>
+        public void Clear()
         {
         {
-            _breakPoints.CopyTo(array, arrayIndex);
+            _breakPoints.Clear();
         }
         }
 
 
-        public IEnumerator<BreakPoint> GetEnumerator()
+        internal BreakPoint FindMatch(DebugHandler debugger, BreakLocation location)
         {
         {
-            return _breakPoints.GetEnumerator();
-        }
+            if (!Active)
+            {
+                return null;
+            }
 
 
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return _breakPoints.GetEnumerator();
-        }
+            if (!_breakPoints.TryGetValue(location, out var breakPoint))
+            {
+                return null;
+            }
 
 
-        internal BreakPoint FindMatch(Engine engine, Location location)
-        {
-            foreach (var breakPoint in _breakPoints)
+            if (!string.IsNullOrEmpty(breakPoint.Condition))
             {
             {
-                if (breakPoint.Source != null)
+                try
                 {
                 {
-                    if (breakPoint.Source != location.Source)
+                    var completionValue = debugger.Evaluate(breakPoint.Condition);
+
+                    // Truthiness check:
+                    if (!TypeConverter.ToBoolean(completionValue))
                     {
                     {
-                        continue;
+                        return null;
                     }
                     }
                 }
                 }
-
-                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)
+                catch (Exception ex) when (ex is JavaScriptException || ex is DebugEvaluationException)
                 {
                 {
-                    continue;
+                    // Error in the condition means it doesn't match - shouldn't actually throw.
+                    return null;
                 }
                 }
+            }
 
 
-                if (!string.IsNullOrEmpty(breakPoint.Condition))
-                {
-                    var completionValue = engine.Evaluate(breakPoint.Condition);
-                    if (!completionValue.AsBoolean())
-                    {
-                        continue;
-                    }
-                }
+            return breakPoint;
+        }
 
 
-                return breakPoint;
-            }
+        public IEnumerator<BreakPoint> GetEnumerator()
+        {
+            return _breakPoints.Values.GetEnumerator();
+        }
 
 
-            return null;
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return _breakPoints.GetEnumerator();
         }
         }
     }
     }
 }
 }

+ 19 - 0
Jint/Runtime/Debugger/DebugEvaluationException.cs

@@ -0,0 +1,19 @@
+#nullable enable
+
+using System;
+
+namespace Jint.Runtime.Debugger;
+
+/// <summary>
+/// Thrown when an evaluation executed through the DebugHandler results in any type of error - parsing or runtime.
+/// </summary>
+public sealed class DebugEvaluationException : JintException
+{
+    public DebugEvaluationException(string message) : base(message)
+    {
+    }
+
+    public DebugEvaluationException(string message, Exception innerException) : base(message, innerException)
+    {
+    }
+}

+ 168 - 58
Jint/Runtime/Debugger/DebugHandler.cs

@@ -2,54 +2,142 @@ using System;
 using Esprima;
 using Esprima;
 using Esprima.Ast;
 using Esprima.Ast;
 using Jint.Native;
 using Jint.Native;
+using Jint.Runtime.Interpreter;
 
 
 namespace Jint.Runtime.Debugger
 namespace Jint.Runtime.Debugger
 {
 {
-    public class DebugHandler
+    public enum PauseType
     {
     {
-        public delegate StepMode DebugStepDelegate(object sender, DebugInformation e);
-        public delegate StepMode BreakDelegate(object sender, DebugInformation e);
+        Step,
+        Break,
+        DebuggerStatement
+    }
 
 
-        private enum PauseType
-        {
-            Step,
-            Break
-        }
+    public class DebugHandler
+    {
+        public delegate StepMode DebugEventHandler(object sender, DebugInformation e);
 
 
         private readonly Engine _engine;
         private readonly Engine _engine;
         private bool _paused;
         private bool _paused;
         private int _steppingDepth;
         private int _steppingDepth;
 
 
-        public event DebugStepDelegate Step;
-        public event BreakDelegate Break;
-
-        internal DebugHandler(Engine engine)
+        /// <summary>
+        /// The Step event is triggered before the engine executes a step-eligible AST node.
+        /// </summary>
+        /// <remarks>
+        /// If the current step mode is <see cref="StepMode.None"/>, this event is never triggered. The script may
+        /// still be paused by a debugger statement or breakpoint, but these will trigger the
+        /// <see cref="Break"/> event.
+        /// </remarks>
+        public event DebugEventHandler Step;
+
+        /// <summary>
+        /// The Break event is triggered when a breakpoint or debugger statement is hit.
+        /// </summary>
+        /// <remarks>
+        /// This is event is not triggered if the current script location was reached by stepping. In that case, only
+        /// the <see cref="Step"/> event is triggered.
+        /// </remarks>
+        public event DebugEventHandler Break;
+
+        internal DebugHandler(Engine engine, StepMode initialStepMode)
         {
         {
             _engine = engine;
             _engine = engine;
-            _steppingDepth = int.MaxValue;
+            HandleNewStepMode(initialStepMode);
         }
         }
 
 
+        /// <summary>
+        /// The location of the current (step-eligible) AST node being executed.
+        /// </summary>
+        /// <remarks>
+        /// The location is available as long as DebugMode is enabled - i.e. even when not stepping
+        /// or hitting a breakpoint.
+        /// </remarks>
+        public Location? CurrentLocation { get; private set; }
+
+        /// <summary>
+        /// Collection of active breakpoints for the engine.
+        /// </summary>
         public BreakPointCollection BreakPoints { get; } = new BreakPointCollection();
         public BreakPointCollection BreakPoints { get; } = new BreakPointCollection();
 
 
-        internal void OnStep(Statement statement)
+        /// <summary>
+        /// Evaluates a script (expression) within the current execution context.
+        /// </summary>
+        /// <remarks>
+        /// Internally, this is used for evaluating breakpoint conditions, but may also be used for e.g. watch lists
+        /// in a debugger.
+        /// </remarks>
+        public JsValue Evaluate(Script script)
         {
         {
-            // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
-            if (_paused)
+            var context = _engine._activeEvaluationContext;
+            if (context == null)
             {
             {
-                return;
+                throw new DebugEvaluationException("Jint has no active evaluation context");
+            }
+            int callStackSize = _engine.CallStack.Count;
+
+            var list = new JintStatementList(null, script.Body);
+            Completion result;
+            try
+            {
+                result = list.Execute(context);
+            }
+            catch (Exception ex)
+            {
+                // An error in the evaluation may return a Throw Completion, or it may throw an exception:
+                throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
+            }
+            finally
+            {
+                // Restore call stack
+                while (_engine.CallStack.Count > callStackSize)
+                {
+                    _engine.CallStack.Pop();
+                }
             }
             }
 
 
-            Location location = statement.Location;
-            BreakPoint breakpoint = BreakPoints.FindMatch(_engine, location);
+            if (result.Type == CompletionType.Throw)
+            {
+                // TODO: Should we return an error here? (avoid exception overhead, since e.g. breakpoint
+                // evaluation may be high volume.
+                var error = result.GetValueOrDefault();
+                var ex = new JavaScriptException(error).SetCallstack(_engine, result.Location);
+                throw new DebugEvaluationException("An error occurred during debugger evaluation", ex);
+            }
 
 
-            if (breakpoint != null)
+            return result.GetValueOrDefault();
+        }
+
+        /// <inheritdoc cref="Evaluate(Script)" />
+        public JsValue Evaluate(string source, ParserOptions options = null)
+        {
+            options ??= new ParserOptions("evaluation") { AdaptRegexp = true, Tolerant = true };
+            var parser = new JavaScriptParser(source, options);
+            try
             {
             {
-                Pause(PauseType.Break, statement);
+                var script = parser.ParseScript();
+                return Evaluate(script);
             }
             }
-            else if (_engine.CallStack.Count <= _steppingDepth)
+            catch (ParserException ex)
+            {
+                throw new DebugEvaluationException("An error occurred during debugger expression parsing", ex);
+            }
+        }
+
+        internal void OnStep(Node node)
+        {
+            // Don't reenter if we're already paused (e.g. when evaluating a getter in a Break/Step handler)
+            if (_paused)
             {
             {
-                Pause(PauseType.Step, statement);
+                return;
             }
             }
+            _paused = true;
+
+            CheckBreakPointAndPause(
+                new BreakLocation(node.Location.Source, node.Location.Start),
+                node: node,
+                location: null,
+                returnValue: null);
         }
         }
 
 
         internal void OnReturnPoint(Node functionBody, JsValue returnValue)
         internal void OnReturnPoint(Node functionBody, JsValue returnValue)
@@ -59,51 +147,82 @@ namespace Jint.Runtime.Debugger
             {
             {
                 return;
                 return;
             }
             }
+            _paused = true;
 
 
             var bodyLocation = functionBody.Location;
             var bodyLocation = functionBody.Location;
             var functionBodyEnd = bodyLocation.End;
             var functionBodyEnd = bodyLocation.End;
             var location = new Location(functionBodyEnd, functionBodyEnd, bodyLocation.Source);
             var location = new Location(functionBodyEnd, functionBodyEnd, bodyLocation.Source);
 
 
-            BreakPoint breakpoint = BreakPoints.FindMatch(_engine, location);
+            CheckBreakPointAndPause(
+                new BreakLocation(bodyLocation.Source, bodyLocation.End),
+                node: null,
+                location: location,
+                returnValue: returnValue);
+        }
 
 
-            if (breakpoint != null)
+        internal void OnDebuggerStatement(Statement statement)
+        {
+            // Don't reenter if we're already paused
+            if (_paused)
             {
             {
-                Pause(PauseType.Break, statement: null, location, returnValue);
+                return;
             }
             }
-            else if (_engine.CallStack.Count <= _steppingDepth)
+            _paused = true;
+
+            bool isStepping = _engine.CallStack.Count <= _steppingDepth;
+
+            // Even though we're at a debugger statement, if we're stepping, ignore the statement. OnStep already
+            // takes care of pausing.
+            if (!isStepping)
             {
             {
-                Pause(PauseType.Step, statement: null, location, returnValue);
+                Pause(PauseType.DebuggerStatement, statement);
             }
             }
+
+            _paused = false;
         }
         }
 
 
-        private void Pause(PauseType type, Statement statement = null, Location? location = null, JsValue returnValue = null)
+        private void CheckBreakPointAndPause(BreakLocation breakLocation, Node node = null, Location? location = null,
+            JsValue returnValue = null)
         {
         {
-            _paused = true;
-            
-            DebugInformation info = CreateDebugInformation(statement, location ?? statement.Location, returnValue);
-            
+            CurrentLocation = location ?? node?.Location;
+            BreakPoint breakpoint = BreakPoints.FindMatch(this, breakLocation);
+
+            bool isStepping = _engine.CallStack.Count <= _steppingDepth;
+
+            if (breakpoint != null || isStepping)
+            {
+                // Even if we matched a breakpoint, if we're stepping, the reason we're pausing is the step.
+                // Still, we need to include the breakpoint at this location, in case the debugger UI needs to update
+                // e.g. a hit count.
+                Pause(isStepping ? PauseType.Step : PauseType.Break, node, location, returnValue, breakpoint);
+            }
+
+            _paused = false;
+        }
+
+        private void Pause(PauseType type, Node node = null, Location? location = null, JsValue returnValue = null,
+            BreakPoint breakPoint = null)
+        {
+            var info = new DebugInformation(
+                engine: _engine,
+                currentNode: node,
+                currentLocation: location ?? node.Location,
+                returnValue: returnValue,
+                currentMemoryUsage: _engine.CurrentMemoryUsage,
+                pauseType: type,
+                breakPoint: breakPoint
+            );
+
             StepMode? result = type switch
             StepMode? result = type switch
             {
             {
                 // Conventionally, sender should be DebugHandler - but Engine is more useful
                 // Conventionally, sender should be DebugHandler - but Engine is more useful
                 PauseType.Step => Step?.Invoke(_engine, info),
                 PauseType.Step => Step?.Invoke(_engine, info),
                 PauseType.Break => Break?.Invoke(_engine, info),
                 PauseType.Break => Break?.Invoke(_engine, info),
+                PauseType.DebuggerStatement => Break?.Invoke(_engine, info),
                 _ => throw new ArgumentException("Invalid pause type", nameof(type))
                 _ => throw new ArgumentException("Invalid pause type", nameof(type))
             };
             };
-            
-            _paused = false;
-            
-            HandleNewStepMode(result);
-        }
 
 
-        internal void OnBreak(Statement statement)
-        {
-            // Don't reenter if we're already paused
-            if (_paused)
-            {
-                return;
-            }
-
-            Pause(PauseType.Break, statement);
+            HandleNewStepMode(result);
         }
         }
 
 
         private void HandleNewStepMode(StepMode? newStepMode)
         private void HandleNewStepMode(StepMode? newStepMode)
@@ -112,21 +231,12 @@ namespace Jint.Runtime.Debugger
             {
             {
                 _steppingDepth = newStepMode switch
                 _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.Over => _engine.CallStack.Count,// Resume stepping when back at this level of the stack
+                    StepMode.Out => _engine.CallStack.Count - 1,// Resume stepping when we've popped the stack
                     StepMode.None => int.MinValue,// Never step
                     StepMode.None => int.MinValue,// Never step
                     _ => int.MaxValue,// Always step
                     _ => int.MaxValue,// Always step
                 };
                 };
             }
             }
         }
         }
-
-        private DebugInformation CreateDebugInformation(Statement statement, Location? currentLocation, JsValue returnValue)
-        {
-            return new DebugInformation(
-                statement,
-                new DebugCallStack(_engine, currentLocation ?? statement.Location, _engine.CallStack, returnValue),
-                _engine.CurrentMemoryUsage
-            );
-        }
     }
     }
 }
 }

+ 28 - 6
Jint/Runtime/Debugger/DebugInformation.cs

@@ -7,24 +7,46 @@ namespace Jint.Runtime.Debugger
 {
 {
     public sealed class DebugInformation : EventArgs
     public sealed class DebugInformation : EventArgs
     {
     {
-        internal DebugInformation(Statement currentStatement, DebugCallStack callStack, long currentMemoryUsage)
+        private readonly Engine _engine;
+        private readonly Location _currentLocation;
+        private readonly JsValue _returnValue;
+
+        private DebugCallStack _callStack;
+
+        internal DebugInformation(Engine engine, Node currentNode, Location currentLocation, JsValue returnValue,
+            long currentMemoryUsage, PauseType pauseType, BreakPoint breakPoint)
         {
         {
-            CurrentStatement = currentStatement;
-            CallStack = callStack;
+            _engine = engine;
+            CurrentNode = currentNode;
+            _currentLocation = currentLocation;
+            _returnValue = returnValue;
             CurrentMemoryUsage = currentMemoryUsage;
             CurrentMemoryUsage = currentMemoryUsage;
+            PauseType = pauseType;
+            BreakPoint = breakPoint;
         }
         }
 
 
+        /// <summary>
+        /// Indicates the type of pause that resulted in this DebugInformation being generated.
+        /// </summary>
+        public PauseType PauseType { get; }
+
+        /// <summary>
+        /// Breakpoint at the current location. This will be set even if the pause wasn't caused by the breakpoint.
+        /// </summary>
+        public BreakPoint BreakPoint { get; }
+
         /// <summary>
         /// <summary>
         /// The current call stack.
         /// The current call stack.
         /// </summary>
         /// </summary>
         /// <remarks>This will always include at least a call frame for the global environment.</remarks>
         /// <remarks>This will always include at least a call frame for the global environment.</remarks>
-        public DebugCallStack CallStack { get; set; }
+        public DebugCallStack CallStack =>
+            _callStack ??= new DebugCallStack(_engine, _currentLocation, _engine.CallStack, _returnValue);
 
 
         /// <summary>
         /// <summary>
-        /// The Statement that will be executed on next step.
+        /// The AST Node that will be executed on next step.
         /// Note that this will be null when execution is at a return point.
         /// Note that this will be null when execution is at a return point.
         /// </summary>
         /// </summary>
-        public Statement CurrentStatement { get; }
+        public Node CurrentNode { get; }
 
 
         /// <summary>
         /// <summary>
         /// The current source Location.
         /// The current source Location.

+ 25 - 3
Jint/Runtime/Debugger/DebugScope.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Jint.Native;
 using Jint.Native;
+using Jint.Native.Object;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Environments;
 
 
 namespace Jint.Runtime.Debugger
 namespace Jint.Runtime.Debugger
@@ -17,6 +18,7 @@ namespace Jint.Runtime.Debugger
             ScopeType = type;
             ScopeType = type;
             _record = record;
             _record = record;
             _bindingNames = bindingNames;
             _bindingNames = bindingNames;
+            BindingObject = record is ObjectEnvironmentRecord objEnv ? objEnv._bindingObject : null;
             IsTopLevel = isTopLevel;
             IsTopLevel = isTopLevel;
         }
         }
 
 
@@ -29,7 +31,7 @@ namespace Jint.Runtime.Debugger
         /// For <see cref="DebugScopeType.Block">block</see> scopes, indicates whether this scope is at the top level of a containing function.
         /// For <see cref="DebugScopeType.Block">block</see> scopes, indicates whether this scope is at the top level of a containing function.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
-        /// Block scopes at the top level of a function are combined with Local scope in Chromium and devtools protocol.
+        /// Block scopes at the top level of a function are combined with Local scope in Chromium.
         /// This property facilitates implementing the same "flattening" in e.g. a UI. Because empty scopes are excluded in the scope chain,
         /// 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.
         /// top level cannot be determined from the scope chain order alone.
         /// </remarks>
         /// </remarks>
@@ -41,13 +43,33 @@ namespace Jint.Runtime.Debugger
         public IReadOnlyList<string> BindingNames => _bindingNames;
         public IReadOnlyList<string> BindingNames => _bindingNames;
 
 
         /// <summary>
         /// <summary>
-        /// Retrieves the value of a specific binding. Note that some bindings (e.g. uninitialized let) may return null.
+        /// Binding object for ObjectEnvironmentRecords - that is, Global scope and With scope. Null for other scopes.
+        /// </summary>
+        /// <remarks>
+        /// This is mainly useful as an optimization for devtools, allowing the BindingObject to be serialized directly rather than
+        /// building a new transient object in response to e.g. Runtime.getProperties.
+        /// </remarks>
+        public ObjectInstance BindingObject { get; }
+
+        /// <summary>
+        /// Retrieves the value of a specific binding. Note that some bindings (e.g. uninitialized let/const) may return null.
         /// </summary>
         /// </summary>
         /// <param name="name">Binding name</param>
         /// <param name="name">Binding name</param>
         /// <returns>Value of the binding</returns>
         /// <returns>Value of the binding</returns>
         public JsValue GetBindingValue(string name)
         public JsValue GetBindingValue(string name)
         {
         {
-            return _record.GetBindingValue(name, strict: false);
+            _record.TryGetBindingValue(name, strict: true, out var result);
+            return result;
+        }
+
+        /// <summary>
+        /// Sets the value of an existing binding.
+        /// </summary>
+        /// <param name="name">Binding name</param>
+        /// <param name="value">New value of the binding</param>
+        public void SetBindingValue(string name, JsValue value)
+        {
+            _record.SetMutableBinding(name, value, strict: true);
         }
         }
     }
     }
 }
 }

+ 4 - 3
Jint/Runtime/Debugger/DebugScopeType.cs

@@ -10,7 +10,6 @@
         /// Global scope bindings.
         /// Global scope bindings.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <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.
         /// A scope chain will only include one scope of this type.
         /// </remarks>
         /// </remarks>
         Global,
         Global,
@@ -18,9 +17,11 @@
         /// <summary>
         /// <summary>
         /// Block scope bindings (let/const) defined at top level.
         /// Block scope bindings (let/const) defined at top level.
         /// </summary>
         /// </summary>
-        /// <remarks>Not currently implemented by Jint. These bindings are instead included in <see cref="Global" />.</remarks>
+        /// <remarks>
+        /// A scope chain will only include one scope of this type.
+        /// </remarks>
         Script,
         Script,
-        
+
         /// <summary>
         /// <summary>
         /// Function local bindings.
         /// Function local bindings.
         /// </summary>
         /// </summary>

+ 14 - 6
Jint/Runtime/Debugger/DebugScopes.cs

@@ -15,14 +15,20 @@ namespace Jint.Runtime.Debugger
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Shortcut to Global scope
+        /// Shortcut to Global scope.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Note that this only includes the object environment record of the Global scope - i.e. it doesn't
+        /// include block scope bindings (let/const).
+        /// </remarks>
         public DebugScope Global { get; private set; }
         public DebugScope Global { get; private set; }
 
 
         /// <summary>
         /// <summary>
-        /// Shortcut to Local scope. Note that this is only present inside functions, and only includes
-        /// function scope bindings.
+        /// Shortcut to Local scope.
         /// </summary>
         /// </summary>
+        /// <remarks>
+        /// Note that this is only present inside functions, and doesn't include block scope bindings (let/const)
+        /// </remarks>
         public DebugScope Local { get; private set; }
         public DebugScope Local { get; private set; }
 
 
         public DebugScope this[int index] => _scopes[index];
         public DebugScope this[int index] => _scopes[index];
@@ -36,8 +42,10 @@ namespace Jint.Runtime.Debugger
                 EnvironmentRecord record = environment;
                 EnvironmentRecord record = environment;
                 switch (record)
                 switch (record)
                 {
                 {
-                    case GlobalEnvironmentRecord:
-                        AddScope(DebugScopeType.Global, record);
+                    case GlobalEnvironmentRecord global:
+                        // Similarly to Chromium, we split the Global environment into Global and Script scopes
+                        AddScope(DebugScopeType.Script, global._declarativeRecord);
+                        AddScope(DebugScopeType.Global, global._objectRecord);
                         break;
                         break;
                     case FunctionEnvironmentRecord:
                     case FunctionEnvironmentRecord:
                         AddScope(inLocalScope ? DebugScopeType.Local : DebugScopeType.Closure, record);
                         AddScope(inLocalScope ? DebugScopeType.Local : DebugScopeType.Closure, record);
@@ -56,7 +64,7 @@ namespace Jint.Runtime.Debugger
                         else
                         else
                         {
                         {
                             bool isTopLevel = environment._outerEnv is FunctionEnvironmentRecord;
                             bool isTopLevel = environment._outerEnv is FunctionEnvironmentRecord;
-                            AddScope(DebugScopeType.Block, record, isTopLevel);
+                            AddScope(DebugScopeType.Block, record, isTopLevel: isTopLevel);
                         }
                         }
                         break;
                         break;
                 }
                 }

+ 51 - 0
Jint/Runtime/Debugger/OptionalSourceBreakLocationEqualityComparer.cs

@@ -0,0 +1,51 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+
+namespace Jint.Runtime.Debugger;
+
+/// <summary>
+/// Equality comparer for BreakLocation matching null Source to any other Source.
+/// </summary>
+/// <remarks>
+/// Equals returns true if all properties are equal - or if Source is null on either BreakLocation.
+/// GetHashCode excludes Source.
+/// </remarks>
+internal sealed class OptionalSourceBreakLocationEqualityComparer : IEqualityComparer<BreakLocation>
+{
+    public bool Equals(BreakLocation? x, BreakLocation? y)
+    {
+        if (Object.ReferenceEquals(x, y))
+        {
+            return true;
+        }
+
+        if (x is null || y is null)
+        {
+            return false;
+        }
+
+        return
+            x.Line == y.Line &&
+            x.Column == y.Column &&
+            (x.Source == null || y.Source == null || x.Source == y.Source);
+    }
+
+    public int GetHashCode(BreakLocation? obj)
+    {
+        if (obj == null)
+        {
+            return 0;
+        }
+        // Keeping this rather than HashCode.Combine, which isn't in net461 or netstandard2.0
+        unchecked
+        {
+            int hash = 17;
+            hash = hash * 33 + obj.Line.GetHashCode();
+            hash = hash * 33 + obj.Column.GetHashCode();
+            // Don't include Source
+            return hash;
+        }
+    }
+}

+ 12 - 0
Jint/Runtime/Environments/DeclarativeEnvironmentRecord.cs

@@ -122,6 +122,18 @@ namespace Jint.Runtime.Environments
             return null;
             return null;
         }
         }
 
 
+        internal override bool TryGetBindingValue(string name, bool strict, out JsValue value)
+        {
+            _dictionary.TryGetValue(name, out var binding);
+            if (binding.IsInitialized())
+            {
+                value = binding.Value;
+                return true;
+            }
+            value = null;
+            return false;
+        }
+
         [MethodImpl(MethodImplOptions.NoInlining)]
         [MethodImpl(MethodImplOptions.NoInlining)]
         private void ThrowUninitializedBindingError(string name)
         private void ThrowUninitializedBindingError(string name)
         {
         {

+ 11 - 0
Jint/Runtime/Environments/EnvironmentRecord.cs

@@ -71,6 +71,17 @@ namespace Jint.Runtime.Environments
         /// <return>The value of an already existing binding from an environment record.</return>
         /// <return>The value of an already existing binding from an environment record.</return>
         public abstract JsValue GetBindingValue(string name, bool strict);
         public abstract JsValue GetBindingValue(string name, bool strict);
 
 
+        /// <summary>
+        /// Returns the value of an already existing binding from an environment record. Unlike <see cref="GetBindingValue(string, bool)"/>
+        /// this does not throw an exception for uninitialized bindings, but instead returns false and sets <paramref name="value"/> to null.
+        /// </summary>
+        /// <param name="name">The identifier of the binding</param>
+        /// <param name="strict">Strict mode</param>
+        /// <param name="value">The value of an already existing binding from an environment record.</param>
+        /// <returns>True if the value is initialized, otherwise false.</returns>
+        /// <remarks>This is used for debugger inspection. Note that this will currently still throw if the binding cannot be retrieved (e.g. because it doesn't exist).</remarks>
+        internal abstract bool TryGetBindingValue(string name, bool strict, out JsValue value);
+
         /// <summary>
         /// <summary>
         /// Delete a binding from an environment record. The String value N is the text of the bound name If a binding for N exists, remove the binding and return true. If the binding exists but cannot be removed return false. If the binding does not exist return true.
         /// Delete a binding from an environment record. The String value N is the text of the bound name If a binding for N exists, remove the binding and return true. If the binding exists but cannot be removed return false. If the binding does not exist return true.
         /// </summary>
         /// </summary>

+ 10 - 2
Jint/Runtime/Environments/GlobalEnvironmentRecord.cs

@@ -14,8 +14,9 @@ namespace Jint.Runtime.Environments
     public sealed class GlobalEnvironmentRecord : EnvironmentRecord
     public sealed class GlobalEnvironmentRecord : EnvironmentRecord
     {
     {
         private readonly ObjectInstance _global;
         private readonly ObjectInstance _global;
-        private readonly DeclarativeEnvironmentRecord _declarativeRecord;
-        private readonly ObjectEnvironmentRecord _objectRecord;
+        // Environment records are needed by debugger
+        internal readonly DeclarativeEnvironmentRecord _declarativeRecord;
+        internal readonly ObjectEnvironmentRecord _objectRecord;
         private readonly HashSet<string> _varNames = new HashSet<string>();
         private readonly HashSet<string> _varNames = new HashSet<string>();
 
 
         public GlobalEnvironmentRecord(Engine engine, ObjectInstance global) : base(engine)
         public GlobalEnvironmentRecord(Engine engine, ObjectInstance global) : base(engine)
@@ -170,6 +171,13 @@ namespace Jint.Runtime.Environments
                 : _objectRecord.GetBindingValue(name, strict);
                 : _objectRecord.GetBindingValue(name, strict);
         }
         }
 
 
+        internal override bool TryGetBindingValue(string name, bool strict, out JsValue value)
+        {
+            return _declarativeRecord._hasBindings && _declarativeRecord.HasBinding(name)
+                ? _declarativeRecord.TryGetBindingValue(name, strict, out value)
+                : _objectRecord.TryGetBindingValue(name, strict, out value);
+        }
+
         public override bool DeleteBinding(string name)
         public override bool DeleteBinding(string name)
         {
         {
             if (_declarativeRecord._hasBindings && _declarativeRecord.HasBinding(name))
             if (_declarativeRecord._hasBindings && _declarativeRecord.HasBinding(name))

+ 13 - 0
Jint/Runtime/Environments/ObjectEnvironmentRecord.cs

@@ -157,6 +157,19 @@ namespace Jint.Runtime.Environments
             return ObjectInstance.UnwrapJsValue(desc, _bindingObject);
             return ObjectInstance.UnwrapJsValue(desc, _bindingObject);
         }
         }
 
 
+        internal override bool TryGetBindingValue(string name, bool strict, out JsValue value)
+        {
+            var desc = _bindingObject.GetProperty(name);
+            if (strict && desc == PropertyDescriptor.Undefined)
+            {
+                value = null;
+                return false;
+            }
+
+            value = ObjectInstance.UnwrapJsValue(desc, _bindingObject);
+            return true;
+        }
+
         public override bool DeleteBinding(string name)
         public override bool DeleteBinding(string name)
         {
         {
             return _bindingObject.Delete(name);
             return _bindingObject.Delete(name);

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

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

+ 6 - 1
Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs

@@ -50,10 +50,15 @@ namespace Jint.Runtime.Interpreter.Statements
                     }
                     }
                 }
                 }
 
 
+                if (context.DebugMode)
+                {
+                    context.Engine.DebugHandler.OnStep(_test._expression);
+                }
+
                 iterating = TypeConverter.ToBoolean(_test.GetValue(context).Value);
                 iterating = TypeConverter.ToBoolean(_test.GetValue(context).Value);
             } while (iterating);
             } while (iterating);
 
 
             return NormalCompletion(v);
             return NormalCompletion(v);
         }
         }
     }
     }
-}
+}

+ 6 - 1
Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs

@@ -207,6 +207,11 @@ namespace Jint.Runtime.Interpreter.Statements
                         }
                         }
                     }
                     }
 
 
+                    if (context.DebugMode)
+                    {
+                        context.Engine.DebugHandler.OnStep(_leftNode);
+                    }
+
                     var status = new Completion();
                     var status = new Completion();
                     if (!destructuring)
                     if (!destructuring)
                     {
                     {
@@ -394,4 +399,4 @@ namespace Jint.Runtime.Interpreter.Statements
             }
             }
         }
         }
     }
     }
-}
+}

+ 15 - 2
Jint/Runtime/Interpreter/Statements/JintForStatement.cs

@@ -126,6 +126,11 @@ namespace Jint.Runtime.Interpreter.Statements
             {
             {
                 if (_test != null)
                 if (_test != null)
                 {
                 {
+                    if (context.DebugMode)
+                    {
+                        context.Engine.DebugHandler.OnStep(_test._expression);
+                    }
+
                     if (!TypeConverter.ToBoolean(_test.GetValue(context).Value))
                     if (!TypeConverter.ToBoolean(_test.GetValue(context).Value))
                     {
                     {
                         return NormalCompletion(v);
                         return NormalCompletion(v);
@@ -156,7 +161,15 @@ namespace Jint.Runtime.Interpreter.Statements
                     CreatePerIterationEnvironment(context);
                     CreatePerIterationEnvironment(context);
                 }
                 }
 
 
-                _increment?.GetValue(context);
+                if (_increment != null)
+                {
+                    if (context.DebugMode)
+                    {
+                        context.Engine.DebugHandler.OnStep(_increment._expression);
+                    }
+
+                    _increment.GetValue(context);
+                }
             }
             }
         }
         }
 
 
@@ -183,4 +196,4 @@ namespace Jint.Runtime.Interpreter.Statements
             engine.UpdateLexicalEnvironment(thisIterationEnv);
             engine.UpdateLexicalEnvironment(thisIterationEnv);
         }
         }
     }
     }
-}
+}

+ 6 - 1
Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs

@@ -29,6 +29,11 @@ namespace Jint.Runtime.Interpreter.Statements
             var v = Undefined.Instance;
             var v = Undefined.Instance;
             while (true)
             while (true)
             {
             {
+                if (context.DebugMode)
+                {
+                    context.Engine.DebugHandler.OnStep(_test._expression);
+                }
+
                 var jsValue = _test.GetValue(context).Value;
                 var jsValue = _test.GetValue(context).Value;
                 if (!TypeConverter.ToBoolean(jsValue))
                 if (!TypeConverter.ToBoolean(jsValue))
                 {
                 {
@@ -57,4 +62,4 @@ namespace Jint.Runtime.Interpreter.Statements
             }
             }
         }
         }
     }
     }
-}
+}