src/adapter/daffodilRuntime.ts (311 lines of code) (raw):

/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { EventEmitter } from 'events' export interface FileAccessor { readFile(path: string): Promise<string> } export interface IDaffodilBreakpoint { id: number line: number verified: boolean } interface IStepInTargets { id: number label: string } interface IStackFrame { index: number name: string file: string line: number column?: number } interface IStack { count: number frames: IStackFrame[] } /** * A Daffodil runtime with minimal debugger functionality. */ export class DaffodilRuntime extends EventEmitter { // the initial (and one and only) file we are 'debugging' private _sourceFile: string = '' public get sourceFile() { return this._sourceFile } // the contents (= lines) of the one and only file private _sourceLines: string[] = [] // This is the next line that will be 'executed' private _currentLine = 0 private _currentColumn: number | undefined // maps from sourceFile to array of Daffodil breakpoints private _breakPoints = new Map<string, IDaffodilBreakpoint[]>() // since we want to send breakpoint events, we will assign an id to every event // so that the frontend can match events with breakpoints. private _breakpointId = 1 private _breakAddresses = new Set<string>() private _noDebug = false private _namedException: string | undefined private _otherExceptions = false constructor(private _fileAccessor: FileAccessor) { super() } /** * Start executing the given program. */ public async start( program: string, stopOnEntry: boolean, noDebug: boolean ): Promise<void> { this._noDebug = noDebug await this.loadSource(program) this._currentLine = -1 await this.verifyBreakpoints(this._sourceFile) if (stopOnEntry) { // we step once this.step(false, 'stopOnEntry') } else { // we just start to run until we hit a breakpoint or an exception this.continue() } } /** * Continue execution to the end/beginning. */ public continue(reverse = false) { this.run(reverse, undefined) } /** * Step to the next/previous non empty line. */ public step(reverse = false, event = 'stopOnStep') { this.run(reverse, event) } /** * "Step into" for Daffodil Debug means: go to next character */ public stepIn(targetId: number | undefined) { if (typeof targetId === 'number') { this._currentColumn = targetId this.sendEvent('stopOnStep') } else { if (typeof this._currentColumn === 'number') { if ( this._currentColumn <= this._sourceLines[this._currentLine].length ) { this._currentColumn += 1 } } else { this._currentColumn = 1 } this.sendEvent('stopOnStep') } } /** * "Step out" for Daffodil Debug means: go to previous character */ public stepOut() { if (typeof this._currentColumn === 'number') { this._currentColumn -= 1 if (this._currentColumn === 0) { this._currentColumn = undefined } } this.sendEvent('stopOnStep') } public getStepInTargets(frameId: number): IStepInTargets[] { const line = this._sourceLines[this._currentLine].trim() // every word of the current line becomes a stack frame. const words = line.split(/\s+/) // return nothing if frameId is out of range if (frameId < 0 || frameId >= words.length) { return [] } // pick the frame for the given frameId const frame = words[frameId] const pos = line.indexOf(frame) // make every character of the frame a potential "step in" target return frame.split('').map((c, ix) => { return { id: pos + ix, label: `target: ${c}`, } }) } /** * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line. */ public stack(startFrame: number, endFrame: number): IStack { const words = this._sourceLines[this._currentLine].trim().split(/\s+/) const frames = new Array<IStackFrame>() // every word of the current line becomes a stack frame. for (let i = startFrame; i < Math.min(endFrame, words.length); i++) { const name = words[i] // use a word of the line as the stackframe name const stackFrame: IStackFrame = { index: i, name: `${name}(${i})`, file: this._sourceFile, line: this._currentLine, } if (typeof this._currentColumn === 'number') { stackFrame.column = this._currentColumn } frames.push(stackFrame) } return { frames: frames, count: words.length, } } public getBreakpoints(path: string, line: number): number[] { const l = this._sourceLines[line] let sawSpace = true const bps: number[] = [] for (let i = 0; i < l.length; i++) { if (l[i] !== ' ') { if (sawSpace) { bps.push(i) sawSpace = false } } else { sawSpace = true } } return bps } /* * Set breakpoint in file with given line. */ public async setBreakPoint( path: string, line: number ): Promise<IDaffodilBreakpoint> { const bp: IDaffodilBreakpoint = { verified: false, line, id: this._breakpointId++, } let bps = this._breakPoints.get(path) if (!bps) { bps = new Array<IDaffodilBreakpoint>() this._breakPoints.set(path, bps) } bps.push(bp) await this.verifyBreakpoints(path) return bp } /* * Clear breakpoint in file with given line. */ public clearBreakPoint( path: string, line: number ): IDaffodilBreakpoint | undefined { const bps = this._breakPoints.get(path) if (bps) { const index = bps.findIndex((bp) => bp.line === line) if (index >= 0) { const bp = bps[index] bps.splice(index, 1) return bp } } return undefined } /* * Clear all breakpoints for file. */ public clearBreakpoints(path: string): void { this._breakPoints.delete(path) } /* * Set data breakpoint. */ public setDataBreakpoint(address: string): boolean { if (address) { this._breakAddresses.add(address) return true } return false } public setExceptionsFilters( namedException: string | undefined, otherExceptions: boolean ): void { this._namedException = namedException this._otherExceptions = otherExceptions } /* * Clear all data breakpoints. */ public clearAllDataBreakpoints(): void { this._breakAddresses.clear() } // private methods private async loadSource(file: string): Promise<void> { if (this._sourceFile !== file) { this._sourceFile = file const contents = await this._fileAccessor.readFile(file) this._sourceLines = contents.split(/\r?\n/) } } /** * Run through the file. * If stepEvent is specified only run a single step and emit the stepEvent. */ private run(reverse = false, stepEvent?: string) { if (reverse) { for (let ln = this._currentLine - 1; ln >= 0; ln--) { if (this.fireEventsForLine(ln, stepEvent)) { this._currentLine = ln this._currentColumn = undefined return } } // no more lines: stop at first line this._currentLine = 0 this._currentColumn = undefined this.sendEvent('stopOnEntry') } else { for ( let ln = this._currentLine + 1; ln < this._sourceLines.length; ln++ ) { if (this.fireEventsForLine(ln, stepEvent)) { this._currentLine = ln this._currentColumn = undefined return true } } // no more lines: run to end this.sendEvent('end') } } private async verifyBreakpoints(path: string): Promise<void> { if (this._noDebug) { return } const bps = this._breakPoints.get(path) if (bps) { await this.loadSource(path) bps.forEach((bp) => { if (!bp.verified && bp.line < this._sourceLines.length) { const srcLine = this._sourceLines[bp.line].trim() // if a line is empty or starts with '+' we don't allow to set a breakpoint but move the breakpoint down if (srcLine.length === 0 || srcLine.indexOf('+') === 0) { bp.line++ } // if a line starts with '-' we don't allow to set a breakpoint but move the breakpoint up if (srcLine.indexOf('-') === 0) { bp.line-- } // don't set 'verified' to true if the line contains the word 'lazy' // in this case the breakpoint will be verified 'lazy' after hitting it once. if (srcLine.indexOf('lazy') < 0) { bp.verified = true this.sendEvent('breakpointValidated', bp) } } }) } } /** * Fire events if line has a breakpoint or the word 'exception' or 'exception(...)' is found. * Returns true if execution needs to stop. */ private fireEventsForLine(ln: number, stepEvent?: string): boolean { if (this._noDebug) { return false } const line = this._sourceLines[ln].trim() // if 'log(...)' found in source -> send argument to debug console const matches = /log\((.*)\)/.exec(line) if (matches && matches.length === 2) { this.sendEvent('output', matches[1], this._sourceFile, ln, matches.index) } // if a word in a line matches a data breakpoint, fire a 'dataBreakpoint' event const words = line.split(' ') for (const word of words) { if (this._breakAddresses.has(word)) { this.sendEvent('stopOnDataBreakpoint') return true } } // if pattern 'exception(...)' found in source -> throw named exception const matches2 = /exception\((.*)\)/.exec(line) if (matches2 && matches2.length === 2) { const exception = matches2[1].trim() if (this._namedException === exception) { this.sendEvent('stopOnException', exception) return true } else { if (this._otherExceptions) { this.sendEvent('stopOnException', undefined) return true } } } else { // if word 'exception' found in source -> throw exception if (line.indexOf('exception') >= 0) { if (this._otherExceptions) { this.sendEvent('stopOnException', undefined) return true } } } // is there a breakpoint? const breakpoints = this._breakPoints.get(this._sourceFile) if (breakpoints) { const bps = breakpoints.filter((bp) => bp.line === ln) if (bps.length > 0) { // send 'stopped' event this.sendEvent('stopOnBreakpoint') // the following shows the use of 'breakpoint' events to update properties of a breakpoint in the UI // if breakpoint is not yet verified, verify it now and send a 'breakpoint' update event if (!bps[0].verified) { bps[0].verified = true this.sendEvent('breakpointValidated', bps[0]) } return true } } // non-empty line if (stepEvent && line.length > 0) { this.sendEvent(stepEvent) return true } // nothing interesting found -> continue return false } private sendEvent(event: string, ...args: any[]) { setImmediate((_) => { this.emit(event, ...args) }) } }