'use strict' ; import { readFileSync } from 'fs' ; import { EventEmitter } from 'events' ; import { relative } from 'path' ; export interface HappeningBreakpoint { id : number ; line : number ; verified : boolean ; } /**
 * A Plan debugging runtime.
 */ export class PlanDebugRuntime extends EventEmitter { private _sourceFile : string | undefined ; public get sourceFile ( ) : string { if ( ! this . _sourceFile ) { throw new Error ( "Debugger not initialized" ) ; } return this . _sourceFile ; } private _sourceLines : string [ ] | undefined ; private _currentLine = 0 ; private _breakPoints = new Map < string , HappeningBreakpoint [ ] > ( ) ; private _breakpointId = 1 ; constructor ( ) { super ( ) ; } /**
     * Start executing the given program.
     */ public start ( program , stopOnEntry ) { this . loadSource ( program ) ; this . _currentLine = - 1 ; if ( ! this . _sourceFile ) { throw new Error ( "Debugger not initialized" ) ; } this . verifyBreakpoints ( this . _sourceFile ) ; if ( stopOnEntry ) { this . step ( false , 'stopOnEntry' ) ; } else { 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 ) ; } /**
     * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line.
     */ public stack ( startFrame , endFrame ) { if ( ! this . _sourceLines ) { throw new Error ( "Debugger is not initialized." ) ; } const words = this . _sourceLines [ this . _currentLine ] . trim ( ) . split ( /\s+/ ) ; const frames = new Array < any > ( ) ; for ( let i = startFrame ; i < Math . min ( endFrame , words . length ) ; i ++ ) { const name = words [ i ] ; frames . push ( { index : i , name : `${ name }(${ i })` , file : this . _sourceFile , line : this . _currentLine } ) ; } return { frames : frames , count : words . length } ; } public setBreakPoint ( path , line ) { const bp = { verified : false , line , id : this . _breakpointId ++ } ; let bps = this . _breakPoints . get ( path ) ; if ( ! bps ) { bps = new Array < HappeningBreakpoint > ( ) ; this . _breakPoints . set ( path , bps ) ; } bps . push ( bp ) ; this . verifyBreakpoints ( path ) ; return bp ; } public clearBreakPoint ( path , line ) { 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 ; } public clearBreakpoints ( path ) { this . _breakPoints . delete ( path ) ; } private loadSource ( file ) { if ( ! this . _sourceFile || relative ( file , this . _sourceFile ) ) { this . _sourceFile = file ; this . _sourceLines = readFileSync ( this . _sourceFile ) . toString ( ) . split ( '\n' ) ; } } /**
     * Run through the file.
     * If stepEvent is specified only run a single step and emit the stepEvent.
     */ private run ( reverse = false , stepEvent ? ) { if ( ! this . _sourceLines ) { throw new Error ( "Debugger is not initialized." ) ; } if ( reverse ) { for ( let ln = this . _currentLine - 1 ; ln >= 0 ; ln -- ) { if ( this . fireEventsForLine ( ln , stepEvent ) ) { this . _currentLine = ln ; return ; } } this . _currentLine = 0 ; this . sendEvent ( 'stopOnEntry' ) ; } else { for ( let ln = this . _currentLine + 1 ; ln < this . _sourceLines . length ; ln ++ ) { if ( this . fireEventsForLine ( ln , stepEvent ) ) { this . _currentLine = ln ; return ; } } this . sendEvent ( 'end' ) ; } } private verifyBreakpoints ( path ) { if ( ! this . _sourceLines ) { throw new Error ( "Debugger is not initialized." ) ; } const bps = this . _breakPoints . get ( path ) ; if ( bps ) { this . loadSource ( path ) ; bps . forEach ( bp => { if ( ! bp . verified && bp . line < this . _sourceLines ! . length ) { const srcLine = this . _sourceLines ! [ bp . line ] . trim ( ) ; if ( srcLine . length === 0 || srcLine . indexOf ( '+' ) === 0 ) { bp . line ++ ; } if ( srcLine . indexOf ( '-' ) === 0 ) { bp . line -- ; } if ( srcLine . indexOf ( 'lazy' ) < 0 ) { bp . verified = true ; this . sendEvent ( 'breakpointValidated' , bp ) ; } } } ) ; } } /**
     * Fire events if line has a breakpoint or the word 'exception' is found.
     * Returns true is execution needs to stop.
     */ private fireEventsForLine ( ln , stepEvent ? ) { if ( ! this . _sourceFile || ! this . _sourceLines ) { throw new Error ( "Debugger not initialized." ) ; } const line = this . _sourceLines [ ln ] . trim ( ) ; const matches = /log\((.*)\)/ . exec ( line ) ; if ( matches && matches . length === 2 ) { this . sendEvent ( 'output' , matches [ 1 ] , this . _sourceFile , ln , matches . index ) ; } if ( line . indexOf ( 'exception' ) >= 0 ) { this . sendEvent ( 'stopOnException' ) ; return true ; } const breakpoints = this . _breakPoints . get ( this . _sourceFile ) ; if ( breakpoints ) { const bps = breakpoints . filter ( bp => bp . line === ln ) ; if ( bps . length > 0 ) { this . sendEvent ( 'stopOnBreakpoint' ) ; if ( ! bps [ 0 ] . verified ) { bps [ 0 ] . verified = true ; this . sendEvent ( 'breakpointValidated' , bps [ 0 ] ) ; } return true ; } } if ( stepEvent && line . length > 0 ) { this . sendEvent ( stepEvent ) ; return true ; } return false ; } private sendEvent ( event , ... args ) { setImmediate ( ( ) => { this . emit ( event , ... args ) ; } ) ; } }