import IParserDebug from "../parsertooling/IParserDebug"; import PObjectMap, { PRuleResult, IPResult } from "./PObjectMap"; import SeepParser from "./SeepParser"; import GRule from "./grammar/GRule"; import ParserDebug from "../parsertooling/ParserDebug"; import ObjectMapper from "./ObjectMapper"; import BreakExpression from "../parsertooling/breakpoint/BreakExpression"; import RuleState from "../parsertooling/RuleState"; import ParserState from "../parsertooling/ParserState"; import {Position, ISourceList } from "@cafetextual/util"; import Assert from "@cafetextual/util/dist/src/assert/Assert"; import SourceLocation from "@cafetextual/util/dist/src/source/SourceLocation"; /** * Manages parsing by line + debugger integration. * */ export default class IncrementalParser implements IParserDebug { /** * Optional debugger */ debugger:IParserDebug; out:PRuleResult; private _p:SeepParser; private _rule0:GRule; private _breakExp:BreakExpression; private _content:ISourceList; // parser state - don't alter these externally (could make read only) private line0:number; private isFirst:boolean = false; char0:number; // invalidation state (dont' alter these externally - could make read only) line1:number; char1:number; invOffset:number; // used when characters inserted line2:number; char2:number; get ___currentLineObject():any { return this._content.indexToObject(this.line0);// derivable from line0 } private _done:boolean = false; inProgress():boolean { return !this.done && !this.hasErrored() && !this.atBreakpoint(); } get done():boolean { return this._done; } hasErrored():boolean { return this._errored; //_lastOut && !_lastOut.isValid(null, _lastLineIndex ); } /** * The root result. */ private _root:PRuleResult; get root():PRuleResult { return this._root; } /** * Simply clears any cached values, doesn't reset state - be sure to subsequently call invalidate */ clearCache(lineID:number):void { var state:RuleState = this.getCachedState(lineID); if (state) { state.release(); } this.cacheState(null,null,lineID); } // removeNodes getLastValidLine():number { return this._lastLineIndex; } /** * Invalidates the parser at a given index, and prepares it for incremental parsing * * - requires previous line to have been sucessfully parsed * */ invalidateLine(lineID:number):ParserState { // x. if the invalidating line comes after the last valid, the return. no further cached data is valid // (at least until we have the possibility to re-stitch)) // - except if (this._content.isAfter(lineID, this._lastLineIndex) && !(this._content.first() == lineID) ) { var nextLineID:number = this._content.nextIndex(this._lastLineIndex) // the only exception is the case when we have errored, and we invalidate the next line // this may be a) the line that errored, b) a new line inserted, or moved up as a result of a line being deleted if (this._done && ! this._content.isBefore(this._content.last(false), this._lastLineIndex ) ) { this._done = false } if (!( (this._errored ) && (nextLineID >= 0) && (lineID == nextLineID)) ) { return null; // <--- no need } } this._errored = false; this._done = false if (lineID == this._content.first()) { // TODO - reset state; this.init(this._p, this._content, this._rule0, this._breakExp); ///init(); } else { // retrieve the cloned state (TO_DO - reconstruct state) //var tmpNode:* = _content.indexToObject(lineID); var prevID:number = this._content.prevIndex(lineID); //var node:* = _content.indexToObject(lineID); // debug only //var prevNode:* = _content.indexToObject(prevID); // debug only var state:RuleState = this._parserStatesByLine[prevID] if (!state) { Assert.fail(); } state = state.clone(null, null,null,null, []) as RuleState; state.trim(lineID); this._lastState = state; this._lastOut = this.toParentLineRule(state); this._lastLineIndex = prevID; Assert.assert(this._lastOut.isValid(null, this._lastLineIndex)); // reset the parser's position to the end of the previous line this._p.setPosition(lineID, 0 /* technically, this should be the end if the line, but in practice we'll be advancing to the next line anyway */ ); } return this._lastState; } // invalidateLine init(p:SeepParser, content:ISourceList, defaultRule:GRule, breakExp:BreakExpression = null):void { this._errored = false; this._p = p; this._breakExp = breakExp; this._p.init(content); this._lastErrorOut = null; this._p.debugger = this as IParserDebug; this._content = content; this._rule0 = defaultRule; this._lastLineIndex = -1; this._debugState = null; Assert.assert(this._rule0.isValid()); // don't forget to set a valid rule this.line0 = content.first(); this.isFirst = true; this.char0 = 0; this._root = null; this.line1 = this.line2 = this.char1 = this.char2 = this.invOffset = -1; this._lastOut = null; this._lastState = null; this._done = (this.line0 < 0); // -ve index means done // TODO - manage release of objects. this._parserStatesByLine = {}; this._resultByLine = {}; } // init private _lastState:RuleState; private _lastOut:PRuleResult; private _lastErrorOut:PRuleResult; private _lastLineIndex:number = -1; private _lastLine:PRuleResult; private _errored:boolean = false; // hash of ParserState by line id private _parserStatesByLine:{[lineID:number]:RuleState} ; private _resultByLine:{[lineID:number]:IPResult}; private cacheState(state:RuleState, result:IPResult, lineID:number):void { var stack:Array = []; this._parserStatesByLine[lineID] = state ? state.clone(null, null,null, null, stack) as RuleState: null; this._resultByLine[lineID] = result; } private getCachedState(lineID:number):RuleState { return this._parserStatesByLine[lineID]; } private _debugState:ParserState; breakpointState():ParserState { return this._debugState; } atBreakpoint():boolean { return this._debugState != null; } private _stepDebugger:IParserDebug; step(stepDebugger:IParserDebug):PRuleResult { Assert.assert(this.atBreakpoint()); this._stepDebugger = stepDebugger; this._p.resume(this._debugState); this._stepDebugger = null; return this.postParse(); } // step cont():PRuleResult { Assert.assert(this.atBreakpoint()); var tmp:BreakExpression = this._breakExp; this._p.resume(this._debugState); return this.postParse(); } // cont private postParse():PRuleResult { if (this.atBreakpoint()) { return null; } if (this.isFirst) { return this.afterFirstLineParse(); } return this.afterLineParse(); } private afterFirstLineParse():PRuleResult { this.isFirst = false; var resultOut:PRuleResult; if (this._errored) { resultOut = this._lastErrorOut } else { this._lastErrorOut = null; resultOut = this._lastOut; Assert.assert(this._lastState != null); } if (resultOut.parentLineResult) { this._root = resultOut.parentLineResult; } else { this._root = resultOut; } if (!this._errored) { this.cacheState(this._lastState, this._root, this.line0); } else { this.cacheState(null, null, this.line0); } return resultOut } afterLineParse():PRuleResult { if (!this._errored) { if (!this.done) { this.cacheState(this._lastState, this._lastOut, this._lastLineIndex ); } this._lastErrorOut = null; } else { // could remove the cache results of the line that // would have been replaced // cacheState(null, null, _lastLineIndex >> next ); } return this._errored ? this._lastErrorOut : this._lastOut; } curentLine():string { if (!( this.done )) { return this._p.getCurrentLine() } return null } parseLine():PRuleResult { Assert.assert( !this.hasErrored() ); Assert.assert( !this.atBreakpoint() ); if (this.isFirst/*line0 == _content.first()*/ && (this.char0 == 0)) { this._lastState = null; this._lastOut = null; this.out = this._p.rule(this._rule0); return this.postParse(); } else { var oldState:RuleState = this._lastState; this._lastState = null; this._lastOut = null; this._p.refresh(); this._p.resume(oldState); return this.postParse(); } //return null; } // parseLine erroredAt():number { return this._content.nextLineIndex(this.getLastValidLine()); } errorStr() :string { if (this._lastErrorOut) { return this._lastErrorOut.quickErrorStr() } return "unknown"; } errLocation():SourceLocation { if (this._lastErrorOut) { var loc:SourceLocation = PObjectMap.resultToLocation(this._lastErrorOut, true) // . problem when we've failed is that we don't have an end index if (loc.end.column < 0 && loc.end.line >= 0) { var txt :string = this._content.indexToValue(loc.end.line) loc = new SourceLocation(loc.start, new Position(loc.end.line, txt.length-1)) } return loc } var index:number = this._content.nextLineIndex(this.getLastValidLine()); if (index >= 0) { txt = this._content.indexToValue(index) return SourceLocation.create(index, 0, index, txt.length -1) } return null } private toParentLineRule(state:RuleState):PRuleResult { var currentState:RuleState = state while (currentState && !currentState.asLineRule) { if (currentState.parentElementState && currentState.parentElementState.parentRuleState) { currentState = currentState.parentElementState.parentRuleState } else { return currentState.ruleResult } } return currentState.ruleResult; } // toParentLineRule // ---- implements IParserDebug --- debug(state:ParserState):boolean { var pause:boolean = this.debug_low(state) this._debugState = null; if (pause) { return pause; } if (this._breakExp || this._stepDebugger) { if (this.done) { return pause; } // ParseDebugUtil.traceState(state); var dbg:IParserDebug = this._stepDebugger ? this._stepDebugger : this._breakExp var debugPause:boolean = dbg.debug(state); if (debugPause) { if (pause) { // 1. we have a regular line pause interacting /w our debug //Assert.fail(' not clear how to handle this'); return pause; } else { this._debugState = state; return true; } } } return pause; } debug_low(state:ParserState):boolean { this._done = this._p.eof(); switch (state.breakpoint) { case ParserDebug.LINE_RULE_FAIL: case ParserDebug.LINE_RULE_MATCH: //ParseDebugUtil.traceState(state); var lineRuleState:RuleState = state as RuleState var lineLastOut:PRuleResult = this.toParentLineRule(lineRuleState); // mechanism to prevent multiple pauses on a single line. // - this occurs when a child rule has child line rules, the parser dispatches // line match breakpoints for a rule when the child rule has already dispatched one. // - Arguably, it would be conceptually cleaner for the parser to be able to recognise and not // dispatch a line match even (in the sense that this dilutes the meaning of the line match breakpoint). // however it's very much simpler to use this mechanism. var line:number = lineRuleState.line1; if (line < 0) { console.log(' no line encountered '); // BUG - line1 not set } if ( (this._content.first() == line && this._lastLineIndex == -1 ) || this._content.isAfter(line, this._lastLineIndex) ) { if (state.breakpoint == ParserDebug.LINE_RULE_FAIL) { this._errored = (state.breakpoint == ParserDebug.LINE_RULE_FAIL); this._lastErrorOut = lineLastOut; } else { this._lastState = lineRuleState this._lastOut = lineLastOut; this._lastLineIndex = line; this._lastLine = this._lastOut; } return true; // when a child rule has child lines line } return false; // parser has failed to recognise that a child line has already called LINE_RULE_MATCH //break; case ParserDebug.SECONDARY_LINE_DONE: this._lastState = state as RuleState; this._lastOut = this._lastState.getLastSecondaryResult(); this._lastLineIndex = this._lastOut.startLineIndex; //_lastLineIndex = _p.getCurrentLineIndex(); TODO - _lastOut.line0 Assert.(index is first OR is next of line 0) return true; case ParserDebug.SECONDARY_PRE_LINE_DONE: this._lastState = state as RuleState; var preResults:Array = this._lastState.ruleResult.preResults this._lastOut = preResults[preResults.length -1]; this._lastLineIndex = this._lastOut.startLineIndex; //_lastLineIndex = _p.getCurrentLineIndex(); return true; case ParserDebug.DONE_WITH_UNMATCHED_TEXT: this._errored = true; this._lastOut = (state as RuleState).ruleResult; this._lastErrorOut = null // this is to indicate that the error occurs after the last good line return true; //_lastState = state as RuleState; //_lastLineIndex = (state as RuleState).lineRuleIndex; //if (_lastLineIndex < 0) { // _lastLineIndex = _lastState.line1; //} //_lastOut = (state as RuleState).ruleResult; //return true; // case ParserDebug.ELEMENT_FAIL: // sometimes useful for debug // console.log(' failed'); // return false; case ParserDebug.DONE: this._done = true; return false; //break; } return false; } // debug get remainder() :string { return this._p.remainder; } get currentMatch() :string { return this._p.currentMatch; } private _objectMapper:ObjectMapper = new ObjectMapper; mapdata():Object { this._objectMapper.mapValues(this._root, this._root.matchedRule, null, null, true /* isRoot */); this.root.map.content = this._content; return this.root.data; } } // class