diff --git a/external-crates/move/crates/move-analyzer/trace-adapter/src/adapter.ts b/external-crates/move/crates/move-analyzer/trace-adapter/src/adapter.ts index 109b07dea465e..8e4886ec9e124 100644 --- a/external-crates/move/crates/move-analyzer/trace-adapter/src/adapter.ts +++ b/external-crates/move/crates/move-analyzer/trace-adapter/src/adapter.ts @@ -11,7 +11,7 @@ import { } from '@vscode/debugadapter'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; -import { Runtime, RuntimeEvents } from './runtime'; +import { Runtime, RuntimeEvents, IRuntimeStack } from './runtime'; const enum LogLevel { Log = 'log', @@ -19,6 +19,17 @@ const enum LogLevel { None = 'none' } +/** + * Customized stack trace response that includes additional data. + */ +interface CustomizedStackTraceResponse extends DebugProtocol.StackTraceResponse { + body: { + stackFrames: StackFrame[]; + totalFrames?: number; + optimized_lines: number[]; + }; +} + /** * Converts a log level string to a Logger.LogLevel. * @@ -163,15 +174,19 @@ export class MoveDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + protected stackTraceRequest(response: CustomizedStackTraceResponse, args: DebugProtocol.StackTraceArguments): void { try { const runtimeStack = this.runtime.stack(); + const stack_height = runtimeStack.frames.length; response.body = { stackFrames: runtimeStack.frames.map(frame => { const fileName = path.basename(frame.file); return new StackFrame(frame.id, frame.name, new Source(fileName, frame.file), frame.line); }).reverse(), - totalFrames: runtimeStack.frames.length + totalFrames: stack_height, + optimized_lines: stack_height > 0 + ? runtimeStack.frames[stack_height - 1].sourceMap.optimized_lines + : [] }; } catch (err) { response.success = false; diff --git a/external-crates/move/crates/move-analyzer/trace-adapter/src/runtime.ts b/external-crates/move/crates/move-analyzer/trace-adapter/src/runtime.ts index 670fbc2fcd13e..cfc35f1f20fce 100644 --- a/external-crates/move/crates/move-analyzer/trace-adapter/src/runtime.ts +++ b/external-crates/move/crates/move-analyzer/trace-adapter/src/runtime.ts @@ -32,7 +32,7 @@ interface IRuntimeStackFrame { * Describes the runtime stack during trace viewing session * (oldest frame is at the bottom of the stack at index 0). */ -interface IRuntimeStack { +export interface IRuntimeStack { frames: IRuntimeStackFrame[]; } diff --git a/external-crates/move/crates/move-analyzer/trace-adapter/src/source_map_utils.ts b/external-crates/move/crates/move-analyzer/trace-adapter/src/source_map_utils.ts index 842caf5511558..2da6e7670707e 100644 --- a/external-crates/move/crates/move-analyzer/trace-adapter/src/source_map_utils.ts +++ b/external-crates/move/crates/move-analyzer/trace-adapter/src/source_map_utils.ts @@ -13,10 +13,22 @@ interface ISrcDefinitionLocation { end: number; } +interface ISrcStructSourceMapEntry { + definition_location: ISrcDefinitionLocation; + type_parameters: [string, ISrcDefinitionLocation][]; + fields: ISrcDefinitionLocation[]; +} + +interface ISrcEnumSourceMapEntry { + definition_location: ISrcDefinitionLocation; + type_parameters: [string, ISrcDefinitionLocation][]; + variants: [[string, ISrcDefinitionLocation], ISrcDefinitionLocation[]][]; +} + interface ISrcFunctionMapEntry { definition_location: ISrcDefinitionLocation; - type_parameters: any[]; - parameters: any[]; + type_parameters: [string, ISrcDefinitionLocation][]; + parameters: [string, ISrcDefinitionLocation][]; locals: [string, ISrcDefinitionLocation][]; nops: Record; code_map: Record; @@ -26,10 +38,10 @@ interface ISrcFunctionMapEntry { interface ISrcRootObject { definition_location: ISrcDefinitionLocation; module_name: string[]; - struct_map: Record; - enum_map: Record; + struct_map: Record; + enum_map: Record; function_map: Record; - constant_map: Record; + constant_map: Record; } // Runtime data types. @@ -47,7 +59,7 @@ interface ILoc { */ interface ISourceMapFunction { // Locations indexed with PC values. - pcLocs: ILoc[] + pcLocs: ILoc[], } /** @@ -68,7 +80,9 @@ export interface IFileInfo { export interface ISourceMap { fileHash: string modInfo: ModuleInfo, - functions: Map + functions: Map, + // Lines that are not present in the source map. + optimized_lines: number[] } export function readAllSourceMaps(directory: string, filesMap: Map): Map { @@ -116,7 +130,10 @@ function readSourceMap(sourceMapPath: string, filesMap: Map): + " when processing source map at: " + sourceMapPath); } - for (const funEntry of Object.values(sourceMapJSON.function_map)) { + const allSourceMapLines = new Set(); + prePopulateSourceMapLines(sourceMapJSON, fileInfo, allSourceMapLines); + const functionMap = sourceMapJSON.function_map; + for (const funEntry of Object.values(functionMap)) { let nameStart = funEntry.definition_location.start; let nameEnd = funEntry.definition_location.end; const funName = fileInfo.content.slice(nameStart, nameEnd); @@ -131,6 +148,9 @@ function readSourceMap(sourceMapPath: string, filesMap: Map): for (const [pc, defLocation] of Object.entries(funEntry.code_map)) { const currentPC = parseInt(pc); const currentLoc = byteOffsetToLineColumn(fileInfo, defLocation.start); + allSourceMapLines.add(currentLoc.line); + // add the end line to the set as well even if we don't need it for pcLocs + allSourceMapLines.add(byteOffsetToLineColumn(fileInfo, defLocation.end).line); for (let i = prevPC + 1; i < currentPC; i++) { pcLocs.push(prevLoc); } @@ -140,9 +160,91 @@ function readSourceMap(sourceMapPath: string, filesMap: Map): } functions.set(funName, { pcLocs }); } - return { fileHash, modInfo, functions }; + let optimized_lines: number[] = []; + for (let i = 0; i < fileInfo.lines.length; i++) { + if (!allSourceMapLines.has(i + 1)) { // allSourceMapLines is 1-based + optimized_lines.push(i); // result must be 0-based + } + } + + return { fileHash, modInfo, functions, optimized_lines }; +} + +/** + * Pre-populates the set of source file lines that are present in the source map + * with lines corresponding to the definitions of module, structs, enums, and functions + * (excluding location of instructions in the function body which are handled elsewhere). + * Constants do not have location information in the source map and must be handled separately. + * + * @param sourceMapJSON + * @param fileInfo + * @param sourceMapLines + */ +function prePopulateSourceMapLines( + sourceMapJSON: ISrcRootObject, + fileInfo: IFileInfo, + sourceMapLines: Set +) { + addLinesForLocation(sourceMapJSON.definition_location, fileInfo, sourceMapLines); + const structMap = sourceMapJSON.struct_map; + for (const structEntry of Object.values(structMap)) { + addLinesForLocation(structEntry.definition_location, fileInfo, sourceMapLines); + for (const typeParam of structEntry.type_parameters) { + addLinesForLocation(typeParam[1], fileInfo, sourceMapLines); + } + for (const fieldDef of structEntry.fields) { + addLinesForLocation(fieldDef, fileInfo, sourceMapLines); + } + } + + const enumMap = sourceMapJSON.enum_map; + for (const enumEntry of Object.values(enumMap)) { + addLinesForLocation(enumEntry.definition_location, fileInfo, sourceMapLines); + for (const typeParam of enumEntry.type_parameters) { + addLinesForLocation(typeParam[1], fileInfo, sourceMapLines); + } + for (const variant of enumEntry.variants) { + addLinesForLocation(variant[0][1], fileInfo, sourceMapLines); + for (const fieldDef of variant[1]) { + addLinesForLocation(fieldDef, fileInfo, sourceMapLines); + } + } + } + + const functionMap = sourceMapJSON.function_map; + for (const funEntry of Object.values(functionMap)) { + addLinesForLocation(funEntry.definition_location, fileInfo, sourceMapLines); + for (const typeParam of funEntry.type_parameters) { + addLinesForLocation(typeParam[1], fileInfo, sourceMapLines); + } + for (const param of funEntry.parameters) { + addLinesForLocation(param[1], fileInfo, sourceMapLines); + } + for (const local of funEntry.locals) { + addLinesForLocation(local[1], fileInfo, sourceMapLines); + } + } +} + +/** + * Adds source file lines for the given location to the set. + * + * @param loc location in the source file. + * @param fileInfo source file information. + * @param sourceMapLines set of source file lines. + */ +function addLinesForLocation( + loc: ISrcDefinitionLocation, + fileInfo: IFileInfo, + sourceMapLines: Set +) { + const startLine = byteOffsetToLineColumn(fileInfo, loc.start).line; + sourceMapLines.add(startLine); + const endLine = byteOffsetToLineColumn(fileInfo, loc.end).line; + sourceMapLines.add(endLine); } + /** * Computes source file location (line/colum) from the byte offset * (assumes that lines and columns are 1-based). diff --git a/external-crates/move/crates/move-analyzer/trace-debug/src/extension.ts b/external-crates/move/crates/move-analyzer/trace-debug/src/extension.ts index a096d01bb37dc..2b94f50f38b35 100644 --- a/external-crates/move/crates/move-analyzer/trace-debug/src/extension.ts +++ b/external-crates/move/crates/move-analyzer/trace-debug/src/extension.ts @@ -4,7 +4,8 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; import * as path from 'path'; -import { WorkspaceFolder, DebugConfiguration, ProviderResult, CancellationToken, DebugSession } from 'vscode'; +import { StackFrame } from '@vscode/debugadapter'; +import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode'; /** * Log level for the debug adapter. @@ -35,6 +36,66 @@ export function activate(context: vscode.ExtensionContext) { }) ); + let previousSourcePath: string | undefined; + const decorationType = vscode.window.createTextEditorDecorationType({ + color: 'grey', + backgroundColor: 'rgba(220, 220, 220, 0.5)' // grey with 50% opacity + }); + context.subscriptions.push( + vscode.debug.onDidChangeActiveStackItem(async stackItem => { + if (stackItem instanceof vscode.DebugStackFrame) { + const session = vscode.debug.activeDebugSession; + if (session) { + // Request the stack frame details from the debug adapter + const stackTraceResponse = await session.customRequest('stackTrace', { + threadId: stackItem.threadId, + startFrame: stackItem.frameId, + levels: 1 + }); + + const stackFrame: StackFrame = stackTraceResponse.stackFrames[0]; + if (stackFrame && stackFrame.source && stackFrame.source.path !== previousSourcePath) { + previousSourcePath = stackFrame.source.path; + const source = stackFrame.source; + const line = stackFrame.line; + console.log(`Frame details: ${source?.name} at line ${line}`); + + const editor = vscode.window.activeTextEditor; + if (editor) { + const optimized_lines = stackTraceResponse.optimized_lines; + const document = editor.document; + let decorationsArray: vscode.DecorationOptions[] = []; + + optimized_lines.forEach((lineNumber: number) => { + const line = document.lineAt(lineNumber); + const lineLength = line.text.length; + const lineText = line.text.trim(); + if (lineText.length !== 0 // ignore empty lines + && !lineText.startsWith("const") // ignore constant declarations (not in the source map) + && !lineText.startsWith("}")) { // ignore closing braces with nothing else on the same line + const decoration = { + range: new vscode.Range(lineNumber, 0, lineNumber, lineLength), + }; + decorationsArray.push(decoration); + } + }); + + editor.setDecorations(decorationType, decorationsArray); + } + + } + } + } + }), + vscode.debug.onDidTerminateDebugSession(() => { + // reset all decorations when the debug session is terminated + // to avoid showing lines for code that was optimized away + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.setDecorations(decorationType, []); + } + }) + ); } /**