Browse Source

Implementation of Debug support

Massimiliano Gentile 10 years ago
parent
commit
f077fe6fae

+ 145 - 4
Jint.Tests/Runtime/EngineTests.cs

@@ -1,12 +1,13 @@
-using System;
+using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Reflection;
 using System.Reflection;
-using System.Threading;
+using System.Threading;
 using Jint.Native.Number;
 using Jint.Native.Number;
 using Jint.Parser;
 using Jint.Parser;
 using Jint.Parser.Ast;
 using Jint.Parser.Ast;
-using Jint.Runtime;
+using Jint.Runtime;
+using Jint.Runtime.Debugger;
 using Xunit;
 using Xunit;
 using Xunit.Extensions;
 using Xunit.Extensions;
 using System.Net;
 using System.Net;
@@ -15,7 +16,9 @@ namespace Jint.Tests.Runtime
 {
 {
     public class EngineTests : IDisposable
     public class EngineTests : IDisposable
     {
     {
-        private readonly Engine _engine;
+        private readonly Engine _engine;
+        private int countBreak = 0;
+        private StepMode stepMode;
 
 
         public EngineTests()
         public EngineTests()
         {
         {
@@ -1211,6 +1214,144 @@ namespace Jint.Tests.Runtime
                 assert(nums[5] === 1.1);
                 assert(nums[5] === 1.1);
                 assert(nums[6] === 1);
                 assert(nums[6] === 1);
             ");
             ");
+        }
+
+        [Fact]
+        public void ShouldBreakWhenBreakpointIsReached()
+        {
+            countBreak = 0;
+            stepMode = StepMode.None;
+
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.Break += EngineStep;
+
+            engine.BreakPoints.Add(new BreakPoint(1, 1));
+
+            engine.Execute(@"var local = true;
+                if (local === true)
+                {}");
+
+            engine.Break -= EngineStep;
+
+            Assert.Equal(1, countBreak);
+        }
+
+        [Fact]
+        public void ShouldExecuteStepByStep()
+        {
+            countBreak = 0;
+            stepMode = StepMode.Into;
+
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.Step += EngineStep;
+            
+            engine.Execute(@"var local = true;
+                var creatingSomeOtherLine = 0;
+                var lastOneIPromise = true");
+
+            engine.Step -= EngineStep;
+
+            Assert.Equal(3, countBreak);
+        }
+
+        [Fact]
+        public void ShouldNotBreakTwiceIfSteppingOverBreakpoint()
+        {
+            countBreak = 0;
+            stepMode = StepMode.Into;
+
+            var engine = new Engine(options => options.DebugMode());
+            engine.BreakPoints.Add(new BreakPoint(1, 1));
+            engine.Step += EngineStep;
+            engine.Break += EngineStep;
+
+            engine.Execute(@"var local = true;");
+
+            engine.Step -= EngineStep;
+            engine.Break -= EngineStep;
+
+            Assert.Equal(1, countBreak);
+        }
+        
+        private StepMode EngineStep(object sender, DebugInformation debugInfo)
+        {
+            Assert.NotNull(sender);
+            Assert.IsType(typeof(Engine), sender);
+            Assert.NotNull(debugInfo);
+
+            countBreak++;
+            return stepMode;
+        }
+
+        [Fact]
+        public void ShouldShowProperDebugInformation()
+        {
+            countBreak = 0;
+            stepMode = StepMode.None;
+
+            var engine = new Engine(options => options.DebugMode());
+            engine.BreakPoints.Add(new BreakPoint(5, 0));
+            engine.Break += EngineStepVerifyDebugInfo;
+
+            engine.Execute(@"var global = true;
+                            function func1()
+                            {
+                                var local = false;
+;
+                            }
+                            func1();");
+
+            engine.Break -= EngineStepVerifyDebugInfo;
+
+            Assert.Equal(1, countBreak);
+        }
+
+        private StepMode EngineStepVerifyDebugInfo(object sender, DebugInformation debugInfo)
+        {
+            Assert.NotNull(sender);
+            Assert.IsType(typeof(Engine), sender);
+            Assert.NotNull(debugInfo);
+
+            Assert.NotNull(debugInfo.CallStack);
+            Assert.NotNull(debugInfo.CurrentStatement);
+            Assert.NotNull(debugInfo.Locals);
+
+            Assert.Equal(1, debugInfo.CallStack.Count);
+            Assert.Equal("func1()", debugInfo.CallStack.Peek());
+            Assert.Contains(debugInfo.Locals, kvp => kvp.Key.Equals("global", StringComparison.Ordinal) && kvp.Value.AsBoolean() == true);
+            Assert.Contains(debugInfo.Locals, kvp => kvp.Key.Equals("local", StringComparison.Ordinal) && kvp.Value.AsBoolean() == false);
+
+
+            countBreak++;
+            return stepMode;
+        }
+
+        [Fact]
+        public void ShouldBreakWhenConditionIsMatched()
+        {
+            countBreak = 0;
+            stepMode = StepMode.None;
+
+            var engine = new Engine(options => options.DebugMode());
+
+            engine.Break += EngineStep;
+
+            engine.BreakPoints.Add(new BreakPoint(5, 16, "condition === true"));
+            engine.BreakPoints.Add(new BreakPoint(6, 16, "condition === false"));
+
+            engine.Execute(@"var local = true;
+                var condition = true;
+                if (local === true)
+                {
+                ;
+                ;
+                }");
+
+            engine.Break -= EngineStep;
+
+            Assert.Equal(1, countBreak);
         }
         }
     }
     }
 }
 }

+ 37 - 2
Jint/Engine.cs

@@ -18,6 +18,7 @@ using Jint.Native.String;
 using Jint.Parser;
 using Jint.Parser;
 using Jint.Parser.Ast;
 using Jint.Parser.Ast;
 using Jint.Runtime;
 using Jint.Runtime;
+using Jint.Runtime.Debugger;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interop;
 using Jint.Runtime.Interop;
@@ -138,6 +139,8 @@ namespace Jint
             }
             }
 
 
             ClrTypeConverter = new DefaultTypeConverter(this);
             ClrTypeConverter = new DefaultTypeConverter(this);
+            BreakPoints = new List<BreakPoint>();
+            DebugHandler = new DebugHandler(this);
         }
         }
 
 
         public LexicalEnvironment GlobalEnvironment;
         public LexicalEnvironment GlobalEnvironment;
@@ -166,6 +169,33 @@ namespace Jint
         public ExecutionContext ExecutionContext { get { return _executionContexts.Peek(); } }
         public ExecutionContext ExecutionContext { get { return _executionContexts.Peek(); } }
 
 
         internal Options Options { get; private set; }
         internal Options Options { get; private set; }
+        
+        #region Debugger
+        public delegate StepMode DebugStepDelegate(object sender, DebugInformation e);
+        public delegate StepMode BreakDelegate(object sender, DebugInformation e);
+        public event DebugStepDelegate Step;
+        public event BreakDelegate Break;
+        internal DebugHandler DebugHandler { get; private set; }
+        public List<BreakPoint> BreakPoints { get; private set; }
+
+        internal StepMode? InvokeStepEvent(DebugInformation info)
+        {
+            if (Step != null)
+            {
+                return Step(this, info);
+            }
+            return null;
+        }
+
+        internal StepMode? InvokeBreakEvent(DebugInformation info)
+        {
+            if (Break != null)
+            {
+                return Break(this, info);
+            }
+            return null;
+        }
+        #endregion
 
 
         public ExecutionContext EnterExecutionContext(LexicalEnvironment lexicalEnvironment, LexicalEnvironment variableEnvironment, JsValue thisBinding)
         public ExecutionContext EnterExecutionContext(LexicalEnvironment lexicalEnvironment, LexicalEnvironment variableEnvironment, JsValue thisBinding)
         {
         {
@@ -301,6 +331,11 @@ namespace Jint
             }
             }
 
 
             _lastSyntaxNode = statement;
             _lastSyntaxNode = statement;
+            
+            if (Options.IsDebugMode())
+            {
+                DebugHandler.OnStep(statement);
+            }
 
 
             switch (statement.Type)
             switch (statement.Type)
             {
             {
@@ -368,10 +403,10 @@ namespace Jint
                     throw new ArgumentOutOfRangeException();
                     throw new ArgumentOutOfRangeException();
             }
             }
         }
         }
-
+        
         public object EvaluateExpression(Expression expression)
         public object EvaluateExpression(Expression expression)
         {
         {
-            _lastSyntaxNode = expression;
+			_lastSyntaxNode = expression;
 
 
             switch (expression.Type)
             switch (expression.Type)
             {
             {

+ 5 - 1
Jint/Jint.csproj

@@ -163,6 +163,10 @@
     <Compile Include="Runtime\CallStack\CallStackElementComparer.cs" />
     <Compile Include="Runtime\CallStack\CallStackElementComparer.cs" />
     <Compile Include="Runtime\CallStack\CallStackElement.cs" />
     <Compile Include="Runtime\CallStack\CallStackElement.cs" />
     <Compile Include="Runtime\Completion.cs" />
     <Compile Include="Runtime\Completion.cs" />
+    <Compile Include="Runtime\Debugger\BreakPoint.cs" />
+    <Compile Include="Runtime\Debugger\DebugHandler.cs" />
+    <Compile Include="Runtime\Debugger\DebugInformation.cs" />
+    <Compile Include="Runtime\Debugger\StepMode.cs" />
     <Compile Include="Runtime\Descriptors\PropertyDescriptor.cs" />
     <Compile Include="Runtime\Descriptors\PropertyDescriptor.cs" />
     <Compile Include="Runtime\Descriptors\Specialized\FieldInfoDescriptor.cs" />
     <Compile Include="Runtime\Descriptors\Specialized\FieldInfoDescriptor.cs" />
     <Compile Include="Runtime\Descriptors\Specialized\IndexDescriptor.cs" />
     <Compile Include="Runtime\Descriptors\Specialized\IndexDescriptor.cs" />
@@ -212,4 +216,4 @@
   <Target Name="AfterBuild">
   <Target Name="AfterBuild">
   </Target>
   </Target>
   -->
   -->
-</Project>
+</Project>

+ 15 - 0
Jint/Options.cs

@@ -13,6 +13,7 @@ namespace Jint
         private bool _discardGlobal;
         private bool _discardGlobal;
         private bool _strict;
         private bool _strict;
         private bool _allowDebuggerStatement;
         private bool _allowDebuggerStatement;
+        private bool _debugMode;
         private bool _allowClr;
         private bool _allowClr;
         private readonly List<IObjectConverter> _objectConverters = new List<IObjectConverter>();
         private readonly List<IObjectConverter> _objectConverters = new List<IObjectConverter>();
         private int _maxStatements;
         private int _maxStatements;
@@ -54,6 +55,15 @@ namespace Jint
             return this;
             return this;
         }
         }
 
 
+        /// <summary>
+        /// Allow to run the script in debug mode.
+        /// </summary>
+        public Options DebugMode(bool debugMode = true)
+        {
+            _debugMode = debugMode;
+            return this;
+        }
+
         /// <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>
@@ -128,6 +138,11 @@ namespace Jint
             return _allowDebuggerStatement;
             return _allowDebuggerStatement;
         }
         }
 
 
+        internal bool IsDebugMode()
+        {
+            return _debugMode;
+        }
+
         internal bool IsClrAllowed()
         internal bool IsClrAllowed()
         {
         {
             return _allowClr;
             return _allowClr;

+ 21 - 0
Jint/Runtime/Debugger/BreakPoint.cs

@@ -0,0 +1,21 @@
+namespace Jint.Runtime.Debugger
+{
+    public class BreakPoint
+    {
+        public int Line { get; set; }
+        public int Char { get; set; }
+        public string Condition { get; set; }
+
+        public BreakPoint(int line, int character)
+        {
+            Line = line;
+            Char = character;
+        }
+
+        public BreakPoint(int line, int character, string condition)
+            : this(line, character)
+        {
+            Condition = condition;
+        }
+    }
+}

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

@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Jint.Native;
+using Jint.Parser.Ast;
+using Jint.Runtime.Environments;
+using Jint.Runtime.References;
+
+namespace Jint.Runtime.Debugger
+{
+    internal class DebugHandler
+    {
+        private readonly Stack<string> _debugCallStack;
+        private StepMode _stepMode;
+        private int _callBackStepOverDepth;
+        private readonly Engine _engine;
+
+        public DebugHandler(Engine engine)
+        {
+            _engine = engine;
+            _debugCallStack = new Stack<string>();
+            _stepMode = StepMode.Into;
+        }
+
+        internal void PopDebugCallStack()
+        {
+            if (_debugCallStack.Count > 0)
+            {
+                _debugCallStack.Pop();
+            }
+            if (_stepMode == StepMode.Over && _debugCallStack.Count <= _callBackStepOverDepth)
+            {
+                _callBackStepOverDepth = _debugCallStack.Count;
+                _stepMode = StepMode.Into;
+            }
+        }
+
+        internal void AddToDebugCallStack(CallExpression callExpression)
+        {
+            var identifier = callExpression.Callee as Identifier;
+            if (identifier != null)
+            {
+                var stack = identifier.Name + "(";
+                var paramStrings = new List<string>();
+
+                foreach (var argument in callExpression.Arguments)
+                {
+                    if (argument != null)
+                    {
+                        var argIdentifier = argument as Identifier;
+                        paramStrings.Add(argIdentifier != null ? argIdentifier.Name : "null");
+                    }
+                    else
+                    {
+                        paramStrings.Add("null");
+                    }
+                }
+
+                stack += string.Join(", ", paramStrings);
+                stack += ")";
+                _debugCallStack.Push(stack);
+            }
+        }
+
+        internal void OnStep(Statement statement)
+        {
+            var old = _stepMode;
+            if (statement == null)
+            {
+                return;
+            }
+
+            
+            
+            BreakPoint breakpoint = _engine.BreakPoints.FirstOrDefault(breakPoint => BpTest(statement, breakPoint));
+            bool breakpointFound = false;
+
+            if (breakpoint != null)
+            {
+                DebugInformation info = CreateDebugInformation(statement);
+                var result = _engine.InvokeBreakEvent(info);
+                if (result.HasValue)
+                {
+                    _stepMode = result.Value;
+                    breakpointFound = true;
+                }
+            }
+
+            if (breakpointFound == false && _stepMode == StepMode.Into)
+            {
+                DebugInformation info = CreateDebugInformation(statement);
+                var result = _engine.InvokeStepEvent(info);
+                if (result.HasValue)
+                {
+                    _stepMode = result.Value;
+                }
+            }
+
+            if (old == StepMode.Into && _stepMode == StepMode.Over)
+            {
+                _callBackStepOverDepth = _debugCallStack.Count;
+            }
+        }
+
+        private bool BpTest(Statement statement, BreakPoint breakpoint)
+        {
+            bool afterStart, beforeEnd;
+
+            afterStart = (breakpoint.Line == statement.Location.Start.Line &&
+                          breakpoint.Char >= statement.Location.Start.Column);
+
+            if (!afterStart)
+            {
+                return false;
+            }
+
+            beforeEnd = (breakpoint.Line == statement.Location.End.Line &&
+                         breakpoint.Char <= statement.Location.End.Column);
+
+            if (!beforeEnd)
+            {
+                return false;
+            }
+
+            if (!string.IsNullOrEmpty(breakpoint.Condition))
+            {
+                return _engine.Execute(breakpoint.Condition).GetCompletionValue().AsBoolean();
+            }
+
+            return true;
+        }
+
+        private DebugInformation CreateDebugInformation(Statement statement)
+        {
+            var info = new DebugInformation { CurrentStatement = statement, CallStack = _debugCallStack };
+
+            if (_engine.ExecutionContext != null && _engine.ExecutionContext.LexicalEnvironment != null)
+            {
+                info.Locals = GetIdentifierReference(_engine.ExecutionContext.LexicalEnvironment);
+            }
+
+            return info;
+        }
+
+        public static Dictionary<string, JsValue> GetIdentifierReference(LexicalEnvironment lex)
+        {
+            Dictionary<string, JsValue> locals = new Dictionary<string, JsValue>();
+            LexicalEnvironment tempLex = lex;
+
+            while (tempLex != null && tempLex.Record != null)
+            {
+                var bindings = tempLex.Record.GetAllBindingNames();
+                foreach (var binding in bindings)
+                {
+                    if (locals.ContainsKey(binding) == false)
+                    {
+                        var jsValue = tempLex.Record.GetBindingValue(binding, false);
+                        if (jsValue.TryCast<ICallable>() == null)
+                        {
+                            locals.Add(binding, jsValue);
+                        }
+                    }
+                }
+                tempLex = tempLex.Outer;
+            }
+            return locals;
+        }
+    }
+}

+ 15 - 0
Jint/Runtime/Debugger/DebugInformation.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using Jint.Native;
+using Jint.Parser.Ast;
+using Jint.Runtime.Environments;
+
+namespace Jint.Runtime.Debugger
+{
+    public class DebugInformation : EventArgs
+    {
+        public Stack<String> CallStack { get; set; }
+        public Statement CurrentStatement { get; set; }
+        public Dictionary<string, JsValue> Locals { get; set; }
+    }
+}

+ 9 - 0
Jint/Runtime/Debugger/StepMode.cs

@@ -0,0 +1,9 @@
+namespace Jint.Runtime.Debugger
+{
+    public enum StepMode
+    {
+        None,
+        Over,
+        Into
+    }
+}

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

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using Jint.Native;
 using Jint.Native;
 
 
 namespace Jint.Runtime.Environments
 namespace Jint.Runtime.Environments
@@ -112,5 +113,14 @@ namespace Jint.Runtime.Environments
             var binding = _bindings[name];
             var binding = _bindings[name];
             binding.Value = value;
             binding.Value = value;
         }
         }
+
+        /// <summary>
+        /// Returns an array of all the defined binding names
+        /// </summary>
+        /// <returns>The array of all defined bindings</returns>
+        public override string[] GetAllBindingNames()
+        {
+            return _bindings.Keys.ToArray();
+        }
     }
     }
 }
 }

+ 5 - 1
Jint/Runtime/Environments/EnvironmentRecord.cs

@@ -56,6 +56,10 @@ namespace Jint.Runtime.Environments
         /// <returns>The value to use as <c>this</c>.</returns>
         /// <returns>The value to use as <c>this</c>.</returns>
         public abstract JsValue ImplicitThisValue();
         public abstract JsValue ImplicitThisValue();
 
 
-
+        /// <summary>
+        /// Returns an array of all the defined binding names
+        /// </summary>
+        /// <returns>The array of all defined bindings</returns>
+        public abstract string[] GetAllBindingNames();
     }
     }
 }
 }

+ 11 - 1
Jint/Runtime/Environments/ObjectEnvironmentRecord.cs

@@ -1,4 +1,5 @@
-using Jint.Native;
+using System.Linq;
+using Jint.Native;
 using Jint.Native.Object;
 using Jint.Native.Object;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Descriptors;
 
 
@@ -72,5 +73,14 @@ namespace Jint.Runtime.Environments
 
 
             return Undefined.Instance;
             return Undefined.Instance;
         }
         }
+
+        public override string[] GetAllBindingNames()
+        {
+            if (_bindingObject != null && _bindingObject.Properties != null)
+            {
+                return _bindingObject.Properties.Keys.ToArray();
+            }
+            return new string[0];
+        }
     }
     }
 }
 }

+ 10 - 0
Jint/Runtime/ExpressionIntepreter.cs

@@ -789,6 +789,11 @@ namespace Jint.Runtime
         {
         {
             var callee = EvaluateExpression(callExpression.Callee);
             var callee = EvaluateExpression(callExpression.Callee);
 
 
+            if (_engine.Options.IsDebugMode())
+            {
+                _engine.DebugHandler.AddToDebugCallStack(callExpression);
+            }
+
             JsValue thisObject;
             JsValue thisObject;
 
 
             // todo: implement as in http://www.ecma-international.org/ecma-262/5.1/#sec-11.2.4
             // todo: implement as in http://www.ecma-international.org/ecma-262/5.1/#sec-11.2.4
@@ -853,6 +858,11 @@ namespace Jint.Runtime
             
             
             var result = callable.Call(thisObject, arguments);
             var result = callable.Call(thisObject, arguments);
 
 
+            if (_engine.Options.IsDebugMode())
+            {
+                _engine.DebugHandler.PopDebugCallStack();
+            }
+
             if (isRecursionHandled)
             if (isRecursionHandled)
             {
             {
                 _engine.CallStack.Pop();
                 _engine.CallStack.Pop();