Skip to content

Commit

Permalink
make variables store work
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 committed Sep 18, 2023
1 parent 676e7e7 commit f525f00
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 63 deletions.
62 changes: 49 additions & 13 deletions src/adapter/breakpoints/breakpointBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Cdp from '../../cdp/api';
import { LogTag } from '../../common/logging';
import { IPosition } from '../../common/positions';
import { absolutePathToFileUrl, urlToRegex } from '../../common/urlUtils';
import { absolutePathToFileUrl } from '../../common/urlUtils';
import Dap from '../../dap/api';
import { BreakpointManager } from '../breakpoints';
import { ISourceScript, IUiLocation, Source, SourceFromMap, base1To0 } from '../source';
Expand Down Expand Up @@ -314,7 +314,7 @@ export abstract class Breakpoint {

const promises: Promise<void>[] = [];
for (const uiLocation of uiLocations) {
promises.push(this._setByScriptId(thread, script, source.offsetSourceToScript(uiLocation)));
promises.push(this._setForSpecific(thread, script, source.offsetSourceToScript(uiLocation)));
}

// If we get a source map that references this script exact URL, then
Expand Down Expand Up @@ -480,7 +480,7 @@ export abstract class Breakpoint {

private async _setByUiLocation(thread: Thread, uiLocation: IUiLocation): Promise<void> {
await Promise.all(
uiLocation.source.scripts.map(script => this._setByScriptId(thread, script, uiLocation)),
uiLocation.source.scripts.map(script => this._setForSpecific(thread, script, uiLocation)),
);
}

Expand Down Expand Up @@ -539,7 +539,9 @@ export abstract class Breakpoint {
lcEqual(bp.args.location, lineColumn)) ||
(script.url &&
isSetByUrl(bp.args) &&
new RegExp(bp.args.urlRegex ?? '').test(script.url) &&
(bp.args.urlRegex
? new RegExp(bp.args.urlRegex).test(script.url)
: script.url === bp.args.url) &&
lcEqual(bp.args, lineColumn)),
);
}
Expand All @@ -549,36 +551,70 @@ export abstract class Breakpoint {
* at the provided script by url regexp already. This is used to deduplicate breakpoint
* requests to avoid triggering any logpoint breakpoints multiple times.
*/
protected hasSetOnLocationByRegexp(urlRegexp: string, lineColumn: LineColumn) {
protected hasSetOnLocationByUrl(kind: 're' | 'url', input: string, lineColumn: LineColumn) {
return this.cdpBreakpoints.find(bp => {
if (isSetByUrl(bp.args)) {
return bp.args.urlRegex === urlRegexp && lcEqual(bp.args, lineColumn);
if (!lcEqual(bp.args, lineColumn)) {
return false;
}

if (kind === 'url') {
return bp.args.urlRegex
? new RegExp(bp.args.urlRegex).test(input)
: bp.args.url === input;
} else {
return kind === 're' && bp.args.urlRegex === input;
}
}

const script = this._manager._sourceContainer.getScriptById(bp.args.location.scriptId);
if (script) {
return lcEqual(bp.args.location, lineColumn) && new RegExp(urlRegexp).test(script.url);
return lcEqual(bp.args.location, lineColumn) && kind === 're'
? new RegExp(input).test(script.url)
: script.url === input;
}

return undefined;
});
}

protected async _setForSpecific(thread: Thread, script: ISourceScript, lineColumn: LineColumn) {
// prefer to set on script URL for non-anonymous scripts, since url breakpoints
// will survive and be hit on reload.
if (script.url) {
return this._setByUrl(thread, script.url, lineColumn);
} else {
return this._setByScriptId(thread, script, lineColumn);
}
}

protected async _setByUrl(thread: Thread, url: string, lineColumn: LineColumn): Promise<void> {
return this._setByUrlRegexp(thread, urlToRegex(url), lineColumn);
lineColumn = base1To0(lineColumn);

const previous = this.hasSetOnLocationByUrl('url', url, lineColumn);
if (previous) {
if (previous.state === CdpReferenceState.Pending) {
await previous.done;
}

return;
}

return this._setAny(thread, {
url,
condition: this.getBreakCondition(),
...lineColumn,
});
}

protected async _setByUrlRegexp(
thread: Thread,
urlRegex: string,
lineColumn: LineColumn,
): Promise<void> {
lineColumn = {
columnNumber: lineColumn.columnNumber - 1,
lineNumber: lineColumn.lineNumber - 1,
};
lineColumn = base1To0(lineColumn);

const previous = this.hasSetOnLocationByRegexp(urlRegex, lineColumn);
const previous = this.hasSetOnLocationByUrl('re', urlRegex, lineColumn);
if (previous) {
if (previous.state === CdpReferenceState.Pending) {
await previous.done;
Expand Down
1 change: 1 addition & 0 deletions src/adapter/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ export const isSourceWithWasm = (
source: unknown,
): source is ISourceWithMap<IWasmLocationProvider> =>
isSourceWithMap(source) && source.sourceMap.type === SourceLocationType.WasmSymbols;

export type ContentGetter = () => Promise<string | undefined>;
export type LineColumn = { lineNumber: number; columnNumber: number }; // 1-based

Expand Down
184 changes: 175 additions & 9 deletions src/adapter/variableStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { generate } from 'astring';
import { inject, injectable } from 'inversify';
import Cdp from '../cdp/api';
import { ICdpApi } from '../cdp/connection';
import { groupBy, iteratorFirst } from '../common/arrayUtils';
import { flatten, isInstanceOf, once } from '../common/objUtils';
import { parseSource, statementsToFunction } from '../common/sourceCodeManipulations';
import { IRenameProvider } from '../common/sourceMaps/renameProvider';
Expand All @@ -18,8 +19,9 @@ import { ProtocolError } from '../dap/protocolError';
import * as objectPreview from './objectPreview';
import { MapPreview, SetPreview } from './objectPreview/betterTypes';
import { PreviewContextType } from './objectPreview/contexts';
import { SourceFromMap, isSourceWithWasm } from './source';
import { StackFrame, StackTrace } from './stackTrace';
import { getSourceSuffix, RemoteException, RemoteObjectId } from './templates';
import { RemoteException, RemoteObjectId, getSourceSuffix } from './templates';
import { getArrayProperties } from './templates/getArrayProperties';
import { getArraySlots } from './templates/getArraySlots';
import {
Expand All @@ -30,6 +32,7 @@ import {
import { invokeGetter } from './templates/invokeGetter';
import { readMemory } from './templates/readMemory';
import { writeMemory } from './templates/writeMemory';
import { IWasmVariable, IWasmVariableEvaluation, WasmScope } from './wasmSymbolProvider';

const getVariableId = (() => {
let last = 0;
Expand Down Expand Up @@ -160,7 +163,7 @@ export interface IStoreSettings {
customPropertiesGenerator?: string;
}

type VariableCtor<TRestArgs extends unknown[] = unknown[], R extends Variable = Variable> = {
type VariableCtor<TRestArgs extends unknown[] = unknown[], R extends IVariable = IVariable> = {
new (context: VariableContext, ...rest: TRestArgs): R;
};

Expand All @@ -177,6 +180,12 @@ interface IContextSettings {
descriptionSymbols?: Promise<Cdp.Runtime.CallArgument>;
}

const wasmScopeNames: { [K in WasmScope]: { name: string; sortOrder: number } } = {
[WasmScope.Parameter]: { name: l10n.t('Native Parameters'), sortOrder: -10 },
[WasmScope.Local]: { name: l10n.t('Native Locals'), sortOrder: -9 },
[WasmScope.Global]: { name: l10n.t('Native Globals'), sortOrder: -8 },
};

class VariableContext {
/** When in a Variable, the name that this variable is accessible as from its parent scope or object */
public readonly name: string;
Expand All @@ -191,11 +200,11 @@ class VariableContext {

constructor(
public readonly cdp: Cdp.Api,
public readonly parent: undefined | Variable | Scope,
public readonly parent: undefined | IVariable | Scope,
ctx: IContextInit,
private readonly vars: VariablesMap,
public readonly locationProvider: IVariableStoreLocationProvider,
private readonly currentRef: undefined | (() => Variable | Scope),
private readonly currentRef: undefined | (() => IVariable | Scope),
private readonly settings: IContextSettings,
) {
this.name = ctx.name;
Expand Down Expand Up @@ -271,6 +280,21 @@ class VariableContext {
return this.createVariable(Variable, ctx, object);
}

public createVariableForWasm(variables: IWasmVariable[], scopeRef: IScopeRef) {
const groups = groupBy(variables, v => v.scope);
const groupVars: WasmScopeVariable[] = [];
for (const [key, display] of Object.entries(wasmScopeNames)) {
const vars = groups.get(key as WasmScope);
if (vars) {
groupVars.push(
this.createVariable(WasmScopeVariable, display, key as WasmScope, vars, scopeRef),
);
}
}

return groupVars;
}

/**
* Ensures symbols for custom descriptions are available, must be used
* before getStringProps/getToStringIfCustom
Expand Down Expand Up @@ -546,14 +570,14 @@ class Variable implements IVariable {
*/
public get accessor(): string {
const { parent, name } = this.context;
if (!parent || parent instanceof Scope) {
return this.context.name;
}

if (parent instanceof AccessorVariable) {
return parent.accessor;
}

if (!(parent instanceof Variable)) {
return this.context.name;
}

// Maps and sets:
const grandparent = parent.context.parent;
if (grandparent instanceof Variable) {
Expand Down Expand Up @@ -1023,6 +1047,131 @@ class GetterVariable extends AccessorVariable {
}
}

class WasmVariable implements IVariable, IMemoryReadable {
public static presentationHint: Dap.VariablePresentationHint = {
attributes: ['readOnly'],
};

/** @inheritdoc */
public readonly id = getVariableId();

/** @inheritdoc */
public readonly sortOrder = 0;

constructor(
private readonly context: VariableContext,
private readonly variable: IWasmVariableEvaluation,
private readonly scopeRef: IScopeRef,
) {}

public toDap(): Promise<Dap.Variable> {
return Promise.resolve({
name: this.context.name,
value: this.variable.description || '',
variablesReference: this.variable.getChildren ? this.id : 0,
memoryReference: this.variable.linearMemoryAddress ? String(this.id) : undefined,
presentationHint: this.context.presentationHint,
});
}

public async getChildren(): Promise<IVariable[]> {
const children = (await this.variable.getChildren?.()) || [];
return children.map(c =>
this.context.createVariable(
WasmVariable,
{ name: c.name, presentationHint: WasmVariable.presentationHint },
c.value,
this.scopeRef,
),
);
}

/** @inheritdoc */
public async readMemory(offset: number, count: number): Promise<Buffer | undefined> {
const addr = this.variable.linearMemoryAddress;
if (addr === undefined) {
return undefined;
}

const result = await this.context.cdp.Debugger.evaluateOnCallFrame({
callFrameId: this.scopeRef.callFrameId,
expression: `(${readMemory.source}).call(memories[0].buffer, ${
+addr + offset
}, ${+count}) ${getSourceSuffix()}`,
returnByValue: true,
});

if (!result) {
throw new RemoteException({
exceptionId: 0,
text: 'No response from CDP',
lineNumber: 0,
columnNumber: 0,
});
}

return Buffer.from(result.result.value, 'hex');
}

/** @inheritdoc */
public async writeMemory(offset: number, memory: Buffer): Promise<number> {
const addr = this.variable.linearMemoryAddress;
if (addr === undefined) {
return 0;
}

const result = await this.context.cdp.Debugger.evaluateOnCallFrame({
callFrameId: this.scopeRef.callFrameId,
expression: `(${writeMemory.source}).call(memories[0].buffer, ${
+addr + offset
}, ${JSON.stringify(memory.toString('hex'))}) ${getSourceSuffix()}`,
returnByValue: true,
});

return result?.result.value || 0;
}
}

class WasmScopeVariable implements IVariable {
/** @inheritdoc */
public readonly id = getVariableId();

/** @inheritdoc */
public readonly sortOrder = wasmScopeNames[this.kind].sortOrder;

constructor(
private readonly context: VariableContext,
public readonly kind: WasmScope,
private readonly variables: readonly IWasmVariable[],
private readonly scopeRef: IScopeRef,
) {}

toDap(): Promise<Dap.Variable> {
return Promise.resolve({
name: wasmScopeNames[this.kind].name,
value: '',
variablesReference: this.id,
});
}

getChildren(): Promise<IVariable[]> {
return Promise.all(
this.variables.map(async v => {
const evaluated = await v.evaluate();
return this.context.createVariable(
WasmVariable,
{
name: v.name,
presentationHint: WasmVariable.presentationHint,
},
evaluated,
this.scopeRef,
);
}),
);
}
}

class Scope implements IVariableContainer {
/** @inheritdoc */
public readonly id = getVariableId();
Expand All @@ -1035,7 +1184,7 @@ class Scope implements IVariableContainer {
private readonly renameProvider: IRenameProvider,
) {}

public async getChildren(_params: Dap.VariablesParams): Promise<Variable[]> {
public async getChildren(_params: Dap.VariablesParams): Promise<IVariable[]> {
const variables = await this.context.createObjectPropertyVars(this.remoteObject);
const existing = new Set(variables.map(v => v.name));
for (const extraProperty of this.extraProperties) {
Expand All @@ -1046,6 +1195,23 @@ class Scope implements IVariableContainer {
}
}

// special case: if the stack frame is in a wasm that had a symbolicated
// source at this location, use its symbol mapping instead of native symbols.
if (this.ref.scopeNumber === 0) {
const location = await this.ref.stackFrame.uiLocation();
if (location?.source instanceof SourceFromMap) {
const c = iteratorFirst(location.source.compiledToSourceUrl.keys());
if (isSourceWithWasm(c) && c.sourceMap.value.settledValue?.getVariablesInScope) {
const wasmVars = await c.sourceMap.value.settledValue.getVariablesInScope(
this.ref.callFrameId,
this.ref.stackFrame.rawPosition,
);
const resolved: IVariable[] = this.context.createVariableForWasm(wasmVars, this.ref);
return resolved.concat(variables);
}
}
}

return variables;
}

Expand Down
Loading

0 comments on commit f525f00

Please sign in to comment.