Skip to content

Commit

Permalink
feat: implement wasm inline functions (#1808)
Browse files Browse the repository at this point in the history
* implement inlined method handling and improve stepping

* update tests
  • Loading branch information
connor4312 authored Sep 20, 2023
1 parent 6095137 commit ed66fe3
Show file tree
Hide file tree
Showing 14 changed files with 617 additions and 170 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"@vscode/dwarf-debugging": "file:../vscode-dwarf-debugging/vscode-dwarf-debugging-0.0.2.tgz",
"@vscode/dwarf-debugging": "^0.0.2",
"@vscode/test-electron": "^2.2.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
Expand Down
131 changes: 115 additions & 16 deletions src/adapter/dwarf/wasmSymbolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
*--------------------------------------------------------*/

import type { IWasmWorker, MethodReturn } from '@vscode/dwarf-debugging';
import { Chrome } from '@vscode/dwarf-debugging/chrome-cxx/mnt/extension-api';
import { randomUUID } from 'crypto';
import { inject, injectable } from 'inversify';
import Cdp from '../../cdp/api';
import { ICdpApi } from '../../cdp/connection';
import { binarySearch } from '../../common/arrayUtils';
import { IDisposable } from '../../common/disposable';
import { ILogger, LogTag } from '../../common/logging';
import { once } from '../../common/objUtils';
import { Base0Position, IPosition } from '../../common/positions';
import { flatten, once } from '../../common/objUtils';
import { Base0Position, IPosition, Range } from '../../common/positions';
import { StepDirection } from '../pause';
import { getSourceSuffix } from '../templates';
import { IDwarfModuleProvider } from './dwarfModuleProvider';

Expand Down Expand Up @@ -169,8 +171,8 @@ export interface IWasmSymbols extends IDisposable {
/**
* Gets the source position for the given position in compiled code.
*
* Following CDP semantics, it returns a position on line 0 with the column
* offset being the byte offset in webassembly.
* Following CDP semantics, it assumes the column is being the byte offset
* in webassembly. However, we encode the inline frame index in the line.
*/
originalPositionFor(
compiledPosition: IPosition,
Expand All @@ -182,16 +184,40 @@ export interface IWasmSymbols extends IDisposable {
* Following CDP semantics, it assumes the position is line 0 with the column
* offset being the byte offset in webassembly.
*/
compiledPositionFor(sourceUrl: string, sourcePosition: IPosition): Promise<IPosition | undefined>;
compiledPositionFor(sourceUrl: string, sourcePosition: IPosition): Promise<IPosition[]>;

/**
* Gets variables in the program scope at the given position. If not
* implemented, the variable store should use its default behavior.
*
* Following CDP semantics, it assumes the position is line 0 with the column
* offset being the byte offset in webassembly.
* Following CDP semantics, it assumes the column is being the byte offset
* in webassembly. However, we encode the inline frame index in the line.
*/
getVariablesInScope?(callFrameId: string, position: IPosition): Promise<IWasmVariable[]>;

/**
* Gets the stack of WASM functions at the given position. Generally this will
* return an element with a single item containing the function name. However,
* inlined functions may return multiple functions for a position.
*
* It may return an empty array if function information is not available.
*
* @see https://github.com/ChromeDevTools/devtools-frontend/blob/c9f204731633fd2e2b6999a2543e99b7cc489b4b/docs/language_extension_api.md#dealing-with-inlined-functions
*/
getFunctionStack?(position: IPosition): Promise<{ name: string }[]>;

/**
* Gets ranges that should be stepped for the given step kind and location.
*
* Following CDP semantics, it assumes the column is being the byte offset
* in webassembly. However, we encode the inline frame index in the line.
*/
getStepSkipList?(
direction: StepDirection,
position: IPosition,
sourceUrl?: string,
mappedPosition?: IPosition,
): Promise<Range[]>;
}

class DecompiledWasmSymbols implements IWasmSymbols {
Expand Down Expand Up @@ -245,19 +271,19 @@ class DecompiledWasmSymbols implements IWasmSymbols {
public async compiledPositionFor(
sourceUrl: string,
sourcePosition: IPosition,
): Promise<IPosition | undefined> {
): Promise<IPosition[]> {
if (sourceUrl !== this.decompiledUrl) {
return undefined;
return [];
}

const { byteOffsetsOfLines } = await this.doDisassemble();
const { lineNumber } = sourcePosition.base0;
if (lineNumber >= byteOffsetsOfLines.length) {
return undefined;
return [];
}

const columnNumber = byteOffsetsOfLines[sourcePosition.base0.lineNumber];
return new Base0Position(0, columnNumber);
return [new Base0Position(0, columnNumber)];
}

public dispose(): void {
Expand Down Expand Up @@ -340,7 +366,7 @@ class WasmSymbols extends DecompiledWasmSymbols {
): Promise<{ url: string; position: IPosition } | undefined> {
const locations = await this.rpc.sendMessage('rawLocationToSourceLocation', {
codeOffset: compiledPosition.base0.columnNumber - this.codeOffset,
inlineFrameIndex: 0,
inlineFrameIndex: compiledPosition.base0.lineNumber,
rawModuleId: this.moduleId,
});

Expand All @@ -358,7 +384,7 @@ class WasmSymbols extends DecompiledWasmSymbols {
public override async compiledPositionFor(
sourceUrl: string,
sourcePosition: IPosition,
): Promise<IPosition | undefined> {
): Promise<IPosition[]> {
const { lineNumber, columnNumber } = sourcePosition.base0;
const locations = await this.rpc.sendMessage('sourceLocationToRawLocation', {
lineNumber,
Expand All @@ -380,8 +406,9 @@ class WasmSymbols extends DecompiledWasmSymbols {
}

// todo@connor4312: will there ever be a location in another module?
const location = locations.find(l => l.rawModuleId === this.moduleId);
return location && new Base0Position(0, this.codeOffset + locations[0].startOffset);
return locations
.filter(l => l.rawModuleId === this.moduleId)
.map(l => new Base0Position(0, this.codeOffset + l.startOffset));
}

/** @inheritdoc */
Expand All @@ -396,7 +423,7 @@ class WasmSymbols extends DecompiledWasmSymbols {
): Promise<IWasmVariable[]> {
const location = {
codeOffset: position.base0.columnNumber - this.codeOffset,
inlineFrameIndex: 0,
inlineFrameIndex: position.base0.lineNumber,
rawModuleId: this.moduleId,
};

Expand All @@ -415,6 +442,78 @@ class WasmSymbols extends DecompiledWasmSymbols {
);
}

/** @inheritdoc */
public async getFunctionStack(position: IPosition): Promise<{ name: string }[]> {
const info = await this.rpc.sendMessage('getFunctionInfo', {
codeOffset: position.base0.columnNumber - this.codeOffset,
inlineFrameIndex: position.base0.lineNumber,
rawModuleId: this.moduleId,
});

return 'frames' in info ? info.frames : [];
}

/** @inheritdoc */
public async getStepSkipList(
direction: StepDirection,
position: IPosition,
sourceUrl?: string,
mappedPosition?: IPosition,
): Promise<Range[]> {
const thisLocation = {
codeOffset: position.base0.columnNumber - this.codeOffset,
inlineFrameIndex: position.base0.lineNumber,
rawModuleId: this.moduleId,
};

const getOwnLineRanges = () => {
if (!(mappedPosition && sourceUrl)) {
return [];
}
return this.rpc.sendMessage('sourceLocationToRawLocation', {
lineNumber: mappedPosition.base0.lineNumber,
columnNumber: -1,
rawModuleId: this.moduleId,
sourceFileURL: sourceUrl,
});
};

let rawRanges: Chrome.DevTools.RawLocationRange[];
switch (direction) {
case StepDirection.Out: {
// Step out should step out of inline functions.
rawRanges = await this.rpc.sendMessage('getInlinedFunctionRanges', thisLocation);
break;
}
case StepDirection.Over: {
// step over should both step over inline functions and any
// intermediary statements on this line, which may exist
// in WAT assembly but not in source code.
const ranges = await Promise.all([
this.rpc.sendMessage('getInlinedCalleesRanges', thisLocation),
getOwnLineRanges(),
]);
rawRanges = flatten(ranges);
break;
}
case StepDirection.In:
// Step in should skip over any intermediary statements on this line
rawRanges = await getOwnLineRanges();
break;
default:
rawRanges = [];
break;
}

return rawRanges.map(
r =>
new Range(
new Base0Position(0, r.startOffset + this.codeOffset),
new Base0Position(0, r.endOffset + this.codeOffset),
),
);
}

private getMappedLines(sourceURL: string) {
const prev = this.mappedLines.get(sourceURL);
if (prev) {
Expand Down
41 changes: 41 additions & 0 deletions src/adapter/pause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import Cdp from '../cdp/api';
import { IPossibleBreakLocation } from './breakpoints';
import { StackTrace } from './stackTrace';
import { Thread } from './threads';

export type PausedReason =
| 'step'
| 'breakpoint'
| 'exception'
| 'pause'
| 'entry'
| 'goto'
| 'function breakpoint'
| 'data breakpoint'
| 'frame_entry';

export const enum StepDirection {
In,
Over,
Out,
}

export type ExpectedPauseReason =
| { reason: Exclude<PausedReason, 'step'>; description?: string }
| { reason: 'step'; description?: string; direction: StepDirection };

export interface IPausedDetails {
thread: Thread;
reason: PausedReason;
event: Cdp.Debugger.PausedEvent;
description: string;
stackTrace: StackTrace;
stepInTargets?: IPossibleBreakLocation[];
hitBreakpoints?: string[];
text?: string;
exception?: Cdp.Runtime.RemoteObject;
}
2 changes: 1 addition & 1 deletion src/adapter/smartStepping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { inject, injectable } from 'inversify';
import { ILogger, LogTag } from '../common/logging';
import { isInstanceOf } from '../common/objUtils';
import { AnyLaunchConfiguration } from '../configuration';
import { ExpectedPauseReason, IPausedDetails, PausedReason, StepDirection } from './pause';
import { isSourceWithMap } from './source';
import { UnmappedReason } from './sourceContainer';
import { StackFrame } from './stackTrace';
import { ExpectedPauseReason, IPausedDetails, PausedReason, StepDirection } from './threads';

export const enum StackFrameStepOverReason {
NotStepped,
Expand Down
15 changes: 10 additions & 5 deletions src/adapter/sourceContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ export class SourceContainer {
continue;
}

let location: IUiLocation;
let locations: IUiLocation[];
if ('decompiledUrl' in value) {
const entry = await value.compiledPositionFor(
sourceUrl,
Expand All @@ -487,8 +487,11 @@ export class SourceContainer {
if (!entry) {
continue;
}
const { lineNumber, columnNumber } = entry.base1;
location = { lineNumber, columnNumber, source: compiled };
locations = entry.map(l => ({
lineNumber: l.base1.lineNumber,
columnNumber: l.base1.columnNumber,
source: compiled,
}));
} else {
const entry = this.sourceMapFactory.guardSourceMapFn(
value,
Expand All @@ -508,10 +511,12 @@ export class SourceContainer {
compiled.inlineScriptOffset,
);

location = { lineNumber, columnNumber, source: compiled };
// recurse for nested sourcemaps:
const location = { lineNumber, columnNumber, source: compiled };
locations = [location, ...(await this.getCompiledLocations(location))];
}

output = output.concat(location, await this.getCompiledLocations(location));
output = output.concat(locations);
}

return output;
Expand Down
Loading

0 comments on commit ed66fe3

Please sign in to comment.