diff --git a/.vscode/settings.json b/.vscode/settings.json index c021ae0..97f656a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,8 @@ "eslint.useFlatConfig": true, "[typescript]": { "editor.codeActionsOnSave": { - "source.fixAll": "explicit", + "source.removeUnusedImports": "always", + "source.fixAll.eslint": "always" } }, } diff --git a/eslint.config.js b/eslint.config.js index 6466f24..a6a89ad 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,20 +10,29 @@ module.exports = [ 'parser': require('@typescript-eslint/parser'), }, 'plugins': { - '@typescript-eslint': require('@typescript-eslint/eslint-plugin'), + '@stylistic': require('@stylistic/eslint-plugin'), + '@typescript': require('@typescript-eslint/eslint-plugin'), }, 'rules': { - 'semi': 'error', 'no-throw-literal': 'error', + 'semi': 'error', + 'no-extra-semi': 'error', + 'eqeqeq': 'error', + 'prefer-const': 'warn', 'curly': 'warn', - 'eqeqeq': 'warn', - '@typescript-eslint/naming-convention': [ + '@typescript/naming-convention': [ 'warn', { 'selector': 'import', 'format': [ 'camelCase', 'PascalCase' ] } ], + '@stylistic/indent': ['warn', 4], + '@stylistic/quotes': ['warn', 'single'], + '@stylistic/brace-style': ['warn', '1tbs'], + '@stylistic/curly-newline': ['warn', {'minElements': 1, 'consistent': true}], + '@stylistic/keyword-spacing': 'warn', + '@stylistic/space-before-blocks': 'warn', }, } ]; diff --git a/package.json b/package.json index b8b6ec7..212efca 100644 --- a/package.json +++ b/package.json @@ -214,9 +214,12 @@ "@types/vscode": "^1.94", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", + "@vscode/vsce": "^3.x", "esbuild": "^0.24", "eslint": "^9.12", - "typescript": "5.5.x", - "@vscode/vsce": "^3.x" + "typescript": "5.5.x" + }, + "dependencies": { + "@stylistic/eslint-plugin": "^2.9.0" } } diff --git a/src/cxxrtl/client.ts b/src/cxxrtl/client.ts index 093d74c..0f765f3 100644 --- a/src/cxxrtl/client.ts +++ b/src/cxxrtl/client.ts @@ -45,17 +45,17 @@ export class Connection { } private traceSend(packet: proto.ClientPacket) { - this.timestamps.push(new Date()); if (packet.type === 'greeting') { - console.debug(`[CXXRTL] C>S`, packet); + console.debug('[CXXRTL] C>S', packet); } else if (packet.type === 'command') { + this.timestamps.push(new Date()); console.debug(`[CXXRTL] C>S#${this.sendIndex++}`, packet); } } private traceRecv(packet: proto.ServerPacket) { if (packet.type === 'greeting') { - console.debug(`[CXXRTL] S>C`, packet); + console.debug('[CXXRTL] S>C', packet); } else if (packet.type === 'response') { const elapsed = new Date().getTime() - this.timestamps.shift()!.getTime(); console.debug(`[CXXRTL] S>C#${this.recvIndex++}`, packet, `(${elapsed}ms)`); @@ -63,7 +63,7 @@ export class Connection { this.timestamps.shift(); console.error(`[CXXRTL] S>C#${this.recvIndex++}`, packet); } else if (packet.type === 'event') { - console.debug(`[CXXRTL] S>C`, packet); + console.debug('[CXXRTL] S>C', packet); } } diff --git a/src/cxxrtl/link.ts b/src/cxxrtl/link.ts index ca0856d..55bebce 100644 --- a/src/cxxrtl/link.ts +++ b/src/cxxrtl/link.ts @@ -9,7 +9,7 @@ export interface ILink { onDone: () => Promise; send(packet: proto.ClientPacket): Promise; -}; +} export class MockLink implements ILink { constructor( diff --git a/src/debugger.ts b/src/debugger.ts index 620255d..33eca7c 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -6,7 +6,7 @@ import { TimeInterval, TimePoint } from './model/time'; import { Scope } from './model/scope'; import { Variable } from './model/variable'; import { StatusBarItem } from './ui/status'; -import { BoundReference, Reference, Sample } from './model/sample'; +import { Reference, UnboundReference, Sample } from './model/sample'; export enum CXXRTLSimulationStatus { Paused = 'paused', @@ -28,12 +28,16 @@ export class CXXRTLDebugger { // Session properties. private _sessionStatus: CXXRTLSessionStatus = CXXRTLSessionStatus.Absent; - public get sessionStatus() { return this._sessionStatus;} + public get sessionStatus() { + return this._sessionStatus; + } private _onDidChangeSessionStatus: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeSessionStatus: vscode.Event = this._onDidChangeSessionStatus.event; private _currentTime: TimePoint = new TimePoint(0n, 0n); - public get currentTime() { return this._currentTime;} + public get currentTime() { + return this._currentTime; + } private _onDidChangeCurrentTime: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeCurrentTime: vscode.Event = this._onDidChangeCurrentTime.event; @@ -42,12 +46,16 @@ export class CXXRTLDebugger { private simulationStatusUpdateTimeout: NodeJS.Timeout | null = null; private _simulationStatus: CXXRTLSimulationStatus = CXXRTLSimulationStatus.Finished; - public get simulationStatus() { return this._simulationStatus; } + public get simulationStatus() { + return this._simulationStatus; + } private _onDidChangeSimulationStatus: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeSimulationStatus: vscode.Event = this._onDidChangeSimulationStatus.event; private _latestTime: TimePoint = new TimePoint(0n, 0n); - public get latestTime() { return this._latestTime;} + public get latestTime() { + return this._latestTime; + } private _onDidChangeLatestTime: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeLatestTime: vscode.Event = this._onDidChangeLatestTime.event; @@ -63,7 +71,7 @@ export class CXXRTLDebugger { public async startSession(): Promise { if (this.terminal !== null) { - vscode.window.showErrorMessage("A debug session is already in the process of being started."); + vscode.window.showErrorMessage('A debug session is already in the process of being started.'); return; } @@ -81,24 +89,24 @@ export class CXXRTLDebugger { this.setSessionStatus(CXXRTLSessionStatus.Starting); const processId = await this.terminal.processId; - console.log("[RTL Debugger] Launched process %d", processId); + console.log('[RTL Debugger] Launched process %d', processId); setTimeout(() => { const socket = net.createConnection({ port: configuration.port, host: '::1' }, () => { - vscode.window.showInformationMessage("Connected to the CXXRTL server."); + vscode.window.showInformationMessage('Connected to the CXXRTL server.'); (async () => { this.connection = new Connection(new NodeStreamLink(socket)); this.setSessionStatus(CXXRTLSessionStatus.Running); this.updateSimulationStatus(); - console.log("[RTL Debugger] Initialized"); + console.log('[RTL Debugger] Initialized'); })().catch(() => { this.stopSession(); }); }); socket.on('error', (err: any) => { if (err.code === 'ECONNREFUSED') { - vscode.window.showErrorMessage("The connection to the CXXRTL server was refused."); + vscode.window.showErrorMessage('The connection to the CXXRTL server was refused.'); } else { vscode.window.showErrorMessage(`The connection to the CXXRTL server has failed: ${err.code}.`); } @@ -106,14 +114,14 @@ export class CXXRTLDebugger { }); socket.on('close', (hadError) => { if (!hadError) { - vscode.window.showInformationMessage("Disconnected from the CXXRTL server."); + vscode.window.showInformationMessage('Disconnected from the CXXRTL server.'); } this.stopSession(); }); }, 500); // FIXME } else { - const OpenSettings = "Open Settings"; - const selection = await vscode.window.showErrorMessage("Configure the launch command to start a debug session.", OpenSettings); + const OpenSettings = 'Open Settings'; + const selection = await vscode.window.showErrorMessage('Configure the launch command to start a debug session.', OpenSettings); if (selection === OpenSettings) { vscode.commands.executeCommand('workbench.action.openSettings', 'rtlDebugger.command'); } @@ -254,7 +262,7 @@ export class CXXRTLDebugger { for (const [cxxrtlName, cxxrtlDesc] of Object.entries(cxxrtlResponse.scopes)) { const nestedScopes: Scope[] = []; const nestedVariables: Thenable = { - // NormallyPromises are evaluated eagerly; this Thenable does it lazily. + // Normally Promises are evaluated eagerly; this Thenable does it lazily. then: (onfulfilled, onrejected) => { return this.getVariablesForScope(cxxrtlName).then(onfulfilled, onrejected); } @@ -292,7 +300,7 @@ export class CXXRTLDebugger { } } - public bindReference(name: string, reference: Reference): BoundReference { + public bindReference(name: string, reference: UnboundReference): Reference { const epoch = this.advanceReferenceEpoch(name); // Note that we do not wait for the command to complete. Although it is possible for // the command to fail, this would only happen if one of the designations is invalid, @@ -303,13 +311,13 @@ export class CXXRTLDebugger { reference: name, items: reference.cxxrtlItemDesignations() }).catch((error) => { - console.error(`[CXXRTL] invalid designation while binding reference`, + console.error('[CXXRTL] invalid designation while binding reference', `${name}#${epoch}`, error); }); - return new BoundReference(name, epoch, reference); + return new Reference(name, epoch, reference); } - public async queryInterval(interval: TimeInterval, reference: BoundReference): Promise { + public async queryInterval(interval: TimeInterval, reference: Reference): Promise { this.verifyReferenceEpoch(reference.name, reference.epoch); const cxxrtlResponse = await this.connection!.queryInterval({ type: 'command', diff --git a/src/model/sample.ts b/src/model/sample.ts index d902dd5..7486209 100644 --- a/src/model/sample.ts +++ b/src/model/sample.ts @@ -2,175 +2,191 @@ import * as proto from '../cxxrtl/proto'; import { TimePoint } from './time'; import { MemoryVariable, ScalarVariable, Variable } from './variable'; -function chunksForBits(width: number): number { - return 0 | ((width + 31) / 32); +class DataRange { + readonly stride: number; + readonly count: number; + + constructor(variable: Variable, count: number = 1) { + this.stride = 0 | ((variable.width + 31) / 32); + this.count = count; + } + + get size() { + return this.stride * this.count; + } + + bigintFromRaw(data: Uint32Array, offset: number = 0) { + if (!(offset <= this.count)) { + throw RangeError(`Offset ${offset} out of bounds for data range with ${this.count} elements`); + } + let value = 0n; + for (let index = 0; index < this.stride; index++) { + value = (value << 32n) + BigInt(data[this.stride * offset + index]); + } + return value; + } } -function getChunksAt(array: Uint32Array, offset: number, count: number) { - let value = 0n; - for (let index = offset; index < offset + count; index++) { - value = (value << 32n) + BigInt(array[offset]); +export abstract class Designation { + abstract variable: Variable; + + abstract get canonicalKey(): string; + + abstract get cxxrtlItemDesignation(): proto.ItemDesignation; + + abstract dataRange(): DataRange; + + abstract extractFromRaw(data: Uint32Array): T; +} + +export class ScalarDesignation extends Designation { + constructor( + readonly variable: ScalarVariable, + ) { + super(); + } + + get canonicalKey(): string { + return this.variable.fullName.join(' '); + } + + get cxxrtlItemDesignation(): proto.ItemDesignation { + return [this.variable.cxxrtlIdentifier]; + } + + override dataRange(): DataRange { + return new DataRange(this.variable); + } + + override extractFromRaw(data: Uint32Array): bigint { + return this.dataRange().bigintFromRaw(data); } - return value; } -// This class is a bit weird because the CXXRTL range designator is inclusive and has configurable -// direction. I.e. `[0, 1]` and `[1, 0]` both designate two rows, in opposite order. -class ReferenceSlice { +export class MemoryRowDesignation extends Designation { constructor( - readonly offset: number, // in chunks - readonly variable: Variable, - readonly range?: [number, number], - ) {} + readonly variable: MemoryVariable, + readonly index: number, + ) { + super(); + } - get size(): number { - return this.count * this.stride; + get canonicalKey(): string { + return `${this.variable.fullName.join(' ')}\u0000${this.index}`; } - get count(): number { - if (this.range === undefined) { - return 1; // scalar - } else { - const [first, last] = this.range; - return (last >= first) ? (last - first + 1) : (first - last + 1); - } + get cxxrtlItemDesignation(): proto.ItemDesignation { + return [this.variable.cxxrtlIdentifier, this.index, this.index]; } - get stride(): number { - return chunksForBits(this.variable.width); - } - - hasIndex(index: number): boolean { - let begin, end; - if (this.range === undefined) { - begin = 0; - end = 1; - } else { - const [first, last] = this.range; - if (last >= first) { - begin = first; - end = last + 1; - } else { - begin = last; - end = first + 1; - } - } - return (index >= begin && index < end); + override dataRange(): DataRange { + return new DataRange(this.variable); } - offsetForIndex(index: number): number { - if (this.range === undefined) { - return this.offset; - } - const [first, last] = this.range; - if (last >= first) { - return this.offset + this.stride * (index - first); - } else { - return this.offset + this.stride * (index - last); - } + override extractFromRaw(data: Uint32Array): bigint { + return this.dataRange().bigintFromRaw(data); } } -export class Reference { - private frozen: boolean = false; - private totalSize: number = 0; // in chunks - private slices: ReferenceSlice[] = []; - private variables: Map = new Map(); +export class MemoryRangeDesignation extends Designation> { + constructor( + readonly variable: MemoryVariable, + readonly first: number, + readonly last: number, + ) { + super(); + } - constructor(variables: Variable[] = []) { - for (const variable of variables) { - this.add(variable); - } + get canonicalKey(): string { + return `${this.variable.fullName.join(' ')}\u0000${this.first}\u0000${this.last}`; } - freeze() { - this.frozen = true; + get cxxrtlItemDesignation(): proto.ItemDesignation { + return [this.variable.cxxrtlIdentifier, this.first, this.last]; } - copy(): Reference { - const newInstance = new Reference(); - newInstance.totalSize = this.totalSize; - newInstance.slices = this.slices.slice(); - newInstance.variables = new Map(this.variables.entries()); - return newInstance; + get count(): number { + return (this.last >= this.first) ? (this.last - this.first + 1) : (this.first - this.last + 1); } - add(variable: Variable): void; - add(memory: MemoryVariable, first: number, last: number): void; + override dataRange(): DataRange { + return new DataRange(this.variable, this.count); + } - add(variable: Variable, first?: number, last?: number) { - if (this.frozen) { - throw new Error(`Cannot add variables to a reference that has been bound to a name`); - } - if (this.variables.has(variable)) { - // This is not a CXXRTL limitation, but a consequence of the use of `Map` for fast - // lookup of sample data during extraction. - throw new Error(`Unable to reference variable ${variable.fullName} twice`); + *extractFromRaw(data: Uint32Array): Iterable { + for (let offset = 0; offset < this.count; offset++) { + yield this.dataRange().bigintFromRaw(data, offset); } - let range: [number, number] | undefined; - if (variable instanceof MemoryVariable) { - if (first === undefined || last === undefined) { - range = [0, variable.depth - 1]; - } else { - range = [first, last]; - } - } - const slice = new ReferenceSlice(this.totalSize, variable, range); - this.totalSize += slice.size; - this.slices.push(slice); - this.variables.set(variable, slice); } +} - extract(variableData: Uint32Array, variable: Variable, index: number = 0): bigint { - const slice = this.variables.get(variable); - if (slice === undefined) { - throw RangeError(`Variable ${variable.fullName} is not referenced`); +export class Handle { + constructor( + readonly designation: Designation, + readonly reference: UnboundReference, + readonly offset: number, + ) {} + + extractFromRaw(data: Uint32Array): T { + return this.designation.extractFromRaw(data.subarray(this.offset)); + } +} + +export class UnboundReference { + private frozen: boolean = false; + private offset: number = 0; // in chunks + private handles: Handle[] = []; + + add(designation: Designation): Handle { + if (this.frozen) { + throw new Error('Cannot add variables to a reference that has been bound to a name'); } - if (!slice.hasIndex(index)) { - throw RangeError(`Variable ${variable.fullName} is referenced but index ${index} is out of bounds`); + const handle = new Handle(designation, this, this.offset); + this.handles.push(handle); + this.offset += designation.dataRange().size; + return handle; + } + + freeze() { + this.frozen = true; + } + + *allHandles(): Iterable<[Designation, Handle]> { + for (const handle of this.handles) { + yield [handle.designation, handle]; } - return getChunksAt(variableData, slice.offsetForIndex(index), slice.stride); } cxxrtlItemDesignations(): proto.ItemDesignation[] { - return this.slices.map((slice) => { - if (slice.variable instanceof ScalarVariable) { - return slice.variable.cxxrtlItemDesignation(); - } else if (slice.variable instanceof MemoryVariable) { - if (slice.range === undefined) { - return slice.variable.cxxrtlItemDesignation(); - } else { - const [first, last] = slice.range; - return slice.variable.cxxrtlItemDesignation(first, last); - } - } else { - throw new Error(`Unknown variable type in ${slice.variable}`); - } - }); + return this.handles.map((slice) => slice.designation.cxxrtlItemDesignation); } } -export class BoundReference { +export class Reference { constructor( readonly name: string, readonly epoch: number, - readonly unbound: Reference, + readonly unbound: UnboundReference, ) { this.unbound.freeze(); } + + allHandles(): Iterable<[Designation, Handle]> { + return this.unbound.allHandles(); + } } export class Sample { constructor( readonly time: TimePoint, - readonly reference: Reference, + readonly reference: UnboundReference, readonly variableData: Uint32Array, ) {} - extract(scalar: ScalarVariable): bigint; - extract(memory: MemoryVariable, row: number): bigint; - - extract(variable: Variable, offset: number = 0): bigint { - return this.reference.extract(this.variableData, variable, offset); + extract(handle: Handle): T { + if (handle.reference !== this.reference) { + throw new ReferenceError('Handle is not bound to the same reference as the sample'); + } + return handle.extractFromRaw(this.variableData); } } diff --git a/src/model/source.ts b/src/model/source.ts index ba9d24a..d2d0222 100644 --- a/src/model/source.ts +++ b/src/model/source.ts @@ -55,4 +55,4 @@ export class Location { const sourceRelativePath = vscode.workspace.asRelativePath(this.file); return `[${sourceRelativePath}:${this.startLine + 1}](${this.asOpenCommandUri()})`; } -}; +} diff --git a/src/model/styling.ts b/src/model/styling.ts index 111d7c0..8477165 100644 --- a/src/model/styling.ts +++ b/src/model/styling.ts @@ -1,3 +1,5 @@ +import * as vscode from 'vscode'; + import { MemoryVariable, ScalarVariable, Variable } from './variable'; export enum DisplayStyle { @@ -6,14 +8,14 @@ export enum DisplayStyle { VHDL = 'VHDL', } -export function variableDescription(style: DisplayStyle, variable: Variable): string { +export function variableDescription(style: DisplayStyle, variable: Variable, { scalar = false } = {}): string { let result = ''; if (variable instanceof ScalarVariable && variable.lsbAt === 0 && variable.width === 1) { return result; } switch (style) { case DisplayStyle.Python: { - if (variable instanceof MemoryVariable) { + if (variable instanceof MemoryVariable && !scalar) { if (variable.zeroAt === 0) { result += `[${variable.depth}] `; } else { @@ -23,7 +25,7 @@ export function variableDescription(style: DisplayStyle, variable: Variable): st if (variable.lsbAt === 0) { result += `[${variable.width}]`; } else { - result += `[${variable.lsbAt}:${variable.lsbAt + variable.width}]`; + result += `[${variable.lsbAt}:${variable.lsbAt + variable.width}] `; } return result; } @@ -31,8 +33,8 @@ export function variableDescription(style: DisplayStyle, variable: Variable): st case DisplayStyle.Verilog: case DisplayStyle.VHDL: { result += `[${variable.lsbAt + variable.width - 1}:${variable.lsbAt}]`; - if (variable instanceof MemoryVariable) { - result += ` [${variable.zeroAt}:${variable.zeroAt + variable.depth - 1}]`; + if (variable instanceof MemoryVariable && !scalar) { + result += ` [${variable.zeroAt}:${variable.zeroAt + variable.depth - 1}] `; } return result; } @@ -63,8 +65,10 @@ export function* memoryRowIndices(variable: MemoryVariable): Generator { } } -export function variableValue(style: DisplayStyle, base: 2 | 8 | 10 | 16, variable: Variable, value: bigint): string { - if (variable.width === 1) { +export function variableValue(style: DisplayStyle, variable: Variable, value: bigint | undefined, base: 2 | 8 | 10 | 16 = 10): string { + if (value === undefined) { + return '...'; + } else if (variable.width === 1) { return value.toString(); } else { switch (style) { @@ -72,7 +76,7 @@ export function variableValue(style: DisplayStyle, base: 2 | 8 | 10 | 16, variab switch (base) { case 2: return `0b${value.toString(2)}`; case 8: return `0o${value.toString(8)}`; - case 10: return `0d${value.toString(10)}`; + case 10: return value.toString(10); case 16: return `0x${value.toString(16)}`; } @@ -89,3 +93,12 @@ export function variableValue(style: DisplayStyle, base: 2 | 8 | 10 | 16, variab } } } + +export function variableTooltip(variable: Variable): vscode.MarkdownString { + const tooltip = new vscode.MarkdownString(variable.fullName.join('.')); + tooltip.isTrusted = true; + if (variable.location) { + tooltip.appendMarkdown(`\n\n- ${variable.location.asMarkdownLink()}`); + } + return tooltip; +} diff --git a/src/model/time.ts b/src/model/time.ts index a6a4a9b..df7a651 100644 --- a/src/model/time.ts +++ b/src/model/time.ts @@ -36,7 +36,7 @@ export class TimePoint { public toString(): string { function groupDecimals(num: bigint) { - let groups: string[] = []; + const groups: string[] = []; if (num === 0n) { groups.push('0'); } else { diff --git a/src/model/variable.ts b/src/model/variable.ts index 67abea2..c873887 100644 --- a/src/model/variable.ts +++ b/src/model/variable.ts @@ -1,4 +1,5 @@ import * as proto from '../cxxrtl/proto'; +import { MemoryRangeDesignation, MemoryRowDesignation, ScalarDesignation } from './sample'; import { Location } from './source'; export abstract class Variable { @@ -43,13 +44,12 @@ export abstract class Variable { get cxxrtlIdentifier(): string { return this.fullName.join(' '); } - - cxxrtlItemDesignation(): proto.ItemDesignation { - return [this.cxxrtlIdentifier]; - } } export class ScalarVariable extends Variable { + designation(): ScalarDesignation { + return new ScalarDesignation(this); + } } export class MemoryVariable extends Variable { @@ -64,15 +64,17 @@ export class MemoryVariable extends Variable { super(fullName, location, lsbAt, width); } - override cxxrtlItemDesignation(): proto.ItemDesignation; - override cxxrtlItemDesignation(first: number, last: number): proto.ItemDesignation; // inclusive! - override cxxrtlItemDesignation(first: number = 0, last: number = this.depth - 1): proto.ItemDesignation { - if (first < 0 || first > this.depth) { - throw new RangeError(`Start index ${first} out of range`); - } - if (last < 0 || last > this.depth) { - throw new RangeError(`End index ${last} out of range`); + designation(index: number): MemoryRowDesignation; + designation(first: number, last: number): MemoryRangeDesignation; + designation(): MemoryRangeDesignation; + + designation(firstOrIndex?: number, last?: number): MemoryRowDesignation | MemoryRangeDesignation { + if (firstOrIndex !== undefined && last === undefined) { + return new MemoryRowDesignation(this, firstOrIndex); + } else if (firstOrIndex !== undefined && last !== undefined) { + return new MemoryRangeDesignation(this, firstOrIndex, last); + } else { + return new MemoryRangeDesignation(this, 0, this.depth - 1); } - return [this.cxxrtlIdentifier, first, last]; } } diff --git a/src/observer.ts b/src/observer.ts new file mode 100644 index 0000000..f771426 --- /dev/null +++ b/src/observer.ts @@ -0,0 +1,100 @@ +import * as vscode from 'vscode'; + +import { UnboundReference, Designation, Reference } from './model/sample'; +import { CXXRTLDebugger } from './debugger'; +import { TimeInterval } from './model/time'; + +class Observable { + private callbacks: ((value: T) => void)[] = []; + private value: T | undefined; + + constructor( + readonly designation: Designation + ) {} + + register(callback: (value: T) => void): { dispose(): void } { + this.callbacks.push(callback); + return { dispose: () => this.callbacks.splice(this.callbacks.indexOf(callback), 1) }; + } + + query(): T | undefined { + return this.value; + } + + update(newValue: T) { + if (this.value !== newValue) { + this.value = newValue; + this.callbacks = this.callbacks.filter((callback) => { + const retain = callback(newValue); + return retain === undefined || retain; + }); + } + } +} + +export class Observer { + private observables: Map> = new Map(); + private reference: Reference | undefined; + + private samplePromise: Promise | undefined; + private refreshNeeded: boolean = false; + + private subscription: vscode.Disposable; + + constructor( + private rtlDebugger: CXXRTLDebugger, + private referenceName: string, + ) { + this.subscription = rtlDebugger.onDidChangeCurrentTime((_time) => + this.invalidate()); + } + + dispose() { + this.subscription.dispose(); + } + + query(designation: Designation): T | undefined { + const observable = this.observables.get(designation.canonicalKey); + return observable?.query(); + } + + observe(designation: Designation, callback: (value: T) => any): { dispose(): void } { + let observable = this.observables.get(designation.canonicalKey); + if (observable === undefined) { + observable = new Observable(designation); + this.observables.set(designation.canonicalKey, observable); + this.reference = undefined; // invalidate reference + this.invalidate(); + } + return observable.register(callback); + } + + invalidate(): void { + this.refreshNeeded = true; + if (this.samplePromise === undefined) { + this.samplePromise = this.refresh(); + } + } + + private async refresh(): Promise { + while (this.refreshNeeded) { + this.refreshNeeded = false; + if (this.reference === undefined) { + const unboundReference = new UnboundReference(); + for (const observable of this.observables.values()) { + unboundReference.add(observable.designation); + } + this.reference = this.rtlDebugger.bindReference(this.referenceName, unboundReference); + } + const interval = new TimeInterval(this.rtlDebugger.currentTime, this.rtlDebugger.currentTime); + const reference = this.reference; // could get invalidated in the meantime + const [sample] = await this.rtlDebugger.queryInterval(interval, reference); + for (const [designation, handle] of reference.allHandles()) { + const observable = this.observables.get(designation.canonicalKey)!; + observable.update(sample.extract(handle)); + } + // ... but we could've got another invalidate() call while awaiting, so check again. + } + this.samplePromise = undefined; + } +} diff --git a/src/ui/sidebar.ts b/src/ui/sidebar.ts index e69c5cb..e31e42a 100644 --- a/src/ui/sidebar.ts +++ b/src/ui/sidebar.ts @@ -1,30 +1,15 @@ import * as vscode from 'vscode'; -import { CXXRTLDebugger, CXXRTLSessionStatus } from '../debugger'; + import { ModuleScope, Scope } from '../model/scope'; import { MemoryVariable, ScalarVariable, Variable } from '../model/variable'; -import { DisplayStyle, variableDescription, variableBitIndices, memoryRowIndices } from '../model/styling'; - -function buildBooleanLikeTreeItem(name: string): vscode.TreeItem { - const treeItem = new vscode.TreeItem(name); - treeItem.iconPath = new vscode.ThemeIcon('symbol-boolean'); - return treeItem; -} - -function buildScalarLikeTreeItem(name: string): vscode.TreeItem { - const treeItem = new vscode.TreeItem(name, vscode.TreeItemCollapsibleState.Collapsed); - treeItem.iconPath = new vscode.ThemeIcon('symbol-variable'); - return treeItem; -} - -function buildMemoryLikeTreeItem(name: string): vscode.TreeItem { - const treeItem = new vscode.TreeItem(name, vscode.TreeItemCollapsibleState.Collapsed); - treeItem.iconPath = new vscode.ThemeIcon('symbol-array'); - return treeItem; -} +import { DisplayStyle, variableDescription, variableBitIndices, memoryRowIndices, variableValue, variableTooltip } from '../model/styling'; +import { CXXRTLDebugger, CXXRTLSessionStatus } from '../debugger'; +import { Observer } from '../observer'; +import { Designation, MemoryRangeDesignation, MemoryRowDesignation, ScalarDesignation } from '../model/sample'; abstract class TreeItem { constructor( - readonly displayStyle: DisplayStyle, + readonly provider: TreeDataProvider ) {} abstract getTreeItem(): vscode.TreeItem | Thenable; @@ -32,120 +17,118 @@ abstract class TreeItem { getChildren(): vscode.ProviderResult { return []; } -} -class VariableTreeItem extends TreeItem { - constructor( - displayStyle: DisplayStyle, - readonly variable: Variable, - ) { - super(displayStyle); - } - - override getTreeItem(): vscode.TreeItem { - let treeItem; - if (this.variable instanceof ScalarVariable) { - if (this.variable.width === 1) { - treeItem = buildBooleanLikeTreeItem(this.variable.name); - } else { - treeItem = buildScalarLikeTreeItem(this.variable.name); - } - } else if (this.variable instanceof MemoryVariable) { - treeItem = buildMemoryLikeTreeItem(this.variable.name); - } else { - throw new Error(`Unknown variable kind ${this.variable}`); - } - treeItem.description = variableDescription(this.displayStyle, this.variable); - treeItem.tooltip = new vscode.MarkdownString(this.variable.fullName.join('.')); - treeItem.tooltip.isTrusted = true; - if (this.variable.location) { - treeItem.tooltip.appendMarkdown(`\n\n- ${this.variable.location.asMarkdownLink()}`); - treeItem.command = this.variable.location.asOpenCommand(); - } - return treeItem; + get displayStyle(): DisplayStyle { + return this.provider.displayStyle; } - override getChildren(): TreeItem[] { - const children = []; - if (this.variable instanceof ScalarVariable && this.variable.width > 1) { - // TODO: Extremely big variables (>1000 bits?) need to be chunked into groups. - for (const bitIndex of variableBitIndices(this.displayStyle, this.variable)) { - children.push(new ScalarBitTreeItem(this.displayStyle, this.variable, bitIndex)); - } - } else if (this.variable instanceof MemoryVariable) { - // TODO: Big memories (>100 rows?) need to be chunked into groups. - for (const rowIndex of memoryRowIndices(this.variable)) { - children.push(new MemoryRowTreeItem(this.displayStyle, this.variable, rowIndex)); - } - } - return children; + getValue(designation: Designation): T | undefined { + return this.provider.getValue(this, designation); } } -class ScalarBitTreeItem extends TreeItem { +class BitTreeItem extends TreeItem { constructor( - displayStyle: DisplayStyle, - readonly variable: Variable, + provider: TreeDataProvider, + readonly designation: ScalarDesignation | MemoryRowDesignation, readonly bitIndex: number, ) { - super(displayStyle); + super(provider); + } + + get variable(): Variable { + return this.designation.variable; } override getTreeItem(): vscode.TreeItem { - return buildBooleanLikeTreeItem(`${this.variable.name}[${this.bitIndex}]`); + const variable = this.designation.variable; + const treeItem = new vscode.TreeItem(variable.name); + if (this.designation instanceof MemoryRowDesignation) { + treeItem.label += `[${this.designation.index}]`; + } + treeItem.label += `[${this.bitIndex}]`; + treeItem.iconPath = new vscode.ThemeIcon('symbol-boolean'); + const value = this.getValue(this.designation); + if (value === undefined) { + treeItem.description = '= ...'; + } else { + treeItem.description = ((value & (1n << BigInt(this.bitIndex))) !== 0n) + ? '= 1' + : '= 0'; + } + treeItem.tooltip = variableTooltip(variable); + return treeItem; } } -class MemoryRowTreeItem extends TreeItem { +class ScalarTreeItem extends TreeItem { constructor( - displayStyle: DisplayStyle, - readonly variable: Variable, - readonly rowIndex: number, + provider: TreeDataProvider, + readonly designation: ScalarDesignation | MemoryRowDesignation, ) { - super(displayStyle); + super(provider); } override getTreeItem(): vscode.TreeItem { - if (this.variable.width === 1) { - return buildBooleanLikeTreeItem(`${this.variable.name}[${this.rowIndex}]`); - } else { - return buildScalarLikeTreeItem(`${this.variable.name}[${this.rowIndex}]`); + const variable = this.designation.variable; + const treeItem = new vscode.TreeItem(variable.name); + if (this.designation instanceof MemoryRowDesignation) { + treeItem.label += `[${this.designation.index}]`; } + if (variable.width > 1) { + treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } + treeItem.iconPath = (variable.width === 1) + ? new vscode.ThemeIcon('symbol-boolean') + : new vscode.ThemeIcon('symbol-variable'); + const value = this.getValue(this.designation); + treeItem.description = variableDescription(this.displayStyle, variable, { scalar: true }); + treeItem.description += (treeItem.description !== '') ? ' = ' : '= '; + treeItem.description += variableValue(this.displayStyle, variable, value); + treeItem.tooltip = variableTooltip(variable); + treeItem.command = variable.location?.asOpenCommand(); + return treeItem; } override getChildren(): TreeItem[] { - const children = []; - if (this.variable.width > 1) { - // TODO: Extremely big variables (>1000 bits?) need to be chunked into groups. - for (const bitIndex of variableBitIndices(this.displayStyle, this.variable)) { - children.push(new MemoryBitTreeItem(this.displayStyle, this.variable, this.rowIndex, bitIndex)); - } - } - return children; + const variable = this.designation.variable; + return Array.from(variableBitIndices(this.displayStyle, variable)).map((index) => + new BitTreeItem(this.provider, this.designation, index)); } } -class MemoryBitTreeItem extends TreeItem { +class ArrayTreeItem extends TreeItem { constructor( - displayStyle: DisplayStyle, - readonly variable: Variable, - readonly rowIndex: number, - readonly bitIndex: number, + provider: TreeDataProvider, + readonly designation: MemoryRangeDesignation, ) { - super(displayStyle); + super(provider); } override getTreeItem(): vscode.TreeItem { - return buildBooleanLikeTreeItem(`${this.variable.name}[${this.rowIndex}][${this.bitIndex}]`); + const variable = this.designation.variable; + const treeItem = new vscode.TreeItem(variable.name); + treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + treeItem.iconPath = new vscode.ThemeIcon('symbol-array'); + treeItem.description = variableDescription(this.displayStyle, variable); + treeItem.tooltip = variableTooltip(variable); + treeItem.command = variable.location?.asOpenCommand(); + return treeItem; + } + + override getChildren(): TreeItem[] { + const variable = this.designation.variable; + return Array.from(memoryRowIndices(variable)).map((index) => + new ScalarTreeItem(this.provider, variable.designation(index))); } } class ScopeTreeItem extends TreeItem { constructor( - displayStyle: DisplayStyle, + provider: TreeDataProvider, readonly scope: Scope, ) { - super(displayStyle); + super(provider); } override async getTreeItem(): Promise { @@ -176,10 +159,15 @@ class ScopeTreeItem extends TreeItem { override async getChildren(): Promise { const children = []; for (const scope of await this.scope.scopes) { - children.push(new ScopeTreeItem(this.displayStyle, scope)); + children.push(new ScopeTreeItem(this.provider, scope)); } for (const variable of await this.scope.variables) { - children.push(new VariableTreeItem(this.displayStyle, variable)); + if (variable instanceof ScalarVariable) { + children.push(new ScalarTreeItem(this.provider, variable.designation())); + } + if (variable instanceof MemoryVariable) { + children.push(new ArrayTreeItem(this.provider, variable.designation())); + } } return children; } @@ -189,20 +177,20 @@ export class TreeDataProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - constructor( - readonly rtlDebugger: CXXRTLDebugger - ) { + private rtlDebugger: CXXRTLDebugger; + private observer: Observer; + + constructor(rtlDebugger: CXXRTLDebugger) { + this.rtlDebugger = rtlDebugger; + this.observer = new Observer(rtlDebugger, 'sidebar'); + vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration('rtlDebugger.displayStyle')) { this._onDidChangeTreeData.fire(null); } }); - rtlDebugger.onDidChangeSessionStatus((_state) => { - this._onDidChangeTreeData.fire(null); - }); - rtlDebugger.onDidChangeCurrentTime((_time) => { - this._onDidChangeTreeData.fire(null); - }); + rtlDebugger.onDidChangeSessionStatus((_state) => + this._onDidChangeTreeData.fire(null)); } getTreeItem(element: TreeItem): vscode.TreeItem | Thenable { @@ -210,17 +198,29 @@ export class TreeDataProvider implements vscode.TreeDataProvider { } async getChildren(element?: TreeItem): Promise { - if (this.rtlDebugger.sessionStatus !== CXXRTLSessionStatus.Running) { - return []; - } else if (element !== undefined) { + if (element !== undefined) { return await element.getChildren(); } else { - const displayStyle = vscode.workspace.getConfiguration('rtlDebugger') - .get('displayStyle') as DisplayStyle; - const rootScope = await this.rtlDebugger.getRootScope(); - return [ - new ScopeTreeItem(displayStyle, rootScope), - ]; + if (this.rtlDebugger.sessionStatus === CXXRTLSessionStatus.Running) { + return [ + new ScopeTreeItem(this, await this.rtlDebugger.getRootScope()), + ]; + } else { + return []; + } } } + + get displayStyle(): DisplayStyle { + const displayStyle = vscode.workspace.getConfiguration('rtlDebugger').get('displayStyle'); + return displayStyle as DisplayStyle; + } + + getValue(element: TreeItem, designation: Designation): T | undefined { + this.observer.observe(designation, (_value) => { + this._onDidChangeTreeData.fire(element); + return false; // one-shot + }); + return this.observer.query(designation); + } } diff --git a/src/ui/status.ts b/src/ui/status.ts index 67464ec..30ecddf 100644 --- a/src/ui/status.ts +++ b/src/ui/status.ts @@ -28,21 +28,21 @@ export class StatusBarItem { } else { this.statusItem.show(); if (this.rtlDebugger.sessionStatus === CXXRTLSessionStatus.Starting) { - this.statusItem.text = `$(gear~spin) Starting...`; - this.statusItem.tooltip = `RTL Debugger: Starting`; + this.statusItem.text = '$(gear~spin) Starting...'; + this.statusItem.tooltip = 'RTL Debugger: Starting'; } else { // this.sessionState === CXXRTLSessionState.Running if (this.rtlDebugger.simulationStatus === CXXRTLSimulationStatus.Running) { this.statusItem.text = '$(debug-pause) '; - this.statusItem.tooltip = `RTL Debugger: Running`; + this.statusItem.tooltip = 'RTL Debugger: Running'; } else if (this.rtlDebugger.simulationStatus === CXXRTLSimulationStatus.Paused) { this.statusItem.text = '$(debug-continue) '; - this.statusItem.tooltip = `RTL Debugger: Paused`; + this.statusItem.tooltip = 'RTL Debugger: Paused'; } else if (this.rtlDebugger.simulationStatus === CXXRTLSimulationStatus.Finished) { this.statusItem.text = ''; - this.statusItem.tooltip = `RTL Debugger: Finished`; + this.statusItem.tooltip = 'RTL Debugger: Finished'; } this.statusItem.text += `${this.rtlDebugger.currentTime} / ${this.rtlDebugger.latestTime}`; } } } -}; +}