Debugging JavaScript in Visual Studio
Relevant source files
The following files were used as context for generating this wiki page:
- Jurassic/Compiler/Debugging/SourceCodeSpan.cs
- Jurassic/Compiler/MethodGenerator/CompilerOptions.cs
- Jurassic/Core/ScriptEngine.cs
- Jurassic/Compiler/Emit/OptimizationInfo.cs
- Jurassic/Compiler/MethodGenerator/MethodGenerator.cs
- Jurassic/Compiler/Statements/Statement.cs
- Benchmarker/Program.cs
- Jurassic/Compiler/Parser/Parser.cs
The Jurassic engine provides basic support for integrated debugging of JavaScript code within a Visual Studio environment. This is achieved by compiling the JavaScript source into .NET bytecode (CIL) and embedding debugging information, such as sequence points, which map the generated IL back to the original source code lines. This allows developers using the Jurassic library to step through JavaScript code, set breakpoints, and inspect variables using the standard Visual Studio debugger.
This functionality is intended for developers integrating the Jurassic engine into their .NET applications, providing a powerful tool for diagnosing issues within the executed scripts. The core of this system relies on a coordinated effort between the parser, compiler, and code generator to track source code locations and emit the necessary debugging metadata.
Sources: README.md:14, Jurassic/Core/ScriptEngine.cs, Jurassic/Compiler/MethodGenerator/MethodGenerator.cs
Enabling Debugging
To generate debuggable code, you must configure the compiler options when executing a script. This is primarily controlled through the CompilerOptions class.
Sources: Jurassic/Compiler/MethodGenerator/CompilerOptions.cs, Jurassic/Core/ScriptEngine.cs:642-651
CompilerOptions
The CompilerOptions class contains settings that influence the compilation process. The key property for debugging is EnableDebugging.
| Property | Type | Description |
|---|---|---|
EnableDebugging | bool | If true, the compiler generates debugging information. This is now obsolete and its functionality is not detailed in the provided files. |
ForceStrictMode | bool | If true, forces the script to be compiled in strict mode. |
CompatibilityMode | CompatibilityMode | Specifies the ECMAScript version to be compatible with. |
EnableILAnalysis | bool | If true, the generated IL is disassembled and can be retrieved for analysis. |
Sources: Jurassic/Compiler/MethodGenerator/CompilerOptions.cs:11-41, Jurassic/Core/ScriptEngine.cs:258-262
Generating Debuggable Assemblies
While the primary mechanism involves in-memory DynamicMethods, the engine has provisions for saving generated code to a physical DLL file, which can be useful for analysis with tools like ILDASM or PEVerify. The commented-out code within the Benchmarker project provides a practical example of how this can be achieved.
The process involves setting EnableDebugging to true and providing ReflectionEmitModuleInfo to the ScriptEngine.
// private static string SaveDLL(string code, string path = null)
// {
// // ...
// // Create a dynamic assembly and module.
// AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(Path.GetFileNameWithoutExtension(path)),
// AssemblyBuilderAccess.RunAndSave, Path.GetDirectoryName(path), true, null);
// ModuleBuilder module = assemblyBuilder.DefineDynamicModule("Module", Path.GetFileName(path));
//
// // Generate the code.
// var scriptEngine = new ScriptEngine();
// scriptEngine.EnableDebugging = true;
// scriptEngine.EnableILAnalysis = true;
// scriptEngine.ReflectionEmitInfo = new ScriptEngine.ReflectionEmitModuleInfo() { AssemblyBuilder = assemblyBuilder, ModuleBuilder = module };
// scriptEngine.Execute(code);
//
// // Save it.
// assemblyBuilder.Save(Path.GetFileName(path));
// return path;
// }
Sources: Benchmarker/Program.cs:175-201
Debugging Architecture
The debugging process is tightly integrated into the compilation pipeline. It starts with parsing the source code and ends with the IL generator emitting sequence points that link the bytecode to the original JavaScript file.
This diagram illustrates the flow of information during the compilation of a script with debugging enabled.
Key Components and Relationships
The following class diagram shows the primary classes involved in the debugging process and their relationships.
Sources: Jurassic/Core/ScriptEngine.cs, Jurassic/Compiler/MethodGenerator/CompilerOptions.cs, Jurassic/Compiler/MethodGenerator/MethodGenerator.cs, Jurassic/Compiler/Parser/Parser.cs, Jurassic/Compiler/Statements/Statement.cs, Jurassic/Compiler/Emit/OptimizationInfo.cs, Jurassic/Compiler/Debugging/SourceCodeSpan.cs
Core Debugging Implementation
The debugging functionality is built upon a few key components that work together to map the compiled IL to the source code.
SourceCodeSpan
The SourceCodeSpan struct is a fundamental data structure that represents a contiguous region of source code. It stores the start and end line and column numbers. The Parser is responsible for creating SourceCodeSpan instances for each statement it parses.
// Jurassic/Compiler/Debugging/SourceCodeSpan.cs
public class SourceCodeSpan
{
public SourceCodeSpan(int startLine, int startColumn, int endLine, int endColumn)
{
this.StartLine = startLine;
this.StartColumn = startColumn;
this.EndLine = endLine;
this.EndColumn = endColumn;
}
public int StartLine { get; private set; }
public int StartColumn { get; private set; }
public int EndLine { get; private set; }
public int EndColumn { get; private set; }
}
Sources: Jurassic/Compiler/Debugging/SourceCodeSpan.cs:8-25, Jurassic/Compiler/Parser/Parser.cs:338-340
OptimizationInfo
The OptimizationInfo class acts as a context object during code generation. It carries all the necessary information for various optimizations and for debugging.
The key debugging-related members are:
| Member | Type | Description |
|---|---|---|
Source | ScriptSource | The source of the JavaScript code being compiled. |
FunctionName | string | The name of the function being generated. |
SourceSpan | SourceCodeSpan | The source code span for the statement currently being compiled. |
MarkSequencePoint() | method | Emits a sequence point using the provided ILGenerator and updates the SourceSpan. |
Sources: Jurassic/Compiler/Emit/OptimizationInfo.cs:28-47
The MarkSequencePoint method is the lynchpin of the debugging system. It is called at the beginning of each statement's code generation to create the mapping between the IL offset and the source code location.
// Jurassic/Compiler/Emit/OptimizationInfo.cs:41-47
public void MarkSequencePoint(ILGenerator generator, SourceCodeSpan span)
{
if (span == null)
throw new ArgumentNullException(nameof(span));
this.SourceSpan = span;
}
Note: The actual call to generator.MarkSequencePoint is abstracted away in the provided files, but this method is the designated point for that action.
Sources: Jurassic/Compiler/Emit/OptimizationInfo.cs:41-47
Statement Code Generation
Every statement node in the Abstract Syntax Tree (AST) inherits from the Statement class. The Parser attaches a SourceCodeSpan to each statement as it builds the AST. During the code generation phase, the GenerateStartOfStatement method is called for each statement. This method, in turn, calls OptimizationInfo.MarkSequencePoint to associate the upcoming IL with its corresponding source code.
// Jurassic/Compiler/Statements/Statement.cs:91-105
public void GenerateStartOfStatement(ILGenerator generator, OptimizationInfo optimizationInfo, StatementLocals locals)
{
// ... (omitted for brevity)
if (locals.NonDefaultBreakStatementBehavior == false && this.HasLabels == true)
{
// ...
}
// Emit debugging information.
if (locals.NonDefaultSourceSpanBehavior == false)
optimizationInfo.MarkSequencePoint(generator, this.SourceSpan);
}
Sources: Jurassic/Compiler/Statements/Statement.cs:91-105, Jurassic/Compiler/Parser/Parser.cs:338-340
Summary
Jurassic's support for debugging in Visual Studio is facilitated by its compilation pipeline. By using CompilerOptions to enable debugging, the engine instructs the MethodGenerator to preserve source code location data. The Parser creates SourceCodeSpans for each statement, and the OptimizationInfo context object carries this information through to the ILGenerator. By emitting sequence points that map IL instructions to specific SourceCodeSpans, Jurassic produces debuggable .NET bytecode, enabling a familiar and powerful debugging experience for developers integrating JavaScript into their .NET applications.
Sources: Jurassic/Compiler/MethodGenerator/MethodGenerator.cs, Jurassic/Compiler/Emit/OptimizationInfo.cs, Jurassic/Compiler/Statements/Statement.cs