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 14f4351d06706..01e0a49e3d326 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 @@ -13,7 +13,14 @@ import { } from '@vscode/debugadapter'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; -import { Runtime, RuntimeEvents, IRuntimeVariableScope } from './runtime'; +import { + Runtime, + RuntimeEvents, + RuntimeValueType, + IRuntimeVariableScope, + CompoundType +} from './runtime'; +import { run } from 'node:test'; const enum LogLevel { Log = 'log', @@ -80,19 +87,16 @@ export class MoveDebugSession extends LoggingDebugSession { private runtime: Runtime; /** - * Handles to create variable scopes - * (ideally we would use numbers but DAP package does not like it) - * + * Handles to create variable scopes and compound variable values. */ - private variableHandles: Handles; - + private variableHandles: Handles; public constructor() { super(); this.setDebuggerLinesStartAt1(false); this.setDebuggerColumnsStartAt1(false); this.runtime = new Runtime(); - this.variableHandles = new Handles(); + this.variableHandles = new Handles(); // setup event handlers @@ -268,27 +272,66 @@ export class MoveDebugSession extends LoggingDebugSession { this.sendResponse(response); } + /** + * Converts a runtime value to a DAP variable. + * + * @param value variable value + * @param name variable name + * @param type optional variable type + * @returns a DAP variable. + */ + private convertRuntimeValue( + value: RuntimeValueType, + name: string, + type?: string + ): DebugProtocol.Variable { + if (typeof value === 'string') { + return { + name, + type, + value, + variablesReference: 0 + }; + } else if (Array.isArray(value)) { + const compoundValueReference = this.variableHandles.create(value); + return { + name, + type, + value: '(' + value.length + ')[...]', + variablesReference: compoundValueReference + }; + } else { + const compoundValueReference = this.variableHandles.create(value); + const accessChainParts = value.type.split('::'); + const datatypeName = accessChainParts[accessChainParts.length - 1]; + return { + name, + type: value.variantName + ? value.type + '::' + value.variantName + : value.type, + value: (value.variantName + ? datatypeName + '::' + value.variantName + : datatypeName + ) + '{...}', + variablesReference: compoundValueReference + }; + } + } + /** * Converts runtime variables to DAP variables. * * @param runtimeScope runtime variables scope, - * @returns an array of variables. + * @returns an array of DAP variables. */ private convertRuntimeVariables(runtimeScope: IRuntimeVariableScope): DebugProtocol.Variable[] { const variables: DebugProtocol.Variable[] = []; const runtimeVariables = runtimeScope.locals; - for (let i = 0; i < runtimeVariables.length; i++) { - const v = runtimeVariables[i]; + runtimeVariables.forEach(v => { if (v) { - variables.push({ - name: v.name, - type: v.type, - value: v.value, - variablesReference: 0 - }); + variables.push(this.convertRuntimeValue(v.value, v.name, v.type)); } - } - + }); return variables; } @@ -296,13 +339,27 @@ export class MoveDebugSession extends LoggingDebugSession { response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments ): void { - const handle = this.variableHandles.get(args.variablesReference); - if (!handle) { - this.sendResponse(response); - return; - } try { - const variables = this.convertRuntimeVariables(handle); + const variableHandle = this.variableHandles.get(args.variablesReference); + let variables: DebugProtocol.Variable[] = []; + if (variableHandle) { + if ('locals' in variableHandle) { + // we are dealing with a sccope + variables = this.convertRuntimeVariables(variableHandle); + } else { + // we are dealing with a compound value + if (Array.isArray(variableHandle)) { + for (let i = 0; i < variableHandle.length; i++) { + const v = variableHandle[i]; + variables.push(this.convertRuntimeValue(v, String(i))); + } + } else { + variableHandle.fields.forEach(([fname, fvalue]) => { + variables.push(this.convertRuntimeValue(fvalue, fname)); + }); + } + } + } if (variables.length > 0) { response.body = { variables @@ -312,7 +369,6 @@ export class MoveDebugSession extends LoggingDebugSession { response.success = false; response.message = err instanceof Error ? err.message : String(err); } - this.sendResponse(response); } 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 6739f3247c170..47d6bca991372 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 @@ -18,12 +18,33 @@ export interface IRuntimeVariableScope { locals: (IRuntimeVariable | undefined)[]; } +/** + * A compound type: + * - a vector (converted to an array of values) + * - a struct/enum (converted to an array of string/field value pairs) + */ +export type CompoundType = RuntimeValueType[] | IRuntimeCompundValue; + +/** + * A runtime value can have any of the following types: + * - boolean, number, string (converted to string) + * - compound type (vector, struct, enum) + */ +export type RuntimeValueType = string | CompoundType; + +export interface IRuntimeCompundValue { + fields: [string, RuntimeValueType][]; + type: string; + variantName?: string; + variantTag?: number; +} + /** * Describes a runtime local variable. */ interface IRuntimeVariable { name: string; - value: string; + value: RuntimeValueType; type: string; } 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 978f28e81a444..23edbd8424175 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 @@ -7,40 +7,40 @@ import { ModuleInfo } from './utils'; // Data types corresponding to source map file JSON schema. -interface ISrcDefinitionLocation { +interface JSONSrcDefinitionLocation { file_hash: number[]; start: number; end: number; } -interface ISrcStructSourceMapEntry { - definition_location: ISrcDefinitionLocation; - type_parameters: [string, ISrcDefinitionLocation][]; - fields: ISrcDefinitionLocation[]; +interface JSONSrcStructSourceMapEntry { + definition_location: JSONSrcDefinitionLocation; + type_parameters: [string, JSONSrcDefinitionLocation][]; + fields: JSONSrcDefinitionLocation[]; } -interface ISrcEnumSourceMapEntry { - definition_location: ISrcDefinitionLocation; - type_parameters: [string, ISrcDefinitionLocation][]; - variants: [[string, ISrcDefinitionLocation], ISrcDefinitionLocation[]][]; +interface JSONSrcEnumSourceMapEntry { + definition_location: JSONSrcDefinitionLocation; + type_parameters: [string, JSONSrcDefinitionLocation][]; + variants: [[string, JSONSrcDefinitionLocation], JSONSrcDefinitionLocation[]][]; } -interface ISrcFunctionMapEntry { - definition_location: ISrcDefinitionLocation; - type_parameters: [string, ISrcDefinitionLocation][]; - parameters: [string, ISrcDefinitionLocation][]; - locals: [string, ISrcDefinitionLocation][]; +interface JSONSrcFunctionMapEntry { + definition_location: JSONSrcDefinitionLocation; + type_parameters: [string, JSONSrcDefinitionLocation][]; + parameters: [string, JSONSrcDefinitionLocation][]; + locals: [string, JSONSrcDefinitionLocation][]; nops: Record; - code_map: Record; + code_map: Record; is_native: boolean; } -interface ISrcRootObject { - definition_location: ISrcDefinitionLocation; +interface JSONSrcRootObject { + definition_location: JSONSrcDefinitionLocation; module_name: string[]; - struct_map: Record; - enum_map: Record; - function_map: Record; + struct_map: Record; + enum_map: Record; + function_map: Record; constant_map: Record; } @@ -126,7 +126,7 @@ export function readAllSourceMaps( * @throws Error if with a descriptive error message if the source map cannot be read. */ function readSourceMap(sourceMapPath: string, filesMap: Map): ISourceMap { - const sourceMapJSON: ISrcRootObject = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8')); + const sourceMapJSON: JSONSrcRootObject = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8')); const fileHash = Buffer.from(sourceMapJSON.definition_location.file_hash).toString('base64'); const modInfo: ModuleInfo = { @@ -212,7 +212,7 @@ function readSourceMap(sourceMapPath: string, filesMap: Map): * @param sourceMapLines */ function prePopulateSourceMapLines( - sourceMapJSON: ISrcRootObject, + sourceMapJSON: JSONSrcRootObject, fileInfo: IFileInfo, sourceMapLines: Set ): void { @@ -265,7 +265,7 @@ function prePopulateSourceMapLines( * @param sourceMapLines set of source file lines. */ function addLinesForLocation( - loc: ISrcDefinitionLocation, + loc: JSONSrcDefinitionLocation, fileInfo: IFileInfo, sourceMapLines: Set ): void { diff --git a/external-crates/move/crates/move-analyzer/trace-adapter/src/trace_utils.ts b/external-crates/move/crates/move-analyzer/trace-adapter/src/trace_utils.ts index 3f9e576210600..8ca53832836e8 100644 --- a/external-crates/move/crates/move-analyzer/trace-adapter/src/trace_utils.ts +++ b/external-crates/move/crates/move-analyzer/trace-adapter/src/trace_utils.ts @@ -3,104 +3,142 @@ import * as fs from 'fs'; import { FRAME_LIFETIME, ModuleInfo } from './utils'; +import { IRuntimeCompundValue, RuntimeValueType } from './runtime'; + // Data types corresponding to trace file JSON schema. -interface ITraceModule { +interface JSONTraceModule { + address: string; + name: string; +} + +interface JSONStructTypeDescription { address: string; + module: string; name: string; + type_args: string[]; } -interface ITraceType { +interface JSONStructType { + struct: JSONStructTypeDescription; +} + +interface JSONVectorType { + vector: JSONBaseType; +} + +type JSONBaseType = string | JSONStructType | JSONVectorType; + +interface JSONTraceType { ref_type: string | null; - type_: string; + type_: JSONBaseType; } -interface ITraceRuntimeValue { - value: any; +type JSONTraceValueType = boolean | number | string | JSONTraceValueType[] | JSONTraceCompound; + +interface JSONTraceFields { + [key: string]: JSONTraceValueType; +} + +interface JSONTraceCompound { + fields: JSONTraceFields; + type: string; + variant_name?: string; + variant_tag?: number; +} + +interface JSONTraceRuntimeValue { + value: JSONTraceValueType; } -interface ITraceValue { - RuntimeValue: ITraceRuntimeValue; +interface JSONTraceValue { + RuntimeValue: JSONTraceRuntimeValue; } -interface ITraceFrame { +interface JSONTraceFrame { binary_member_index: number; frame_id: number; function_name: string; is_native: boolean; - locals_types: ITraceType[]; - module: ITraceModule; - parameters: ITraceValue[]; - return_types: ITraceType[]; + locals_types: JSONTraceType[]; + module: JSONTraceModule; + parameters: JSONTraceValue[]; + return_types: JSONTraceType[]; type_instantiation: string[]; } -interface ITraceOpenFrame { - frame: ITraceFrame; +interface JSONTraceOpenFrame { + frame: JSONTraceFrame; gas_left: number; } -interface ITraceInstruction { +interface JSONTraceInstruction { gas_left: number; instruction: string; pc: number; type_parameters: any[]; } -interface ITraceLocation { +interface JSONTraceLocalLocation { Local: [number, number]; } -interface ITraceWriteEffect { - location: ITraceLocation; - root_value_after_write: ITraceValue; +interface JSONTraceIndexedLocation { + Indexed: [JSONTraceLocalLocation, number]; } -interface ITraceReadEffect { - location: ITraceLocation; +type JSONTraceLocation = JSONTraceLocalLocation | JSONTraceIndexedLocation; + +interface JSONTraceWriteEffect { + location: JSONTraceLocation; + root_value_after_write: JSONTraceValue; +} + +interface JSONTraceReadEffect { + location: JSONTraceLocation; moved: boolean; - root_value_read: ITraceValue; + root_value_read: JSONTraceValue; } -interface ITracePushEffect { - RuntimeValue?: ITraceRuntimeValue; +interface JSONTracePushEffect { + RuntimeValue?: JSONTraceRuntimeValue; MutRef?: { - location: ITraceLocation; + location: JSONTraceLocation; snapshot: any[]; }; } -interface ITracePopEffect { - RuntimeValue?: ITraceRuntimeValue; +interface JSONTracePopEffect { + RuntimeValue?: JSONTraceRuntimeValue; MutRef?: { - location: ITraceLocation; + location: JSONTraceLocation; snapshot: any[]; }; } -interface ITraceEffect { - Push?: ITracePushEffect; - Pop?: ITracePopEffect; - Write?: ITraceWriteEffect; - Read?: ITraceReadEffect; +interface JSONTraceEffect { + Push?: JSONTracePushEffect; + Pop?: JSONTracePopEffect; + Write?: JSONTraceWriteEffect; + Read?: JSONTraceReadEffect; } -interface ITraceCloseFrame { +interface JSONTraceCloseFrame { frame_id: number; gas_left: number; - return_: ITraceRuntimeValue[]; + return_: JSONTraceRuntimeValue[]; } -interface ITraceEvent { - OpenFrame?: ITraceOpenFrame; - Instruction?: ITraceInstruction; - Effect?: ITraceEffect; - CloseFrame?: ITraceCloseFrame; +interface JSONTraceEvent { + OpenFrame?: JSONTraceOpenFrame; + Instruction?: JSONTraceInstruction; + Effect?: JSONTraceEffect; + CloseFrame?: JSONTraceCloseFrame; } -interface ITraceRootObject { - events: ITraceEvent[]; +interface JSONTraceRootObject { + events: JSONTraceEvent[]; version: number; } @@ -158,7 +196,7 @@ export enum TraceValKind { * Value in the trace. */ export type TraceValue = - | { type: TraceValKind.Runtime, value: string }; + | { type: TraceValKind.Runtime, value: RuntimeValueType }; /** * Kind of an effect of an instruction. @@ -188,7 +226,6 @@ interface ITrace { localLifetimeEnds: Map; } - /** * Reads a Move VM execution trace from a JSON file. * @@ -196,7 +233,7 @@ interface ITrace { * @returns execution trace. */ export function readTrace(traceFilePath: string): ITrace { - const traceJSON: ITraceRootObject = JSON.parse(fs.readFileSync(traceFilePath, 'utf8')); + const traceJSON: JSONTraceRootObject = JSON.parse(fs.readFileSync(traceFilePath, 'utf8')); const events: TraceEvent[] = []; // We compute the end of lifetime for a local variable as follows. // When a given local variable is read or written in an effect, we set the end of its lifetime @@ -224,7 +261,7 @@ export function readTrace(traceFilePath: string): ITrace { const localsTypes = []; const frame = event.OpenFrame.frame; for (const type of frame.locals_types) { - localsTypes.push(type.type_); + localsTypes.push(JSONTraceTypeToString(type.type_)); } // process parameters - store their values in trace and set their // initial lifetimes @@ -234,7 +271,10 @@ export function readTrace(traceFilePath: string): ITrace { const value = frame.parameters[i]; if (value) { const runtimeValue: TraceValue = - { type: TraceValKind.Runtime, value: JSON.stringify(value.RuntimeValue.value) }; + { + type: TraceValKind.Runtime, + value: traceValueFromJSON(value.RuntimeValue.value) + }; paramValues.push(runtimeValue); lifetimeEnds[i] = FRAME_LIFETIME; } @@ -288,36 +328,117 @@ export function readTrace(traceFilePath: string): ITrace { // if a local is read or written, set its end of lifetime // to infinite (end of frame) const location = effect.Write ? effect.Write.location : effect.Read!.location; - const frameId = location.Local[0]; - const localIndex = location.Local[1]; - const lifetimeEnds = localLifetimeEnds.get(frameId) || []; - lifetimeEnds[localIndex] = FRAME_LIFETIME; - localLifetimeEnds.set(frameId, lifetimeEnds); - + // there must be at least one frame on the stack when processing a write effect + // so we can safely access the last frame ID + const currentFrameID = frameIDs[frameIDs.length - 1]; + const localIndex = processJSONLocation(location, localLifetimeEnds, currentFrameID); + if (localIndex === undefined) { + continue; + } if (effect.Write) { - const value = JSON.stringify(effect.Write.root_value_after_write.RuntimeValue.value); + const value = traceValueFromJSON(effect.Write.root_value_after_write.RuntimeValue.value); const traceValue: TraceValue = { type: TraceValKind.Runtime, value }; - const TraceLocation: TraceLocation = { + const traceLocation: TraceLocation = { type: TraceLocKind.Local, - frameId, + frameId: currentFrameID, localIndex }; events.push({ type: TraceEventKind.Effect, effect: { type: TraceEffectKind.Write, - location: TraceLocation, + location: traceLocation, value: traceValue } }); } } + } + } + return { events, localLifetimeEnds }; +} + +/** + * Converts a JSON trace type to a string representation. + */ +function JSONTraceTypeToString(type: JSONBaseType): string { + if (typeof type === 'string') { + return type; + } else if ('vector' in type) { + return `vector<${JSONTraceTypeToString(type.vector)}>`; + } else { + return JSONTraceAddressToHexString(type.struct.address) + + "::" + + type.struct.module + + "::" + + type.struct.name; + } +} +/** + * Attempts to convert an address found in the trace (which is a string + * representing a 32-byte number) to a shorter and more readable hex string. + * Returns original string address if conversion fails. + */ +function JSONTraceAddressToHexString(address: string): string { + try { + const number = BigInt(address); + const hexAddress = number.toString(16); + return `0x${hexAddress}`; + } catch (error) { + // Return the original string if it's not a valid number + return address; + } +} +/// Processes a location in a JSON trace (sets the end of lifetime for a local variable) +/// and returns the local index if the location is a local variable in the current frame. +function processJSONLocation( + location: JSONTraceLocation, + localLifetimeEnds: Map, + currentFrameID: number +): number | undefined { + // TODO: handle Global and Indexed for other frames + if ('Local' in location) { + const frameId = location.Local[0]; + const localIndex = location.Local[1]; + const lifetimeEnds = localLifetimeEnds.get(frameId) || []; + lifetimeEnds[localIndex] = FRAME_LIFETIME; + localLifetimeEnds.set(frameId, lifetimeEnds); + return localIndex; + } else if ('Indexed' in location) { + const frameId = location.Indexed[0].Local[0]; + if (frameId === currentFrameID) { + const localIndex = location.Indexed[0].Local[1]; + const lifetimeEnds = localLifetimeEnds.get(frameId) || []; + lifetimeEnds[localIndex] = FRAME_LIFETIME; + localLifetimeEnds.set(frameId, lifetimeEnds); + return localIndex; } } - return { events, localLifetimeEnds }; + return undefined; +} + +/// Converts a JSON trace value to a runtime trace value. +function traceValueFromJSON(value: JSONTraceValueType): RuntimeValueType { + if (typeof value === 'boolean' + || typeof value === 'number' + || typeof value === 'string') { + return String(value); + } else if (Array.isArray(value)) { + return value.map(item => traceValueFromJSON(item)); + } else { + const fields: [string, RuntimeValueType][] = + Object.entries(value.fields).map(([key, value]) => [key, traceValueFromJSON(value)]); + const compoundValue: IRuntimeCompundValue = { + fields, + type: value.type, + variantName: value.variant_name, + variantTag: value.variant_tag + }; + return compoundValue; + } }