├── .gitignore ├── Jint.DebuggerExample ├── Data │ ├── main.js │ ├── index.html │ ├── example-module.js │ ├── fields-example-script.js │ └── silly-example-script.js ├── CommandException.cs ├── ProgramException.cs ├── Jint.DebuggerExample.csproj.user ├── Properties │ └── launchSettings.json ├── ExtendedBreakPoint.cs ├── GlobalSuppressions.cs ├── EsprimaPositionComparer.cs ├── Jint.DebuggerExample.csproj ├── Helpers │ ├── ConsoleHelpers.cs │ └── StringCropExtensions.cs ├── SourceManager.cs ├── Program.cs ├── BreakPointCollector.cs ├── ValueRenderer.cs ├── SourceInfo.cs ├── ConsoleObject.cs ├── CommandLine.cs └── Debugger.cs ├── LICENSE.md ├── README.md ├── Jint.DebuggerExample.sln └── NOTES.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Data/main.js: -------------------------------------------------------------------------------- 1 | import test from "./example-module.js"; 2 | 3 | test.init("Hello world!"); 4 | test.run(); 5 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/CommandException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JintDebuggerExample; 4 | 5 | /// 6 | /// Thrown on debugger command syntax errors. 7 | /// 8 | internal class CommandException : Exception 9 | { 10 | public CommandException(string? message) : base(message) 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Data/example-module.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | init(prefix) { 3 | this.prefix = prefix; 4 | } 5 | 6 | run() { 7 | this.values = []; 8 | for (let i = 0; i < 10; i++) { 9 | this.values.push(this.prefix + i); 10 | } 11 | } 12 | } 13 | 14 | export default new Test(); 15 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Data/fields-example-script.js: -------------------------------------------------------------------------------- 1 | class C 2 | { 3 | #field = 123; 4 | get value() 5 | { 6 | debugger; 7 | return this.#field; 8 | } 9 | } 10 | 11 | const c = new C(); 12 | console.log(c.value); 13 | console.warn(c.value); 14 | console.error(c.value); 15 | console.debug(c.value); 16 | console.info(c.value); 17 | 18 | 19 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/ProgramException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JintDebuggerExample; 4 | 5 | /// 6 | /// Thrown for common fatal program errors that should get a nice message rather than a stack trace. 7 | /// 8 | internal class ProgramException : Exception 9 | { 10 | public ProgramException(string? message) : base(message) 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Jint.DebuggerExample.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | Debug fields example 8 | 9 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WSL": { 4 | "commandName": "WSL2", 5 | "distributionName": "" 6 | }, 7 | "Debug single script": { 8 | "commandName": "Project", 9 | "commandLineArgs": "Data\\silly-example-script.js" 10 | }, 11 | "Debug module": { 12 | "commandName": "Project", 13 | "commandLineArgs": "Data\\main.js -m" 14 | }, 15 | "Debug fields example": { 16 | "commandName": "Project", 17 | "commandLineArgs": "Data\\fields-example-script.js" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Jint.DebuggerExample/ExtendedBreakPoint.cs: -------------------------------------------------------------------------------- 1 | using Jint.Runtime.Debugger; 2 | 3 | namespace JintDebuggerExample; 4 | 5 | /// 6 | /// Simple example of extending breakpoints. This one uses gdb's idea of a temporary breakpoint for single use - i.e. 7 | /// it's removed when hit. 8 | /// 9 | internal class ExtendedBreakPoint : BreakPoint 10 | { 11 | public bool Temporary { get; } 12 | 13 | public ExtendedBreakPoint(string? source, int line, int column, string? condition = null, bool temporary = false) 14 | : base(source, line, column, condition) 15 | { 16 | Temporary = temporary; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "May be good for performance, not necessarily good for API")] 9 | [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "Not a fan of simple using statements - hide scope")] 10 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/EsprimaPositionComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Esprima; 3 | 4 | namespace JintDebuggerExample; 5 | 6 | /// 7 | /// Does comparison of Esprima positions (line/column) for binary search - until Esprima.Position might implement the 8 | /// IComparable interface. 9 | /// 10 | internal class EsprimaPositionComparer : IComparer 11 | { 12 | public static readonly EsprimaPositionComparer Default = new(); 13 | 14 | public int Compare(Position a, Position b) 15 | { 16 | if (a.Line != b.Line) 17 | { 18 | return a.Line - b.Line; 19 | } 20 | return a.Column - b.Column; 21 | } 22 | } -------------------------------------------------------------------------------- /Jint.DebuggerExample/Jint.DebuggerExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | disable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Always 17 | 18 | 19 | Always 20 | 21 | 22 | Always 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Data/silly-example-script.js: -------------------------------------------------------------------------------- 1 | const example = { 2 | regex: /[a-z0-9]/i, 3 | date: new Date(), 4 | boolean: true, 5 | string: "Here's a string\nwith new-line", 6 | number: 3.14159265359, 7 | bigint: 340282366920938463463374607431768211456n, 8 | null: null, 9 | undefined: undefined, 10 | symbol: Symbol("testsymbol"), 11 | numSymbol: Symbol(1), 12 | array: [], 13 | byteArray: new Uint8Array(5), 14 | intArray: new Int32Array(10), 15 | 16 | get myGetter() 17 | { 18 | return "A property getter value!"; 19 | } 20 | }; 21 | 22 | function testFunction(a, b) 23 | { 24 | let x = arguments.length; 25 | const arr = []; 26 | debugger; 27 | while (true) 28 | { 29 | arr.push(x); 30 | if (x >= 500) 31 | { 32 | break; 33 | } 34 | x++; 35 | } 36 | } 37 | 38 | for (let i = 0; i < 1000; i++) 39 | { 40 | example.array.push(i); 41 | } 42 | 43 | testFunction(5, "Hello Jint!"); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Jither 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (Relatively) Simple Jint Debugger Example 2 | ========================================= 3 | A relatively basic console application demonstrating many of the concepts needed to implement an interactive debugger for [Jint](https://github.com/sebastienros/jint). Currently builds against the latest `main` branch and .NET 6.0. 4 | 5 | * Stepping (into/over/out) 6 | * Setting, deleting, and clearing break points 7 | * Example of extending the `BreakPoint` class to add support for temporary break points 8 | * Displaying scopes and their bindings (variables and properties) 9 | * Displaying call stack 10 | * Evaluating expressions 11 | * Simple module support 12 | 13 | Usage 14 | ----- 15 | Simply call the executable with a path to a script file. The script will be paused before execution of the first line. Type `help` for a list of debugger commands. The listing shows full command name, short command name (if any), arguments (if any - angular brackets = required, square brackets = optional), and a description of the commmand. 16 | 17 | Modules 18 | ------- 19 | The example debugger supports modules in the base path of the provided script, when called with `-m` option. I.e., calling: 20 | 21 | Jint.DebuggerExample D:\app\main.js -m 22 | 23 | ... will allow imports from `D:\app`. 24 | 25 | import foo from "./foo.js" 26 | 27 | ... will import from `D:\app\foo.js`. 28 | -------------------------------------------------------------------------------- /Jint.DebuggerExample.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jint.DebuggerExample", "Jint.DebuggerExample\Jint.DebuggerExample.csproj", "{5612A754-7DB8-4A69-AF82-D1C9C33CD921}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jint", "..\Jint\Jint\Jint.csproj", "{02E3BA0D-99A5-4590-A4E3-93BB9A5B2CC6}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {5612A754-7DB8-4A69-AF82-D1C9C33CD921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {5612A754-7DB8-4A69-AF82-D1C9C33CD921}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {5612A754-7DB8-4A69-AF82-D1C9C33CD921}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {5612A754-7DB8-4A69-AF82-D1C9C33CD921}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {02E3BA0D-99A5-4590-A4E3-93BB9A5B2CC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {02E3BA0D-99A5-4590-A4E3-93BB9A5B2CC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {02E3BA0D-99A5-4590-A4E3-93BB9A5B2CC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {02E3BA0D-99A5-4590-A4E3-93BB9A5B2CC6}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {21D070F3-A78F-493C-8D07-7B98733DE9A7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Helpers/ConsoleHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace JintDebuggerExample.Helpers; 8 | 9 | internal static class ConsoleHelpers 10 | { 11 | private static bool IsColorEnabled => !Console.IsOutputRedirected && Environment.GetEnvironmentVariable("NO_COLOR") == null; 12 | 13 | public static string Color(string str, uint color) 14 | { 15 | if (!IsColorEnabled) 16 | { 17 | return str; 18 | } 19 | uint red = (color >> 16) & 0xff; 20 | uint green = (color >> 8) & 0xff; 21 | uint blue = color & 0xff; 22 | 23 | int returnToConsoleColor = ConsoleColorToAnsi(Console.ForegroundColor); 24 | return $"\x1b[38;2;{red};{green};{blue}m{str}\x1b[38;5;{returnToConsoleColor}m"; 25 | } 26 | 27 | private static int ConsoleColorToAnsi(ConsoleColor color) 28 | { 29 | return color switch 30 | { 31 | ConsoleColor.Black => 0, 32 | ConsoleColor.DarkRed => 1, 33 | ConsoleColor.DarkGreen => 2, 34 | ConsoleColor.DarkYellow => 3, 35 | ConsoleColor.DarkBlue => 4, 36 | ConsoleColor.DarkMagenta => 5, 37 | ConsoleColor.DarkCyan => 6, 38 | ConsoleColor.Gray => 7, 39 | ConsoleColor.DarkGray => 8, 40 | ConsoleColor.Red => 9, 41 | ConsoleColor.Green => 10, 42 | ConsoleColor.Yellow => 11, 43 | ConsoleColor.Blue => 12, 44 | ConsoleColor.Magenta => 13, 45 | ConsoleColor.Cyan => 14, 46 | ConsoleColor.White => 15, 47 | _ => throw new ArgumentException($"Unknown ConsoleColor: {color}"), 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/SourceManager.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Collections.Generic; 3 | using Esprima; 4 | 5 | namespace JintDebuggerExample; 6 | 7 | /// 8 | /// Manages the loaded scripts - although in this example debugger, there's only one. 9 | /// It does, however, give access to which helps with source 10 | /// line output and finding breakpoint locations. 11 | /// 12 | internal class SourceManager 13 | { 14 | private readonly Dictionary sourceInfoById = new(); 15 | 16 | public string Load(Esprima.Ast.Program ast, string sourceId, string path) 17 | { 18 | string script; 19 | try 20 | { 21 | script = File.ReadAllText(path); 22 | sourceInfoById.Add(sourceId, new SourceInfo(sourceId, script, ast)); 23 | return script; 24 | } 25 | catch (IOException ex) 26 | { 27 | throw new ProgramException($"Script could not be read: {ex.Message}"); 28 | } 29 | } 30 | 31 | public Position FindNearestBreakPointPosition(string sourceId, Position position) 32 | { 33 | var source = GetSourceInfo(sourceId); 34 | return source.FindNearestBreakPointPosition(position); 35 | } 36 | 37 | public string GetLine(Location location) 38 | { 39 | if (location.Source == null) 40 | { 41 | throw new ProgramException($"Location included no source ID"); 42 | } 43 | // We gave Esprima our source ID when we executed the script - so we can get it back from the location 44 | var source = GetSourceInfo(location.Source); 45 | 46 | return source.GetLine(location.Start); 47 | } 48 | 49 | private SourceInfo GetSourceInfo(string sourceId) 50 | { 51 | if (!sourceInfoById.TryGetValue(sourceId, out var info)) 52 | { 53 | throw new ProgramException($"Script with source ID '{sourceId}' was not found."); 54 | } 55 | return info; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using JintDebuggerExample.Helpers; 6 | 7 | namespace JintDebuggerExample; 8 | 9 | internal class Program 10 | { 11 | private class Options 12 | { 13 | public bool IsModule { get; } 14 | public string Path { get; } 15 | 16 | public Options(string path, bool isModule) 17 | { 18 | Path = path; 19 | IsModule = isModule; 20 | } 21 | } 22 | 23 | private static string Version => FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion ?? "1.0"; 24 | 25 | private static void Main(string[] args) 26 | { 27 | Console.WriteLine(ConsoleHelpers.Color($"Simple Jint Example Debugger v{Version}", 0xffcc00)); 28 | Console.WriteLine(); 29 | try 30 | { 31 | var options = ParseArguments(args); 32 | 33 | var basePath = Path.GetDirectoryName(options.Path); 34 | if (basePath == null) 35 | { 36 | throw new ProgramException("Couldn't determine base path for script."); 37 | } 38 | 39 | var debugger = new Debugger(basePath, options.IsModule); 40 | debugger.Execute(options.Path); 41 | } 42 | catch (ProgramException ex) 43 | { 44 | OutputError(ex); 45 | } 46 | } 47 | 48 | private static Options ParseArguments(string[] args) 49 | { 50 | string? path = null; 51 | bool isModule = false; 52 | foreach (var arg in args) 53 | { 54 | if (arg.StartsWith('-')) 55 | { 56 | switch (arg[1..]) 57 | { 58 | case "m": 59 | isModule = true; 60 | break; 61 | default: 62 | throw new ProgramException($"Unknown option: {arg}"); 63 | } 64 | } 65 | else 66 | { 67 | path = Path.GetFullPath(arg); 68 | } 69 | } 70 | 71 | if (path == null) 72 | { 73 | throw new ProgramException("No script/module path specified."); 74 | } 75 | 76 | return new Options(path, isModule); 77 | } 78 | 79 | private static void OutputError(Exception ex) 80 | { 81 | Console.WriteLine(ex.Message); 82 | OutputHelp(); 83 | } 84 | 85 | private static void OutputHelp() 86 | { 87 | string exeName = Path.GetFileName(Assembly.GetExecutingAssembly().Location) ?? "JintDebuggerExample"; 88 | Console.WriteLine($"Usage: {exeName} [options]"); 89 | Console.WriteLine("Options:"); 90 | Console.WriteLine(" -m Load script as module."); 91 | } 92 | } -------------------------------------------------------------------------------- /Jint.DebuggerExample/BreakPointCollector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Esprima; 3 | using Esprima.Ast; 4 | using Esprima.Utils; 5 | 6 | namespace JintDebuggerExample; 7 | 8 | /// 9 | /// Collects valid breakpoint locations. Eventually, this should probably be part of Jint itself, since it's really 10 | /// Jint that makes the decision about what locations are valid. 11 | /// 12 | public class BreakPointCollector : AstVisitor 13 | { 14 | private readonly List positions = new(); 15 | 16 | public List Positions => positions; 17 | 18 | public BreakPointCollector() 19 | { 20 | } 21 | 22 | public override object Visit(Node node) 23 | { 24 | if (node is Statement && node is not BlockStatement) 25 | { 26 | positions.Add(node.Location.Start); 27 | } 28 | base.Visit(node); 29 | 30 | return node; 31 | } 32 | 33 | protected override object VisitDoWhileStatement(DoWhileStatement doWhileStatement) 34 | { 35 | base.VisitDoWhileStatement(doWhileStatement); 36 | 37 | positions.Add(doWhileStatement.Test.Location.Start); 38 | 39 | return doWhileStatement; 40 | } 41 | 42 | protected override object VisitForInStatement(ForInStatement forInStatement) 43 | { 44 | base.VisitForInStatement(forInStatement); 45 | 46 | positions.Add(forInStatement.Left.Location.Start); 47 | 48 | return forInStatement; 49 | } 50 | 51 | protected override object VisitForOfStatement(ForOfStatement forOfStatement) 52 | { 53 | base.VisitForOfStatement(forOfStatement); 54 | 55 | positions.Add(forOfStatement.Left.Location.Start); 56 | 57 | return forOfStatement; 58 | } 59 | 60 | protected override object VisitForStatement(ForStatement forStatement) 61 | { 62 | base.VisitForStatement(forStatement); 63 | 64 | if (forStatement.Test != null) 65 | { 66 | positions.Add(forStatement.Test.Location.Start); 67 | } 68 | if (forStatement.Update != null) 69 | { 70 | positions.Add(forStatement.Update.Location.Start); 71 | } 72 | 73 | return forStatement; 74 | } 75 | 76 | protected override object VisitArrowFunctionExpression(ArrowFunctionExpression arrowFunctionExpression) 77 | { 78 | base.VisitArrowFunctionExpression(arrowFunctionExpression); 79 | 80 | positions.Add(arrowFunctionExpression.Body.Location.End); 81 | 82 | return arrowFunctionExpression; 83 | } 84 | 85 | protected override object VisitFunctionDeclaration(FunctionDeclaration functionDeclaration) 86 | { 87 | base.VisitFunctionDeclaration(functionDeclaration); 88 | 89 | positions.Add(functionDeclaration.Body.Location.End); 90 | 91 | return functionDeclaration; 92 | } 93 | 94 | protected override object VisitFunctionExpression(FunctionExpression function) 95 | { 96 | base.VisitFunctionExpression(function); 97 | 98 | positions.Add(function.Body.Location.End); 99 | 100 | return function; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/ValueRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Encodings.Web; 4 | using System.Text.Json; 5 | using Jint.Native; 6 | using Jint.Native.Function; 7 | using Jint.Native.Object; 8 | using JintDebuggerExample.Helpers; 9 | 10 | namespace Jint.DebuggerExample; 11 | 12 | /// 13 | /// A somewhat minimal approach to yielding useful output about variables, properties and objects. 14 | /// Jint.DebugAdapter has a much more complete example of handling various types of values. 15 | /// 16 | internal class ValueRenderer 17 | { 18 | private static readonly JsonSerializerOptions stringToJsonOptions = new() 19 | { 20 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 21 | }; 22 | 23 | public string RenderBinding(string name, JsValue? value) 24 | { 25 | string valueString = RenderValue(value); 26 | return RenderBinding(name, valueString); 27 | } 28 | 29 | // Although strings are implicitly converted to JsString, we don't want literal strings (e.g. "(...)") 30 | // JSON encoded, like JsString is - hence this overload of RenderBinding 31 | public string RenderBinding(string name, string value) 32 | { 33 | string croppedName = name.CropEnd(20); 34 | string croppedValue = value.CropEnd(55); 35 | return $"{croppedName,-20} : {croppedValue,-55}"; 36 | } 37 | 38 | public string RenderValue(JsValue? value, bool renderProperties = false) 39 | { 40 | return value switch 41 | { 42 | null => "null", 43 | JsString => JsonSerializer.Serialize(value.ToString(), stringToJsonOptions), 44 | Function func => RenderFunction(func), 45 | ObjectInstance obj => renderProperties ? RenderObject(obj) : obj.ToString(), 46 | _ => value.ToString() 47 | }; 48 | } 49 | 50 | private string RenderObject(ObjectInstance obj) 51 | { 52 | var result = new List(); 53 | foreach (var prop in obj.GetOwnProperties()) 54 | { 55 | string name = prop.Key.ToString(); 56 | if (prop.Value.Get != null) 57 | { 58 | result.Add(RenderBinding(name, "(...)")); 59 | } 60 | else 61 | { 62 | result.Add(RenderBinding(name, prop.Value.Value)); 63 | } 64 | } 65 | 66 | // Let's also output getters of the prototype chain 67 | var proto = obj.Prototype; 68 | while (proto != null && proto is not ObjectConstructor) 69 | { 70 | var props = proto.GetOwnProperties(); 71 | foreach (var prop in props) 72 | { 73 | if (prop.Value.Get != null) 74 | { 75 | result.Add(RenderBinding(prop.Key.ToString(), "(...)")); 76 | } 77 | } 78 | proto = proto.Prototype; 79 | } 80 | 81 | return String.Join(Environment.NewLine, result); 82 | } 83 | 84 | private string RenderFunction(Function func) 85 | { 86 | string result = func.ToString(); 87 | 88 | if (result.StartsWith("function ")) 89 | { 90 | result = string.Concat("ƒ ", result.AsSpan("function ".Length)); 91 | } 92 | 93 | return result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/SourceInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using Esprima; 4 | using Esprima.Ast; 5 | 6 | namespace JintDebuggerExample; 7 | 8 | /// 9 | /// Holds "metadata" information about a loaded script: Its source code, and valid breakpoint positions. 10 | /// 11 | internal class SourceInfo 12 | { 13 | private static readonly char[] lineBreaks = new[] { '\n', '\r' }; 14 | private readonly List linePositions = new(); 15 | private readonly List breakPointPositions; 16 | 17 | public string Id { get; } 18 | public string Source { get; } 19 | 20 | public SourceInfo(string id, string source, Esprima.Ast.Program ast) 21 | { 22 | Id = id; 23 | Source = source; 24 | LocateLines(); 25 | breakPointPositions = CollectBreakPointPositions(ast); 26 | } 27 | 28 | public string GetLine(Position position) 29 | { 30 | // Note that in Esprima, and hence Jint, the first line is line 1. The first column is column 0. 31 | int lineStart = linePositions[position.Line - 1]; 32 | 33 | // Don't include newline 34 | int lineEnd = linePositions[position.Line] - 1; 35 | 36 | // ... or carriage return, if it's there 37 | if (lineBreaks.Contains(Source[lineEnd])) 38 | { 39 | lineEnd--; 40 | } 41 | 42 | return Source[lineStart..lineEnd]; 43 | } 44 | 45 | public Position FindNearestBreakPointPosition(Position position) 46 | { 47 | var positions = breakPointPositions; 48 | int index = positions.BinarySearch(position, EsprimaPositionComparer.Default); 49 | if (index < 0) 50 | { 51 | // Get the first break after the candidate position 52 | index = ~index; 53 | } 54 | 55 | // If we're past the last position, just use the last position 56 | if (index >= positions.Count) 57 | { 58 | index = positions.Count - 1; 59 | } 60 | return positions[index]; 61 | } 62 | 63 | // Quick implementation of more memory efficient lookup of script lines than keeping a lines array. 64 | private void LocateLines() 65 | { 66 | int linePosition = 0; 67 | while (true) 68 | { 69 | linePositions.Add(linePosition); 70 | 71 | linePosition = Source.IndexOfAny(lineBreaks, linePosition); 72 | if (linePosition < 0) 73 | { 74 | break; 75 | } 76 | linePosition++; 77 | if (Source[linePosition - 1] == '\r') 78 | { 79 | // Carriage return may be followed by newline, which also needs skipping 80 | if (linePosition < Source.Length && Source[linePosition] == '\n') 81 | { 82 | linePosition++; 83 | } 84 | } 85 | } 86 | linePositions.Add(Source.Length); 87 | } 88 | 89 | private List CollectBreakPointPositions(Esprima.Ast.Program ast) 90 | { 91 | var collector = new BreakPointCollector(); 92 | collector.Visit(ast); 93 | 94 | // We need positions distinct and sorted (they'll be used in binary search) 95 | var positions = collector.Positions.Distinct().ToList(); 96 | positions.Sort(EsprimaPositionComparer.Default); 97 | 98 | return positions; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Helpers/StringCropExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace JintDebuggerExample.Helpers; 8 | 9 | public static class StringCropExtensions 10 | { 11 | /// 12 | /// Crops end of string to make it fit a maximum length, including a separator (e.g. ellipsis). 13 | /// 14 | /// 15 | /// Note that the string is not guaranteed to be cropped to exactly max length, if the string includes 16 | /// surrogate pairs (32-bit code points). In that case, it may be shorter, in order to not split a 17 | /// pair. 18 | /// 19 | public static string CropEnd(this string str, int maxLength, string ellipsis = "...") 20 | { 21 | if (maxLength < ellipsis.Length) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, 24 | $"{nameof(maxLength)} should be >= length of {nameof(ellipsis)} (i.e. {ellipsis.Length})."); 25 | } 26 | if (str.Length <= maxLength) 27 | { 28 | return str; 29 | } 30 | int length = maxLength - ellipsis.Length; 31 | if (length <= 0) 32 | { 33 | return ellipsis; 34 | } 35 | if (Char.IsSurrogatePair(str, length - 1)) 36 | { 37 | length--; 38 | } 39 | 40 | return string.Concat(str.AsSpan(0, length), ellipsis); 41 | } 42 | 43 | /// 44 | /// Crops start of string to make it fit a maximum length, including a separator (e.g. ellipsis). 45 | /// 46 | /// 47 | /// Note that the string is not guaranteed to be cropped to exactly max length, if the string includes 48 | /// surrogate pairs (32-bit code points). In that case, it may be shorter, in order to not split a 49 | /// pair. 50 | /// 51 | public static string CropStart(this string str, int maxLength, string ellipsis = "...") 52 | { 53 | if (maxLength < ellipsis.Length) 54 | { 55 | throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, 56 | $"{nameof(maxLength)} should be >= length of {nameof(ellipsis)} (i.e. {ellipsis.Length})."); 57 | } 58 | if (str.Length <= maxLength) 59 | { 60 | return str; 61 | } 62 | int start = str.Length - (maxLength - ellipsis.Length); 63 | if (start >= str.Length) 64 | { 65 | return ellipsis; 66 | } 67 | if (start > 0 && Char.IsSurrogatePair(str, start - 1)) 68 | { 69 | start++; 70 | } 71 | 72 | return string.Concat(ellipsis, str.AsSpan(start)); 73 | } 74 | 75 | /// 76 | /// Crops middle of string to make it fit a maximum length, including a separator (e.g. ellipsis). 77 | /// 78 | /// 79 | /// Note that the string is not guaranteed to be cropped to exactly max length, if the string includes 80 | /// surrogate pairs (32-bit code points). In that case, it may be shorter, in order to not split a 81 | /// pair. 82 | /// 83 | public static string CropMiddle(this string str, int maxLength, string ellipsis = "...") 84 | { 85 | if (maxLength < ellipsis.Length) 86 | { 87 | throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, 88 | $"{nameof(maxLength)} should be >= length of {nameof(ellipsis)} (i.e. {ellipsis.Length})."); 89 | } 90 | if (str.Length <= maxLength) 91 | { 92 | return str; 93 | } 94 | 95 | if (maxLength == ellipsis.Length) 96 | { 97 | return ellipsis; 98 | } 99 | 100 | maxLength -= ellipsis.Length; 101 | int leftLength = maxLength / 2; 102 | int rightLength = maxLength - leftLength; 103 | 104 | // No space for anything except separator (or a single character on one side) 105 | if (rightLength <= 0 || leftLength <= 0) 106 | { 107 | return ellipsis; 108 | } 109 | 110 | // Ensure we're not splitting surrogate pairs: 111 | if (Char.IsSurrogatePair(str, str.Length - rightLength - 1)) 112 | { 113 | leftLength++; 114 | rightLength--; 115 | } 116 | 117 | if (Char.IsSurrogatePair(str, leftLength - 1)) 118 | { 119 | leftLength--; 120 | } 121 | 122 | return string.Concat( 123 | str.AsSpan(0, leftLength), 124 | ellipsis, 125 | str.AsSpan(str.Length - rightLength, rightLength) 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/ConsoleObject.cs: -------------------------------------------------------------------------------- 1 | using Jint.Native; 2 | using Jint.Runtime; 3 | using JintDebuggerExample; 4 | using JintDebuggerExample.Helpers; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Globalization; 9 | using System.Linq; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace Jint.DebuggerExample; 13 | 14 | internal class ConsoleObject 15 | { 16 | internal enum LogType 17 | { 18 | Debug, 19 | Log, 20 | Info, 21 | Warn, 22 | Error, 23 | } 24 | 25 | private readonly int engineThreadId; 26 | private readonly CommandLine commandLine; 27 | private readonly Dictionary timers = new(); 28 | private readonly Dictionary counters = new(); 29 | private readonly ValueRenderer renderer = new(); 30 | 31 | public ConsoleObject(CommandLine commandLine) 32 | { 33 | this.commandLine = commandLine; 34 | this.engineThreadId = Environment.CurrentManagedThreadId; 35 | } 36 | 37 | public void Assert(JsValue assertion, params JsValue[] values) 38 | { 39 | if (!TypeConverter.ToBoolean(assertion)) 40 | { 41 | Error(new JsValue[] { "Assertion failed:" }.Concat(values).ToArray()); 42 | } 43 | } 44 | 45 | public void Clear() 46 | { 47 | commandLine.Clear(); 48 | } 49 | 50 | public void Count(string label = null) 51 | { 52 | label ??= "default"; 53 | 54 | if (!counters.TryGetValue(label, out var count)) 55 | { 56 | count = 0; 57 | } 58 | count++; 59 | counters[label] = count; 60 | Log($"{label}: {count}"); 61 | } 62 | 63 | public void CountReset(string label = null) 64 | { 65 | label ??= "default"; 66 | 67 | if (!counters.ContainsKey(label)) 68 | { 69 | Warn($"Count for '{label}' does not exist."); 70 | return; 71 | } 72 | 73 | counters[label] = 0; 74 | Log($"{label}: 0"); 75 | } 76 | 77 | public void Debug(params JsValue[] values) 78 | { 79 | Log(LogType.Debug, values); 80 | } 81 | 82 | // TODO: Dir(), DirXml() 83 | 84 | public void Error(params JsValue[] values) 85 | { 86 | Log(LogType.Error, values); 87 | } 88 | 89 | // TODO: Groups 90 | /* 91 | public void Group(string label) 92 | { 93 | InternalSend(OutputCategory.Stdout, label, group: OutputGroup.Start); 94 | } 95 | 96 | public void GroupCollapsed(string label) 97 | { 98 | InternalSend(OutputCategory.Stdout, label, group: OutputGroup.StartCollapsed); 99 | } 100 | 101 | public void GroupEnd() 102 | { 103 | InternalSend(OutputCategory.Stdout, String.Empty, group: OutputGroup.End); 104 | } 105 | */ 106 | 107 | public void Info(params JsValue[] values) 108 | { 109 | Log(LogType.Info, values); 110 | } 111 | 112 | public void Log(params JsValue[] values) 113 | { 114 | Log(LogType.Log, values); 115 | } 116 | 117 | private static readonly Regex rxLineBreaks = new(@"\r?\n"); 118 | 119 | private void Log(LogType type, params JsValue[] values) 120 | { 121 | var valuesString = String.Join(' ', values.Select(v => renderer.RenderValue(v, renderProperties: true))); 122 | 123 | // Result may include line breaks - log prefix should be added to each of them: 124 | var lines = rxLineBreaks.Split(valuesString); 125 | 126 | uint color = type switch 127 | { 128 | LogType.Log => 0x40a4d8, 129 | LogType.Error => 0xdc3839, 130 | LogType.Warn => 0xfecc2f, 131 | LogType.Info => 0xb2c444, 132 | _ => 0xa0a0a0 133 | }; 134 | string typeName = type.ToString().ToLowerInvariant().PadRight(5, ' '); 135 | foreach (var line in lines) 136 | { 137 | commandLine.Output($"[{ConsoleHelpers.Color(typeName, color)}] {line}"); 138 | } 139 | } 140 | 141 | // TODO: Table() 142 | 143 | public void Time(string label = null) 144 | { 145 | label ??= "default"; 146 | 147 | timers[label] = Stopwatch.GetTimestamp(); 148 | } 149 | 150 | public void TimeEnd(string label = null) 151 | { 152 | InternalTimeLog(label, end: true); 153 | } 154 | 155 | public void TimeLog(string label = null) 156 | { 157 | InternalTimeLog(label, end: false); 158 | } 159 | 160 | private void InternalTimeLog(string label, bool end) 161 | { 162 | label ??= "default"; 163 | 164 | if (!timers.TryGetValue(label, out var started)) 165 | { 166 | Warn($"Timer '{label}' does not exist."); 167 | return; 168 | } 169 | 170 | var elapsed = Stopwatch.GetTimestamp() - started; 171 | string ms = (elapsed / 10000d).ToString(CultureInfo.InvariantCulture); 172 | string message = $"{label}: {ms} ms"; 173 | if (end) 174 | { 175 | message += " - timer ended."; 176 | timers.Remove(label); 177 | } 178 | Log(message); 179 | } 180 | 181 | public void Trace() 182 | { 183 | // TODO: Stack trace from console.trace() 184 | } 185 | 186 | public void Warn(params JsValue[] values) 187 | { 188 | Log(LogType.Warn, values); 189 | } 190 | 191 | private void EnsureOnEngineThread() 192 | { 193 | System.Diagnostics.Debug.Assert(Environment.CurrentManagedThreadId == engineThreadId, 194 | "Console methods should only be called on engine thread"); 195 | } 196 | } -------------------------------------------------------------------------------- /Jint.DebuggerExample/CommandLine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Esprima; 4 | using Jint.DebuggerExample; 5 | using Jint.Native; 6 | using JintDebuggerExample.Helpers; 7 | 8 | namespace JintDebuggerExample; 9 | 10 | /// 11 | /// Handles debugger command line parsing, output and rendering. 12 | /// 13 | internal class CommandLine 14 | { 15 | /// 16 | /// Callback and metadata (for "help" generation) for a single debugger command 17 | /// 18 | private class CommandHandler 19 | { 20 | public Func Callback { get; } 21 | public string Description { get; } 22 | public string Command { get; } 23 | public string? ShortCommand { get; } 24 | public string? Parameters { get; } 25 | 26 | public CommandHandler(string description, Func callback, string command, string? shortCommand = null, string? parameters = null) 27 | { 28 | Description = description; 29 | Callback = callback; 30 | Command = command; 31 | ShortCommand = shortCommand; 32 | Parameters = parameters; 33 | } 34 | 35 | public override string ToString() 36 | { 37 | return $"{Command,-10} {ShortCommand,-5} {Parameters,-20} {Description,-40}"; 38 | } 39 | } 40 | 41 | private readonly Dictionary commandHandlersByCommand = new(); 42 | private readonly List commandHandlers = new(); 43 | private readonly ValueRenderer renderer = new(); 44 | 45 | public void Clear() 46 | { 47 | Console.Clear(); 48 | } 49 | 50 | /// 51 | /// The input loop. Keeps asking for a command until a valid command is entered, and it's handler returns 52 | /// true (handlers that continue execution). 53 | /// 54 | public void Input() 55 | { 56 | bool done; 57 | do 58 | { 59 | Console.Write(ConsoleHelpers.Color("> ", 0x88bbff)); 60 | string? commandLine = Console.ReadLine(); 61 | 62 | if (commandLine == null) 63 | { 64 | break; 65 | } 66 | 67 | done = HandleCommand(commandLine); 68 | } 69 | while (!done); 70 | } 71 | 72 | /// 73 | /// Registers a debugger command. This allows us to auto-generate the help. 74 | /// 75 | public void Register(string description, Func callback, string command, string? shortCommand = null, string? parameters = null) 76 | { 77 | if (commandHandlersByCommand.TryGetValue(command, out var existingHandler)) 78 | { 79 | throw new ArgumentException($"Command name '{command}' is already in use by {existingHandler}"); 80 | } 81 | 82 | var handler = new CommandHandler(description, callback, command, shortCommand, parameters); 83 | commandHandlers.Add(handler); 84 | commandHandlersByCommand.Add(command, handler); 85 | if (shortCommand != null) 86 | { 87 | if (commandHandlersByCommand.TryGetValue(shortCommand, out existingHandler)) 88 | { 89 | throw new ArgumentException($"Short command name '{command}' is already in use by {existingHandler}"); 90 | } 91 | commandHandlersByCommand.Add(shortCommand, handler); 92 | } 93 | } 94 | 95 | public void Output(string message) 96 | { 97 | Console.WriteLine(message); 98 | } 99 | 100 | public void OutputHelp() 101 | { 102 | foreach (var handler in commandHandlers) 103 | { 104 | Console.WriteLine(handler); 105 | } 106 | } 107 | 108 | /// 109 | /// Outputs a nice information line about the current position in the script. 110 | /// 111 | public void OutputPosition(Location location, string line) 112 | { 113 | var pos = location.Start; 114 | // Insert colored position marker: 115 | line = String.Concat( 116 | line.AsSpan(0, pos.Column), 117 | ConsoleHelpers.Color("»", 0x88cc55), 118 | line.AsSpan(pos.Column)); 119 | 120 | var locationString = ConsoleHelpers.Color($"{location.Source?.CropStart(20)} {pos.Line,4}:{pos.Column,4}", 0x909090); 121 | 122 | Output($"{locationString} {line}"); 123 | } 124 | 125 | /// 126 | /// Outputs a single binding or object property (name + value) 127 | /// 128 | public void OutputBinding(string name, JsValue? value) 129 | { 130 | Output(renderer.RenderBinding(name, value)); 131 | } 132 | 133 | /// 134 | /// Outputs a single JsValue. 135 | /// 136 | public void OutputValue(JsValue value) 137 | { 138 | string valueString = renderer.RenderValue(value, renderProperties: true); 139 | Output(valueString); 140 | } 141 | 142 | private bool HandleCommand(string commandLine) 143 | { 144 | // Split into command name and arguments (arguments as a single string) 145 | var parts = commandLine.Split(" ", 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 146 | if (parts.Length == 0) 147 | { 148 | // Nothing entered 149 | return false; 150 | } 151 | 152 | var command = parts[0]; 153 | 154 | try 155 | { 156 | if (!commandHandlersByCommand.TryGetValue(command, out var commandHandler)) 157 | { 158 | throw new CommandException($"Unknown command: {command}"); 159 | } 160 | 161 | var arguments = parts.Length == 2 ? parts[1] : String.Empty; 162 | 163 | return commandHandler.Callback(arguments); 164 | } 165 | catch (CommandException ex) 166 | { 167 | Output(ConsoleHelpers.Color(ex.Message, 0xdd6666)); 168 | return false; 169 | } 170 | } 171 | 172 | // Parse* - Simple argument parser methods for the debugger commands. 173 | 174 | public Position ParseBreakPoint(string args) 175 | { 176 | if (args == String.Empty) 177 | { 178 | throw new CommandException("You need to specify a breakpoint position, e.g. 'break 5' or 'break 5:4'"); 179 | } 180 | var parts = args.Split(":"); 181 | if (!Int32.TryParse(parts[0], out int line)) 182 | { 183 | throw new CommandException("Breakpoint line should be an integer"); 184 | } 185 | 186 | int column = 0; 187 | if (parts.Length == 2) 188 | { 189 | if (!Int32.TryParse(parts[1], out column)) 190 | { 191 | throw new CommandException("Breakpoint column should be an integer"); 192 | } 193 | } 194 | 195 | return Position.From(line, column); 196 | } 197 | 198 | public int ParseIndex(string args, int count, string listCommand) 199 | { 200 | if (args == String.Empty) 201 | { 202 | throw new CommandException($"You need to specify an index. Use '{listCommand}' to list items with indices."); 203 | } 204 | if (!Int32.TryParse(args, out int index)) 205 | { 206 | throw new CommandException("Index must be an integer"); 207 | } 208 | 209 | if (index < 0 || index >= count) 210 | { 211 | string range = count switch 212 | { 213 | < 1 => "no entries in list", 214 | 1 => "0", 215 | > 1 => $"0 - {count}" 216 | }; 217 | throw new CommandException($"Index {index} out of range ({range})"); 218 | } 219 | 220 | return index; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Jint.DebuggerExample/Debugger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using Esprima; 5 | using Jint; 6 | using Jint.DebuggerExample; 7 | using Jint.Native; 8 | using Jint.Runtime.Debugger; 9 | using Jint.Runtime.Modules; 10 | using JintDebuggerExample.Helpers; 11 | 12 | namespace JintDebuggerExample; 13 | 14 | /// 15 | /// The main debugger logic, handling debugger commands and interacting with Engine.DebugHandler. 16 | /// 17 | internal class Debugger 18 | { 19 | private readonly Engine engine; 20 | private readonly CommandLine commandLine; 21 | private readonly SourceManager sources; 22 | private readonly bool isModule; 23 | 24 | private StepMode stepMode = StepMode.Into; 25 | private DebugInformation? currentInfo; 26 | 27 | public Debugger(string basePath, bool isModule) 28 | { 29 | this.isModule = isModule; 30 | commandLine = new CommandLine(); 31 | sources = new SourceManager(); 32 | 33 | commandLine.Register("Continue running", Continue, "continue", "c"); 34 | commandLine.Register("Step into", StepInto, "into", "i"); 35 | commandLine.Register("Step over", StepOver, "over", "o"); 36 | commandLine.Register("Step out", StepOut, "out", "u"); 37 | commandLine.Register("List breakpoints", InfoBreakPoints, "breaks"); 38 | commandLine.Register("Set breakpoint", SetBreakPoint, "break", "b", parameters: " [column]"); 39 | commandLine.Register("Set temporary breakpoint (removed after hit)", SetTemporaryBreakPoint, "tbreak", "tb", parameters: " [column]"); 40 | commandLine.Register("Clear breakpoints", ClearBreakPoints, "clear"); 41 | commandLine.Register("Delete breakpoint", DeleteBreakPoint, "delete", parameters: ""); 42 | commandLine.Register("List current call stack", InfoStack, "stack"); 43 | commandLine.Register("List current scope chain", InfoScopes, "scopes"); 44 | commandLine.Register("List bindings in scope", InfoScope, "scope", parameters: ""); 45 | commandLine.Register("Evaluate expression", Evaluate, "eval", "!", parameters: ""); 46 | commandLine.Register("Help", Help, "help", "h"); 47 | commandLine.Register("Exit debugger", Exit, "exit", "x"); 48 | 49 | engine = new Engine(options => 50 | { 51 | options 52 | .DebugMode() 53 | .DebuggerStatementHandling(DebuggerStatementHandling.Script) 54 | .InitialStepMode(stepMode); 55 | 56 | if (isModule) 57 | { 58 | options.EnableModules(basePath); 59 | } 60 | }); 61 | 62 | var console = new ConsoleObject(commandLine); 63 | engine.SetValue("console", console); 64 | 65 | //if (isModule) 66 | { 67 | // We need to keep track of scripts loaded in SourceManager - for breakpoints etc. 68 | // Since modules may load other modules, we're adding them to SourceManager through 69 | // the BeforeEvaluate event. 70 | engine.Debugger.BeforeEvaluate += DebugHandler_BeforeEvaluate; 71 | } 72 | 73 | engine.Debugger.Break += DebugHandler_Break; 74 | engine.Debugger.Step += DebugHandler_Step; 75 | } 76 | 77 | private void DebugHandler_BeforeEvaluate(object sender, Esprima.Ast.Program ast) 78 | { 79 | // We're using the DefaultModuleLoader, which uses the path as the "source" of the AST locations - this 80 | // allows us to load the actual script. 81 | string? source = ast.Location.Source; 82 | if (source != null) 83 | { 84 | sources.Load(ast, source, source); 85 | } 86 | } 87 | 88 | public void Execute(string scriptPath) 89 | { 90 | if (isModule) 91 | { 92 | engine.Modules.Import(scriptPath); 93 | } 94 | else 95 | { 96 | // string script = sources.Load(scriptPath, scriptPath); 97 | string script = System.IO.File.ReadAllText(scriptPath); 98 | engine.Execute(script, source: scriptPath); 99 | } 100 | commandLine.Output("Execution reached end of script."); 101 | } 102 | 103 | private bool StepInto(string args) 104 | { 105 | stepMode = StepMode.Into; 106 | return true; 107 | } 108 | 109 | private bool StepOut(string args) 110 | { 111 | stepMode = StepMode.Out; 112 | return true; 113 | } 114 | 115 | private bool StepOver(string args) 116 | { 117 | stepMode = StepMode.Over; 118 | return true; 119 | } 120 | 121 | private bool Continue(string args) 122 | { 123 | // Note that in some cases (and maybe eventually in this case), we may want to set StepMode.Into here, and 124 | // keep track of "running" vs "stepping" ourselves. This in order to be called regularly, and give the user 125 | // a chance to interactively pause the script when it's running. 126 | // For now, however, we stick with StepMode.None. 127 | stepMode = StepMode.None; 128 | return true; 129 | } 130 | 131 | private bool SetBreakPoint(string args) 132 | { 133 | var position = InternalSetBreakPoint(args, temporary: false); 134 | commandLine.Output($"Added breakpoint at {position}"); 135 | 136 | return false; 137 | } 138 | 139 | private bool SetTemporaryBreakPoint(string args) 140 | { 141 | var position = InternalSetBreakPoint(args, temporary: true); 142 | commandLine.Output($"Added temporary breakpoint at {position}"); 143 | 144 | return false; 145 | } 146 | 147 | private bool DeleteBreakPoint(string args) 148 | { 149 | var breakPoints = engine.Debugger.BreakPoints; 150 | int index = commandLine.ParseIndex(args, engine.Debugger.BreakPoints.Count, "breaks"); 151 | 152 | // Yeah, this is where I realize that BreakPointCollection should probably be ICollection 153 | var breakPoint = breakPoints.Skip(index).First(); 154 | breakPoints.RemoveAt(breakPoint.Location); 155 | commandLine.Output($"Removed breakpoint"); 156 | 157 | return false; 158 | } 159 | 160 | private bool ClearBreakPoints(string args) 161 | { 162 | engine.Debugger.BreakPoints.Clear(); 163 | commandLine.Output($"All breakpoints cleared."); 164 | 165 | return false; 166 | } 167 | 168 | private bool InfoBreakPoints(string args) 169 | { 170 | if (engine.Debugger.BreakPoints.Count == 0) 171 | { 172 | commandLine.Output("No breakpoints set."); 173 | return false; 174 | } 175 | 176 | int index = 0; 177 | foreach (var breakPoint in engine.Debugger.BreakPoints) 178 | { 179 | string flags = " "; 180 | if (breakPoint is ExtendedBreakPoint extended) 181 | { 182 | flags = extended.Temporary ? "T" : " "; 183 | } 184 | string location = RenderLocation(breakPoint.Location.Source, breakPoint.Location.Line, breakPoint.Location.Column); 185 | commandLine.Output($"{index, -4} {flags} {location} {breakPoint.Condition}"); 186 | index++; 187 | } 188 | 189 | return false; 190 | } 191 | 192 | private bool InfoStack(string args) 193 | { 194 | Debug.Assert(currentInfo != null); 195 | 196 | int index = 0; 197 | foreach (var frame in currentInfo.CallStack) 198 | { 199 | string location = RenderLocation(frame.Location); 200 | commandLine.Output($"{index, -4} {frame.FunctionName,-40} {location}"); 201 | index++; 202 | } 203 | 204 | return false; 205 | } 206 | 207 | private bool InfoScopes(string args) 208 | { 209 | Debug.Assert(currentInfo != null); 210 | 211 | int index = 0; 212 | foreach (var scope in currentInfo.CurrentScopeChain) 213 | { 214 | commandLine.Output($"{index, -4} {scope.ScopeType}"); 215 | index++; 216 | } 217 | 218 | return false; 219 | } 220 | 221 | private bool InfoScope(string args) 222 | { 223 | Debug.Assert(currentInfo != null); 224 | 225 | int index = commandLine.ParseIndex(args, currentInfo.CurrentScopeChain.Count, "scopes"); 226 | 227 | var scope = currentInfo.CurrentScopeChain[index]; 228 | 229 | commandLine.Output($"{scope.ScopeType} scope:"); 230 | 231 | // For local scope, we output return value (if at a return point - i.e. if ReturnValue isn't null) 232 | // and "this" (if defined) 233 | if (scope.ScopeType == DebugScopeType.Local) 234 | { 235 | if (currentInfo.ReturnValue != null) 236 | { 237 | commandLine.OutputBinding("return value", currentInfo.ReturnValue); 238 | } 239 | if (!currentInfo.CurrentCallFrame.This.IsUndefined()) 240 | { 241 | commandLine.OutputBinding("this", currentInfo.CurrentCallFrame.This); 242 | } 243 | } 244 | 245 | // And now all the scope's bindings ("variables") 246 | foreach (var name in scope.BindingNames) 247 | { 248 | JsValue? value = scope.GetBindingValue(name); 249 | commandLine.OutputBinding(name, value); 250 | } 251 | 252 | return false; 253 | } 254 | 255 | private bool Evaluate(string args) 256 | { 257 | if (args == String.Empty) 258 | { 259 | throw new CommandException("No expression to evaluate."); 260 | } 261 | try 262 | { 263 | // DebugHandler.Evaluate allows us to evaluate the expression in the Engine's current execution context. 264 | var result = engine.Debugger.Evaluate(args); 265 | commandLine.OutputValue(result); 266 | } 267 | catch (DebugEvaluationException ex) 268 | { 269 | // InnerException is the original JavaScriptException or ParserException. 270 | // We want the message from those, if it's available. 271 | // In rare cases, other exceptions may be thrown by Jint - in those cases, 272 | // we display the DebugEvaluationException's own message. 273 | throw new CommandException(ex.InnerException?.Message ?? ex.Message); 274 | } 275 | 276 | return false; 277 | } 278 | 279 | private bool Help(string args) 280 | { 281 | commandLine.OutputHelp(); 282 | 283 | return false; 284 | } 285 | 286 | private bool Exit(string args) 287 | { 288 | Environment.Exit(0); 289 | return false; 290 | } 291 | 292 | private Position InternalSetBreakPoint(string args, bool temporary) 293 | { 294 | Debug.Assert(currentInfo != null); 295 | 296 | var position = commandLine.ParseBreakPoint(args); 297 | 298 | // This is a bit of a cheat, since we're only dealing with one script, but some classes here are prepared 299 | // for many - we just use the source ID of the current location. We "know" Source is not null, but the compiler 300 | // doesn't: 301 | string sourceId = currentInfo.Location.Source ?? ""; 302 | 303 | // Jint requires *exact* breakpoint positions (line/column of the Esprima node targeted) 304 | // SourceManager/SourceInfo includes code for achieving that (may eventually be part of Jint API): 305 | position = sources.FindNearestBreakPointPosition(sourceId, position); 306 | 307 | engine.Debugger.BreakPoints.Set(new ExtendedBreakPoint(sourceId, position.Line, position.Column, temporary: temporary)); 308 | 309 | return position; 310 | } 311 | 312 | private StepMode DebugHandler_Step(object sender, DebugInformation e) 313 | { 314 | // TODO: Workaround: Currently, Jint may sometimes trigger Step when it shouldn't, and where it doesn't have a source. 315 | // Of course, this will also happen if we let the engine interpret scripts without giving it a source. 316 | if (e.Location.Source == null) 317 | { 318 | commandLine.Output("Warning: Skipped step without source."); 319 | return stepMode; 320 | } 321 | Pause(e); 322 | return stepMode; 323 | } 324 | 325 | private StepMode DebugHandler_Break(object sender, DebugInformation e) 326 | { 327 | if (e.BreakPoint is ExtendedBreakPoint breakPoint) 328 | { 329 | // Temporary breakpoints are removed when hit 330 | if (breakPoint.Temporary) 331 | { 332 | engine.Debugger.BreakPoints.RemoveAt(e.BreakPoint.Location); 333 | } 334 | } 335 | Pause(e); 336 | return stepMode; 337 | } 338 | 339 | private void Pause(DebugInformation e) 340 | { 341 | currentInfo = e; 342 | string line = sources.GetLine(e.Location); 343 | 344 | // Output the location we're at: 345 | commandLine.OutputPosition(e.Location, line); 346 | 347 | // In this - single threaded - example debugger, we let Console.ReadLine take care of blocking the execution. 348 | // In debuggers involving a UI or in a debug server, we'd need script execution to be on a separate thread 349 | // from the UI/server, and use e.g. a ManualResetEvent, or a message queue loop here to block until the user 350 | // signals that the execution should continue. 351 | commandLine.Input(); 352 | } 353 | 354 | private string RenderLocation(Location location) 355 | { 356 | string? source = location.Source?.CropStart(20); 357 | int line = location.Start.Line; 358 | int column = location.Start.Column; 359 | return RenderLocation(source, line, column); 360 | } 361 | 362 | private string RenderLocation(string? source, int line, int column) 363 | { 364 | return $"{source,-20} {line,4}:{column,4}"; 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes (and early documentation) on Jint's DebugHandler 2 | 3 | Jint's DebugHandler provides functionality for inspecting a script while it runs - mainly events triggered at each execution point in the script, as well as information about call stack, scopes etc. It also has built in support for breakpoints. 4 | 5 | Note that this is useful for other use cases than interactive debuggers: Any use case where inspection of the engine at each "step" of the script is needed. 6 | 7 | ## Setup 8 | There are a few DebugHandler-related options that can be specified via the Jint `Engine` constructor's options parameter: 9 | 10 | * `DebugMode()` - enables the debug handler. 11 | * `DebuggerStatementHandling()` - specifies how Javascript `debugger` statements are handled. Either by: 12 | * breaking in the CLR debugger (e.g. Visual Studio) - `DebuggerStatementHandling.Clr`. 13 | * triggering an event on `DebugHandler`, equivalent to a breakpoint (`DebuggerStatementHandling.Script`) - this would be used for implementing the typical behavior of the statement. 14 | * being ignored completely, which is the default (`DebuggerStatementHandling.Ignore`). 15 | * `InitialStepMode()` - specifies the `StepMode` to use when the script is first executed. By default, this is `StepMode.None`. 16 | 17 | ## Stepping 18 | `DebugHandler` maintains a `StepMode`, which can be set initially (through the `InitialStepMode()` option) - and changed through its events. 19 | 20 | * `StepMode.None` - means "don't step" - equivalent to "Continue" in debuggers. 21 | * `StepMode.Into` - means "step into functions calls and getters/setters". 22 | * `StepMode.Over` - means "step over function calls and getter/setter invocations". 23 | * `StepMode.Out` - means "step out of the current function/getter/setter". 24 | 25 | An event will trigger for *every* step of the script, regardless of `StepMode` - what changes is the *kind* of event that will trigger. More on that below. 26 | 27 | __Note__ that "steps" does not mean "statements". See below. 28 | 29 | ## Breakpoints 30 | Breakpoints can be set through `DebugHandler.BreakPoints`. A `BreakPoint` consists of a break location (line/column/source) and an optional condition. Note that - across all of Jint and Esprima.NET - the first line is line 1, and the first column is column 0. 31 | 32 | The `BreakPoint` class may be extended to add additional properties needed by the debugger implementation - e.g. for logpoints, hit counts etc. 33 | 34 | * `Set` - adds a breakpoint - if a breakpoint already exists at the same break location, it will be replaced. 35 | * `RemoveAt` - removes the breakpoint at a given location. Note that if the `source` of the location given is null, it will match *any* source. __To be considered:__ Does this make sense? Should it be changed? 36 | 37 | In addition, `DebugHandler.BreakPoints` can be cleared (`.Clear()`), all breakpoints can be enabled/disabled (`.Active`), and you can check the number of breakpoints (`.Count`) as well as whether a breakpoint exists at a given position (`Contains()`). 38 | 39 | Breakpoints can be added at any of the execution points in the code that Jint supports - including at return points from functions, the beginning *or* iterator of a `for` loop, etc. 40 | 41 | Breakpoints aren't line based: Multiple execution points, and hence breakpoint locations, may exist in a single line - whether it's multiple statements on the same line, or different points in a loop statement. __For this reason, the break location *must* match a "step-eligible" AST node's position *exactly*.__ (This also allows for efficient internal handling of breakpoints). 42 | 43 | __How it should be:__ Jint should provide a method to find valid break locations - and locate the valid break location nearest to a location given by the user. Jint.ExampleDebugger has an example implementation of such a method, but since Jint decides what constitutes a valid break location, it should probably also be Jint's job to make those locations easy to determine for the user. 44 | 45 | ## Events 46 | The bulk of interfacing with `DebugHandler` happens through its events. 47 | 48 | For each execution point, `DebugHandler` triggers one (and only one) of the events `Step`, `Break` or `Skip`. `Break` is used for both breakpoints and `debugger` statements - the exact reason for the event triggering can be determined through the `PauseType` that's set when the event is triggered. 49 | 50 | To see how the type of event is determined, let's define two states for the execution: "stepping" and "running": 51 | 52 | * "stepping": when `StepMode = Into/Over/Out` and the next step is reached. 53 | * "running" when `StepMode = None` *or* when `StepMode = Over/Out` and the next step hasn't been reached yet. 54 | 55 | Then the type of event is determined thus: 56 | 57 | * `Step` - when stepping. 58 | * `BreakPoint` - when reaching an active breakpoint while running. 59 | * `DebuggerStatement` - when reaching a debugger statement while running. 60 | * `Skip` - all other cases - i.e. while running, but we're not at a breakpoint or debugger statement. 61 | 62 | Note that `BreakPoint` and `DebuggerStatement` are only triggered while running. *Stepping* to an execution point that has a breakpoint or debugger statement will only trigger `Step`, because the reason for the event isn't the breakpoint, but simply that we stepped to the execution point. 63 | 64 | Some things to note: 65 | 66 | The only difference between these events is the reason - which may be used by the debugger implementation to make decisions on UI etc. All of the events have access to the same data from the `DebugHandler` - current node, the current breakpoint (even if it wasn't the reason for the event triggering), the call stack, etc. 67 | 68 | `Step` still has access to the breakpoint, even when it wasn't the breakpoint that triggered the event. This is useful for e.g. log points (which, depending on implementation, would still need to log even when stepping through the log point location), or breakpoints with hit count (which, again, would still need to increment the hit counter even when stepping through the breakpoint location). 69 | 70 | There are a few use cases for the `Skip` event, e.g. responding to the user clicking `Pause` or `Stop` in a debugger while the script is running. Remember that when running, only `BreakPoint`/`DebuggerStatement` and `Skip` events will be called - so the only way to respond to user interaction when there's no `debugger` statement or breakpoint is through the `Skip` event. In most other cases, `Skip` events should result in no action from the debugger. 71 | 72 | ## Information and methods available in event handlers 73 | 74 | __How it is now:__ The event includes a parameter of type `DebugInformation`, which contains most of the information. In addition, `DebugHandler` itself has a property, `CurrentLocation`, which holds the location of the current execution point. The reason this is on both the `DebugInformation` object and the `DebugHandler` itself is that e.g. a console object may need access to it in order to output the code location. 75 | 76 | __How it should be:__ Considering removing the `DebugInformation` parameter and moving all its properties to `DebugHandler` alongsie `CurrentLocation` - it's not just `CurrentLocation` that's useful outside of the step events. Also, it avoids allocating a `DebugInformation` object for every execution point. 77 | 78 | The information includes: 79 | 80 | * `PauseType` (which should probably be renamed when adding the `Skip` event type) - this is the `Reason` enum described above. 81 | * `BreakPoint` - breakpoint at the current location. This is set even if the breakpoint wasn't what triggered the event. 82 | * `CurrentNode` - the AST node that will be executed at the next step. Note that this will be `null` when execution is at a return point. 83 | * `Location` - same as the `CurrentLocation` currently on `DebugHandler` - the location (start and end position) in the code. For return points, the start and end are at the end of the function body (before the closing brace if there is one). 84 | * `CallStack` - read only list of all call stack frames (and their scope chains), starting with the current call frame. See below. 85 | * `CurrentCallFrame` - the current call stack frame - in other words, simply a "shortcut" to `CallStack[0]`. 86 | * `CurrentScopeChain` - the current scope chain - in other words, simply a "shortcut" to `CurrentCallFrame.ScopeChain`. 87 | * `ReturnValue` - the return value of the currently executing call frame. Only set at return points - otherwise, it's `null`. 88 | * `CurrentMemoryUsage` - only there for historical reasons. I believe it will still always return 0. __How it should be:__ Consider removing? 89 | 90 | The API allows inspection of every scope chain for every call frame on the stack through `CallStack`. For many use cases of simple scope inspection, `CurrentScopeChain` will be enough. 91 | 92 | ### Call stack frames 93 | 94 | Each `CallFrame` includes: 95 | 96 | * `FunctionName` - name of the function of this frame. For global scope (and other cases), this will be `"(anonymous)"`. 97 | * `FunctionLocation` - code location of the function (start and end of its definition). For top level (global) call frames, as well as for functions not defined in script, this will be `null`. 98 | * `Location` - currently executing source location in this call frame. 99 | * `ScopeChain` - read only list of the scopes in the scope chain of this call frame. See below. 100 | * `This` - the value of `this` in the call frame. 101 | * `ReturnValue` - the return value of this call frame. Will be `null` if not at a return point - which also means it's `null` for everything that isn't the top of the call stack (since no other frame has reached the return point yet). In other words, if any `CallFrame` has a non-null `ReturnValue`, it will be the top frame, and the same value will be in `ReturnValue` on the main information object. 102 | 103 | ### Scopes 104 | 105 | A scope chain, `DebugScopes`, contains all the scopes of a call frame. Note that each call frame may have a very different scope chain from another - all of them are accessible through `CallStack[n].ScopeChain`, allowing a debugger implementation to make the call stack "browsable", updating the scope chain depending on where in the call stack the user is inspecting. 106 | 107 | Scopes are organized similarly to Chromium: 108 | 109 | * It uses the same scope types - see the source and comments for the `DebugScopeTypes` enum. Only `Eval` and `WasmExpressionStack` are not currently used. 110 | * The __Global__ scope only includes the properties of the global object, variables declared with `var`, and functions (in other words, the global *object environment record*). 111 | * The __Script__ scope includes the top level variables declared with `let`/`const` as well as top level declared classes (in other words, the global *declarative environment record*). 112 | * Scopes with no bindings are not included in the chain. __How it should be:__ Should they be? 113 | * There can only be a single __Local__ scope - in the innermost function. Any outer "local" scopes have the type __Closure__. 114 | * `catch` gets its own __Catch__ scope, which only ever includes the argument to `catch`. 115 | * Modules also get their own __Module__ scope, which corresponds to the *module environment record*. 116 | 117 | Note that, like other debuggers, the scope chain allows inspection of shadowed variables in outer scopes. 118 | 119 | Each scope in the chain includes: 120 | 121 | * `ScopeType` - the type of scope (see above). 122 | * `IsTopLevel` - boolean indicating whether this is a block scope that is at the top level of its containing function. Chromium combines top level block scopes with the local scope - this allows a debugger to do the same. It's only needed because we cannot infer "top level" from the scope chain order alone - because we're leaving out any scopes that happen to not have any bindings (in other words, if the top level block scope has no bindings, any inner block scope would appear to be top level). 123 | * `BindingNames` - the names of all bindings in the scope. 124 | * `BindingObject` - for *object environment records* (i.e. the Global scope and With scopes), this returns the actual binding object. This is mainly intended as an optimization for a Chromium dev-tools implementation, allowing direct serialization of the object without creating a new transient object. 125 | * `GetBindingValue()` - retrieves the value of a named binding. 126 | * `SetBindingValue()` - sets the value of an *existing* (and mutable) binding. 127 | 128 | ### Lazy evaluation and semantics 129 | 130 | All of the objects and collections within the debug information are lazily instantiated - since many uses won't need them (e.g. most uses of `Skip` events would not need any of the debug information). 131 | 132 | __How it is now:__ New `CallFrame`, `DebugScopes` and `DebugScope` objects are created when queried on each step. 133 | 134 | __How it should be:__ It would be preferable to only create a single object for each internal call frame, scope chain and scope (environment record) - both for memory use and performance, but also to make it easier for a debugger to determine changes between steps (through reference equality). Haven't decided on the best approach here - any ideas? Considering e.g. unique ID's for frames/scopes (dev-tools wants a unique ID for call frames in any event). Or maybe better(?): `ConditionalWeakTable` mapping environment records and frames to `DebugScope` and `CallFrame`. 135 | 136 | ### Access to script AST's 137 | 138 | In many cases, debuggers will need access to the AST of the scripts. Jint obviously already parses the scripts and creates the ASTs. 139 | 140 | When Jint is about to execute a script or module - but after the code has been parsed - a `BeforeEvaluate` event will be triggered with an `ast` argument that holds the AST of the script or module. This both allows the debugger to reuse Jint's parsed AST - and, for interactive debuggers, allows the debugger UI to e.g. add breakpoints placed before launch, based on the AST. 141 | 142 | ### Exceptions 143 | 144 | __How it is now:__ `DebugHandler` does not deal with exceptions at all. 145 | 146 | __How it should be:__ `DebugHandler` should be called (when in DebugMode) whenever an exception is (about to be) thrown - in order to debug at the site, by e.g. `DebugHandler` triggering an `Error` event. This requires (still) centralization of Jint's script exception handling. 🙂 147 | --------------------------------------------------------------------------------