UNPKG

chevrotain

Version:

Chevrotain is a high performance fault tolerant javascript parsing DSL for building recursive decent parsers

923 lines (822 loc) 30.3 kB
import { analyzeTokenTypes, charCodeToOptimizedIndex, cloneEmptyGroups, DEFAULT_MODE, LineTerminatorOptimizedTester, performRuntimeChecks, performWarningRuntimeChecks, SUPPORT_STICKY, validatePatterns } from "./lexer" import { cloneArr, cloneObj, forEach, IDENTITY, isArray, isEmpty, isUndefined, keys, last, map, merge, NOOP, PRINT_WARNING, reduce, reject, timer, toFastProperties } from "../utils/utils" import { augmentTokenTypes } from "./tokens" import { CustomPatternMatcherFunc, ILexerConfig, ILexerDefinitionError, ILexingError, IMultiModeLexerDefinition, IToken, TokenType } from "../../api" import { defaultLexerErrorProvider } from "../scan/lexer_errors_public" import { clearRegExpParserCache } from "./reg_exp_parser" export interface ILexingResult { tokens: IToken[] groups: { [groupName: string]: IToken[] } errors: ILexingError[] } export enum LexerDefinitionErrorType { MISSING_PATTERN, INVALID_PATTERN, EOI_ANCHOR_FOUND, UNSUPPORTED_FLAGS_FOUND, DUPLICATE_PATTERNS_FOUND, INVALID_GROUP_TYPE_FOUND, PUSH_MODE_DOES_NOT_EXIST, MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE, MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY, MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST, LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED, SOI_ANCHOR_FOUND, EMPTY_MATCH_PATTERN, NO_LINE_BREAKS_FLAGS, UNREACHABLE_PATTERN, IDENTIFY_TERMINATOR, CUSTOM_LINE_BREAK } export interface IRegExpExec { exec: CustomPatternMatcherFunc } const DEFAULT_LEXER_CONFIG: ILexerConfig = { deferDefinitionErrorsHandling: false, positionTracking: "full", lineTerminatorsPattern: /\n|\r\n?/g, lineTerminatorCharacters: ["\n", "\r"], ensureOptimizations: false, safeMode: false, errorMessageProvider: defaultLexerErrorProvider, traceInitPerf: false, skipValidations: false } Object.freeze(DEFAULT_LEXER_CONFIG) export class Lexer { public static SKIPPED = "This marks a skipped Token pattern, this means each token identified by it will" + "be consumed and then thrown into oblivion, this can be used to for example to completely ignore whitespace." public static NA = /NOT_APPLICABLE/ public lexerDefinitionErrors: ILexerDefinitionError[] = [] public lexerDefinitionWarning: ILexerDefinitionError[] = [] protected patternIdxToConfig: any = {} protected charCodeToPatternIdxToConfig: any = {} protected modes: string[] = [] protected defaultMode: string protected emptyGroups: { [groupName: string]: IToken } = {} private config: ILexerConfig = undefined private trackStartLines: boolean = true private trackEndLines: boolean = true private hasCustom: boolean = false private canModeBeOptimized: any = {} private traceInitPerf: boolean | number private traceInitMaxIdent: number private traceInitIndent: number constructor( protected lexerDefinition: TokenType[] | IMultiModeLexerDefinition, config: ILexerConfig = DEFAULT_LEXER_CONFIG ) { if (typeof config === "boolean") { throw Error( "The second argument to the Lexer constructor is now an ILexerConfig Object.\n" + "a boolean 2nd argument is no longer supported" ) } // todo: defaults func? this.config = merge(DEFAULT_LEXER_CONFIG, config) const traceInitVal = this.config.traceInitPerf if (traceInitVal === true) { this.traceInitMaxIdent = Infinity this.traceInitPerf = true } else if (typeof traceInitVal === "number") { this.traceInitMaxIdent = traceInitVal this.traceInitPerf = true } this.traceInitIndent = -1 this.TRACE_INIT("Lexer Constructor", () => { let actualDefinition: IMultiModeLexerDefinition let hasOnlySingleMode = true this.TRACE_INIT("Lexer Config handling", () => { if ( this.config.lineTerminatorsPattern === DEFAULT_LEXER_CONFIG.lineTerminatorsPattern ) { // optimized built-in implementation for the defaults definition of lineTerminators this.config.lineTerminatorsPattern = LineTerminatorOptimizedTester } else { if ( this.config.lineTerminatorCharacters === DEFAULT_LEXER_CONFIG.lineTerminatorCharacters ) { throw Error( "Error: Missing <lineTerminatorCharacters> property on the Lexer config.\n" + "\tFor details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS" ) } } if (config.safeMode && config.ensureOptimizations) { throw Error( '"safeMode" and "ensureOptimizations" flags are mutually exclusive.' ) } this.trackStartLines = /full|onlyStart/i.test( this.config.positionTracking ) this.trackEndLines = /full/i.test(this.config.positionTracking) // Convert SingleModeLexerDefinition into a IMultiModeLexerDefinition. if (isArray(lexerDefinition)) { actualDefinition = <any>{ modes: {} } actualDefinition.modes[DEFAULT_MODE] = cloneArr( <TokenType[]>lexerDefinition ) actualDefinition[DEFAULT_MODE] = DEFAULT_MODE } else { // no conversion needed, input should already be a IMultiModeLexerDefinition hasOnlySingleMode = false actualDefinition = cloneObj( <IMultiModeLexerDefinition>lexerDefinition ) } }) if (this.config.skipValidations === false) { this.TRACE_INIT("performRuntimeChecks", () => { this.lexerDefinitionErrors = this.lexerDefinitionErrors.concat( performRuntimeChecks( actualDefinition, this.trackStartLines, this.config.lineTerminatorCharacters ) ) }) this.TRACE_INIT("performWarningRuntimeChecks", () => { this.lexerDefinitionWarning = this.lexerDefinitionWarning.concat( performWarningRuntimeChecks( actualDefinition, this.trackStartLines, this.config.lineTerminatorCharacters ) ) }) } // for extra robustness to avoid throwing an none informative error message actualDefinition.modes = actualDefinition.modes ? actualDefinition.modes : {} // an error of undefined TokenTypes will be detected in "performRuntimeChecks" above. // this transformation is to increase robustness in the case of partially invalid lexer definition. forEach(actualDefinition.modes, (currModeValue, currModeName) => { actualDefinition.modes[currModeName] = reject<TokenType>( currModeValue, (currTokType) => isUndefined(currTokType) ) }) let allModeNames = keys(actualDefinition.modes) forEach( actualDefinition.modes, (currModDef: TokenType[], currModName) => { this.TRACE_INIT(`Mode: <${currModName}> processing`, () => { this.modes.push(currModName) if (this.config.skipValidations === false) { this.TRACE_INIT(`validatePatterns`, () => { this.lexerDefinitionErrors = this.lexerDefinitionErrors.concat( validatePatterns(<TokenType[]>currModDef, allModeNames) ) }) } // If definition errors were encountered, the analysis phase may fail unexpectedly/ // Considering a lexer with definition errors may never be used, there is no point // to performing the analysis anyhow... if (isEmpty(this.lexerDefinitionErrors)) { augmentTokenTypes(currModDef) let currAnalyzeResult this.TRACE_INIT(`analyzeTokenTypes`, () => { currAnalyzeResult = analyzeTokenTypes(currModDef, { lineTerminatorCharacters: this.config .lineTerminatorCharacters, positionTracking: config.positionTracking, ensureOptimizations: config.ensureOptimizations, safeMode: config.safeMode, tracer: this.TRACE_INIT.bind(this) }) }) this.patternIdxToConfig[currModName] = currAnalyzeResult.patternIdxToConfig this.charCodeToPatternIdxToConfig[currModName] = currAnalyzeResult.charCodeToPatternIdxToConfig this.emptyGroups = merge( this.emptyGroups, currAnalyzeResult.emptyGroups ) this.hasCustom = currAnalyzeResult.hasCustom || this.hasCustom this.canModeBeOptimized[currModName] = currAnalyzeResult.canBeOptimized } }) } ) this.defaultMode = actualDefinition.defaultMode if ( !isEmpty(this.lexerDefinitionErrors) && !this.config.deferDefinitionErrorsHandling ) { let allErrMessages = map(this.lexerDefinitionErrors, (error) => { return error.message }) let allErrMessagesString = allErrMessages.join( "-----------------------\n" ) throw new Error( "Errors detected in definition of Lexer:\n" + allErrMessagesString ) } // Only print warning if there are no errors, This will avoid pl forEach(this.lexerDefinitionWarning, (warningDescriptor) => { PRINT_WARNING(warningDescriptor.message) }) this.TRACE_INIT("Choosing sub-methods implementations", () => { // Choose the relevant internal implementations for this specific parser. // These implementations should be in-lined by the JavaScript engine // to provide optimal performance in each scenario. if (SUPPORT_STICKY) { this.chopInput = <any>IDENTITY this.match = this.matchWithTest } else { this.updateLastIndex = NOOP this.match = this.matchWithExec } if (hasOnlySingleMode) { this.handleModes = NOOP } if (this.trackStartLines === false) { this.computeNewColumn = IDENTITY } if (this.trackEndLines === false) { this.updateTokenEndLineColumnLocation = NOOP } if (/full/i.test(this.config.positionTracking)) { this.createTokenInstance = this.createFullToken } else if (/onlyStart/i.test(this.config.positionTracking)) { this.createTokenInstance = this.createStartOnlyToken } else if (/onlyOffset/i.test(this.config.positionTracking)) { this.createTokenInstance = this.createOffsetOnlyToken } else { throw Error( `Invalid <positionTracking> config option: "${this.config.positionTracking}"` ) } if (this.hasCustom) { this.addToken = this.addTokenUsingPush this.handlePayload = this.handlePayloadWithCustom } else { this.addToken = this.addTokenUsingMemberAccess this.handlePayload = this.handlePayloadNoCustom } }) this.TRACE_INIT("Failed Optimization Warnings", () => { const unOptimizedModes = reduce( this.canModeBeOptimized, (cannotBeOptimized, canBeOptimized, modeName) => { if (canBeOptimized === false) { cannotBeOptimized.push(modeName) } return cannotBeOptimized }, [] ) if (config.ensureOptimizations && !isEmpty(unOptimizedModes)) { throw Error( `Lexer Modes: < ${unOptimizedModes.join( ", " )} > cannot be optimized.\n` + '\t Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode.\n' + "\t Or inspect the console log for details on how to resolve these issues." ) } }) this.TRACE_INIT("clearRegExpParserCache", () => { clearRegExpParserCache() }) this.TRACE_INIT("toFastProperties", () => { toFastProperties(this) }) }) } public tokenize( text: string, initialMode: string = this.defaultMode ): ILexingResult { if (!isEmpty(this.lexerDefinitionErrors)) { let allErrMessages = map(this.lexerDefinitionErrors, (error) => { return error.message }) let allErrMessagesString = allErrMessages.join( "-----------------------\n" ) throw new Error( "Unable to Tokenize because Errors detected in definition of Lexer:\n" + allErrMessagesString ) } let lexResult = this.tokenizeInternal(text, initialMode) return lexResult } // There is quite a bit of duplication between this and "tokenizeInternalLazy" // This is intentional due to performance considerations. private tokenizeInternal(text: string, initialMode: string): ILexingResult { let i, j, matchAltImage, longerAltIdx, matchedImage, payload, altPayload, imageLength, group, tokType, newToken, errLength, droppedChar, msg, match let orgText = text let orgLength = orgText.length let offset = 0 let matchedTokensIndex = 0 // initializing the tokensArray to the "guessed" size. // guessing too little will still reduce the number of array re-sizes on pushes. // guessing too large (Tested by guessing x4 too large) may cost a bit more of memory // but would still have a faster runtime by avoiding (All but one) array resizing. let guessedNumberOfTokens = this.hasCustom ? 0 // will break custom token pattern APIs the matchedTokens array will contain undefined elements. : Math.floor(text.length / 10) let matchedTokens = new Array(guessedNumberOfTokens) let errors: ILexingError[] = [] let line = this.trackStartLines ? 1 : undefined let column = this.trackStartLines ? 1 : undefined let groups: any = cloneEmptyGroups(this.emptyGroups) let trackLines = this.trackStartLines const lineTerminatorPattern = this.config.lineTerminatorsPattern let currModePatternsLength = 0 let patternIdxToConfig = [] let currCharCodeToPatternIdxToConfig = [] let modeStack = [] const emptyArray = [] Object.freeze(emptyArray) let getPossiblePatterns = undefined function getPossiblePatternsSlow() { return patternIdxToConfig } function getPossiblePatternsOptimized(charCode) { const optimizedCharIdx = charCodeToOptimizedIndex(charCode) const possiblePatterns = currCharCodeToPatternIdxToConfig[optimizedCharIdx] if (possiblePatterns === undefined) { return emptyArray } else { return possiblePatterns } } let pop_mode = (popToken) => { // TODO: perhaps avoid this error in the edge case there is no more input? if ( modeStack.length === 1 && // if we have both a POP_MODE and a PUSH_MODE this is in-fact a "transition" // So no error should occur. popToken.tokenType.PUSH_MODE === undefined ) { // if we try to pop the last mode there lexer will no longer have ANY mode. // thus the pop is ignored, an error will be created and the lexer will continue parsing in the previous mode. let msg = this.config.errorMessageProvider.buildUnableToPopLexerModeMessage( popToken ) errors.push({ offset: popToken.startOffset, line: popToken.startLine !== undefined ? popToken.startLine : undefined, column: popToken.startColumn !== undefined ? popToken.startColumn : undefined, length: popToken.image.length, message: msg }) } else { modeStack.pop() let newMode = last(modeStack) patternIdxToConfig = this.patternIdxToConfig[newMode] currCharCodeToPatternIdxToConfig = this.charCodeToPatternIdxToConfig[ newMode ] currModePatternsLength = patternIdxToConfig.length const modeCanBeOptimized = this.canModeBeOptimized[newMode] && this.config.safeMode === false if (currCharCodeToPatternIdxToConfig && modeCanBeOptimized) { getPossiblePatterns = getPossiblePatternsOptimized } else { getPossiblePatterns = getPossiblePatternsSlow } } } function push_mode(newMode) { modeStack.push(newMode) currCharCodeToPatternIdxToConfig = this.charCodeToPatternIdxToConfig[ newMode ] patternIdxToConfig = this.patternIdxToConfig[newMode] currModePatternsLength = patternIdxToConfig.length currModePatternsLength = patternIdxToConfig.length const modeCanBeOptimized = this.canModeBeOptimized[newMode] && this.config.safeMode === false if (currCharCodeToPatternIdxToConfig && modeCanBeOptimized) { getPossiblePatterns = getPossiblePatternsOptimized } else { getPossiblePatterns = getPossiblePatternsSlow } } // this pattern seems to avoid a V8 de-optimization, although that de-optimization does not // seem to matter performance wise. push_mode.call(this, initialMode) let currConfig while (offset < orgLength) { matchedImage = null let nextCharCode = orgText.charCodeAt(offset) const chosenPatternIdxToConfig = getPossiblePatterns(nextCharCode) let chosenPatternsLength = chosenPatternIdxToConfig.length for (i = 0; i < chosenPatternsLength; i++) { currConfig = chosenPatternIdxToConfig[i] let currPattern = currConfig.pattern payload = null // manually in-lined because > 600 chars won't be in-lined in V8 let singleCharCode = currConfig.short if (singleCharCode !== false) { if (nextCharCode === singleCharCode) { // single character string matchedImage = currPattern } } else if (currConfig.isCustom === true) { match = currPattern.exec(orgText, offset, matchedTokens, groups) if (match !== null) { matchedImage = match[0] if (match.payload !== undefined) { payload = match.payload } } else { matchedImage = null } } else { this.updateLastIndex(currPattern, offset) matchedImage = this.match(currPattern, text, offset) } if (matchedImage !== null) { // even though this pattern matched we must try a another longer alternative. // this can be used to prioritize keywords over identifiers longerAltIdx = currConfig.longerAlt if (longerAltIdx !== undefined) { // TODO: micro optimize, avoid extra prop access // by saving/linking longerAlt on the original config? let longerAltConfig = patternIdxToConfig[longerAltIdx] let longerAltPattern = longerAltConfig.pattern altPayload = null // single Char can never be a longer alt so no need to test it. // manually in-lined because > 600 chars won't be in-lined in V8 if (longerAltConfig.isCustom === true) { match = longerAltPattern.exec( orgText, offset, matchedTokens, groups ) if (match !== null) { matchAltImage = match[0] if (match.payload !== undefined) { altPayload = match.payload } } else { matchAltImage = null } } else { this.updateLastIndex(longerAltPattern, offset) matchAltImage = this.match(longerAltPattern, text, offset) } if (matchAltImage && matchAltImage.length > matchedImage.length) { matchedImage = matchAltImage payload = altPayload currConfig = longerAltConfig } } break } } // successful match if (matchedImage !== null) { imageLength = matchedImage.length group = currConfig.group if (group !== undefined) { tokType = currConfig.tokenTypeIdx // TODO: "offset + imageLength" and the new column may be computed twice in case of "full" location information inside // createFullToken method newToken = this.createTokenInstance( matchedImage, offset, tokType, currConfig.tokenType, line, column, imageLength ) this.handlePayload(newToken, payload) // TODO: optimize NOOP in case there are no special groups? if (group === false) { matchedTokensIndex = this.addToken( matchedTokens, matchedTokensIndex, newToken ) } else { groups[group].push(newToken) } } text = this.chopInput(text, imageLength) offset = offset + imageLength // TODO: with newlines the column may be assigned twice column = this.computeNewColumn(column, imageLength) if (trackLines === true && currConfig.canLineTerminator === true) { let numOfLTsInMatch = 0 let foundTerminator let lastLTEndOffset lineTerminatorPattern.lastIndex = 0 do { foundTerminator = lineTerminatorPattern.test(matchedImage) if (foundTerminator === true) { lastLTEndOffset = lineTerminatorPattern.lastIndex - 1 numOfLTsInMatch++ } } while (foundTerminator === true) if (numOfLTsInMatch !== 0) { line = line + numOfLTsInMatch column = imageLength - lastLTEndOffset this.updateTokenEndLineColumnLocation( newToken, group, lastLTEndOffset, numOfLTsInMatch, line, column, imageLength ) } } // will be NOOP if no modes present this.handleModes(currConfig, pop_mode, push_mode, newToken) } else { // error recovery, drop characters until we identify a valid token's start point let errorStartOffset = offset let errorLine = line let errorColumn = column let foundResyncPoint = false while (!foundResyncPoint && offset < orgLength) { // drop chars until we succeed in matching something droppedChar = orgText.charCodeAt(offset) // Identity Func (when sticky flag is enabled) text = this.chopInput(text, 1) offset++ for (j = 0; j < currModePatternsLength; j++) { let currConfig = patternIdxToConfig[j] let currPattern = currConfig.pattern // manually in-lined because > 600 chars won't be in-lined in V8 let singleCharCode = currConfig.short if (singleCharCode !== false) { if (orgText.charCodeAt(offset) === singleCharCode) { // single character string foundResyncPoint = true } } else if (currConfig.isCustom === true) { foundResyncPoint = currPattern.exec(orgText, offset, matchedTokens, groups) !== null } else { this.updateLastIndex(currPattern, offset) foundResyncPoint = currPattern.exec(text) !== null } if (foundResyncPoint === true) { break } } } errLength = offset - errorStartOffset // at this point we either re-synced or reached the end of the input text msg = this.config.errorMessageProvider.buildUnexpectedCharactersMessage( orgText, errorStartOffset, errLength, errorLine, errorColumn ) errors.push({ offset: errorStartOffset, line: errorLine, column: errorColumn, length: errLength, message: msg }) } } // if we do have custom patterns which push directly into the // TODO: custom tokens should not push directly?? if (!this.hasCustom) { // if we guessed a too large size for the tokens array this will shrink it to the right size. matchedTokens.length = matchedTokensIndex } return { tokens: matchedTokens, groups: groups, errors: errors } } private handleModes(config, pop_mode, push_mode, newToken) { if (config.pop === true) { // need to save the PUSH_MODE property as if the mode is popped // patternIdxToPopMode is updated to reflect the new mode after popping the stack let pushMode = config.push pop_mode(newToken) if (pushMode !== undefined) { push_mode.call(this, pushMode) } } else if (config.push !== undefined) { push_mode.call(this, config.push) } } private chopInput(text, length): string { return text.substring(length) } private updateLastIndex(regExp, newLastIndex): void { regExp.lastIndex = newLastIndex } // TODO: decrease this under 600 characters? inspect stripping comments option in TSC compiler private updateTokenEndLineColumnLocation( newToken, group, lastLTIdx, numOfLTsInMatch, line, column, imageLength ): void { let lastCharIsLT, fixForEndingInLT if (group !== undefined) { // a none skipped multi line Token, need to update endLine/endColumn lastCharIsLT = lastLTIdx === imageLength - 1 fixForEndingInLT = lastCharIsLT ? -1 : 0 if (!(numOfLTsInMatch === 1 && lastCharIsLT === true)) { // if a token ends in a LT that last LT only affects the line numbering of following Tokens newToken.endLine = line + fixForEndingInLT // the last LT in a token does not affect the endColumn either as the [columnStart ... columnEnd) // inclusive to exclusive range. newToken.endColumn = column - 1 + -fixForEndingInLT } // else single LT in the last character of a token, no need to modify the endLine/EndColumn } } private computeNewColumn(oldColumn, imageLength) { return oldColumn + imageLength } // Place holder, will be replaced by the correct variant according to the locationTracking option at runtime. /* istanbul ignore next - place holder */ private createTokenInstance(...args: any[]): IToken { return null } private createOffsetOnlyToken(image, startOffset, tokenTypeIdx, tokenType) { return { image, startOffset, tokenTypeIdx, tokenType } } private createStartOnlyToken( image, startOffset, tokenTypeIdx, tokenType, startLine, startColumn ) { return { image, startOffset, startLine, startColumn, tokenTypeIdx, tokenType } } private createFullToken( image, startOffset, tokenTypeIdx, tokenType, startLine, startColumn, imageLength ) { return { image, startOffset, endOffset: startOffset + imageLength - 1, startLine, endLine: startLine, startColumn, endColumn: startColumn + imageLength - 1, tokenTypeIdx, tokenType } } // Place holder, will be replaced by the correct variant according to the locationTracking option at runtime. /* istanbul ignore next - place holder */ private addToken(tokenVector, index, tokenToAdd): number { return 666 } private addTokenUsingPush(tokenVector, index, tokenToAdd): number { tokenVector.push(tokenToAdd) return index } private addTokenUsingMemberAccess(tokenVector, index, tokenToAdd): number { tokenVector[index] = tokenToAdd index++ return index } // Place holder, will be replaced by the correct variant according to the hasCustom flag option at runtime. /* istanbul ignore next - place holder */ private handlePayload(token: IToken, payload: any): void {} private handlePayloadNoCustom(token: IToken, payload: any): void {} private handlePayloadWithCustom(token: IToken, payload: any): void { if (payload !== null) { token.payload = payload } } /* istanbul ignore next - place holder to be replaced with chosen alternative at runtime */ private match(pattern: RegExp, text: string, offset?: number): string { return null } private matchWithTest(pattern: RegExp, text: string, offset: number): string { let found = pattern.test(text) if (found === true) { return text.substring(offset, pattern.lastIndex) } return null } private matchWithExec(pattern, text): string { let regExpArray = pattern.exec(text) return regExpArray !== null ? regExpArray[0] : regExpArray } // Duplicated from the parser's perf trace trait to allow future extraction // of the lexer to a separate package. TRACE_INIT<T>(phaseDesc: string, phaseImpl: () => T): T { // No need to optimize this using NOOP pattern because // It is not called in a hot spot... if (this.traceInitPerf === true) { this.traceInitIndent++ const indent = new Array(this.traceInitIndent + 1).join("\t") if (this.traceInitIndent < this.traceInitMaxIdent) { console.log(`${indent}--> <${phaseDesc}>`) } const { time, value } = timer(phaseImpl) /* istanbul ignore next - Difficult to reproduce specific performance behavior (>10ms) in tests */ const traceMethod = time > 10 ? console.warn : console.log if (this.traceInitIndent < this.traceInitMaxIdent) { traceMethod(`${indent}<-- <${phaseDesc}> time: ${time}ms`) } this.traceInitIndent-- return value } else { return phaseImpl() } } }