UNPKG

chevrotain

Version:

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

453 lines (411 loc) 14.1 kB
import { AtLeastOneSepMethodOpts, ConsumeMethodOpts, CstNode, DSLMethodOpts, DSLMethodOptsWithErr, GrammarAction, IOrAlt, IParserConfig, IProduction, IToken, ManySepMethodOpts, OrMethodOpts, SubruleMethodOpts, TokenType, } from "@chevrotain/types"; import { MixedInParser } from "./parser_traits.js"; import { Alternation, Alternative, NonTerminal, Option, Repetition, RepetitionMandatory, RepetitionMandatoryWithSeparator, RepetitionWithSeparator, Rule, Terminal, } from "@chevrotain/gast"; import { Lexer } from "../../../scan/lexer_public.js"; import { augmentTokenTypes, hasShortKeyProperty, } from "../../../scan/tokens.js"; import { createToken, createTokenInstance, } from "../../../scan/tokens_public.js"; import { END_OF_FILE } from "../parser.js"; import { BITS_FOR_OCCURRENCE_IDX } from "../../grammar/keys.js"; import { ParserMethodInternal } from "../types.js"; type ProdWithDef = IProduction & { definition?: IProduction[] }; const RECORDING_NULL_OBJECT = { description: "This Object indicates the Parser is during Recording Phase", }; Object.freeze(RECORDING_NULL_OBJECT); const HANDLE_SEPARATOR = true; const MAX_METHOD_IDX = Math.pow(2, BITS_FOR_OCCURRENCE_IDX) - 1; const RFT = createToken({ name: "RECORDING_PHASE_TOKEN", pattern: Lexer.NA }); augmentTokenTypes([RFT]); const RECORDING_PHASE_TOKEN = createTokenInstance( RFT, "This IToken indicates the Parser is in Recording Phase\n\t" + "" + "See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details", // Using "-1" instead of NaN (as in EOF) because an actual number is less likely to // cause errors if the output of LA or CONSUME would be (incorrectly) used during the recording phase. -1, -1, -1, -1, -1, -1, ); Object.freeze(RECORDING_PHASE_TOKEN); const RECORDING_PHASE_CSTNODE: CstNode = { name: "This CSTNode indicates the Parser is in Recording Phase\n\t" + "See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details", children: {}, }; /** * This trait handles the creation of the GAST structure for Chevrotain Grammars */ export class GastRecorder { recordingProdStack: ProdWithDef[]; RECORDING_PHASE: boolean; initGastRecorder(this: MixedInParser, config: IParserConfig): void { this.recordingProdStack = []; this.RECORDING_PHASE = false; } enableRecording(this: MixedInParser): void { this.RECORDING_PHASE = true; this.TRACE_INIT("Enable Recording", () => { /** * Warning Dark Voodoo Magic upcoming! * We are "replacing" the public parsing DSL methods API * With **new** alternative implementations on the Parser **instance** * * So far this is the only way I've found to avoid performance regressions during parsing time. * - Approx 30% performance regression was measured on Chrome 75 Canary when attempting to replace the "internal" * implementations directly instead. */ for (let i = 0; i < 10; i++) { const idx = i > 0 ? i : ""; this[`CONSUME${idx}` as "CONSUME"] = function (arg1, arg2) { return this.consumeInternalRecord(arg1, i, arg2); }; this[`SUBRULE${idx}` as "SUBRULE"] = function (arg1, arg2) { return this.subruleInternalRecord(arg1, i, arg2) as any; }; this[`OPTION${idx}` as "OPTION"] = function (arg1) { return this.optionInternalRecord(arg1, i); }; this[`OR${idx}` as "OR"] = function (arg1) { return this.orInternalRecord(arg1, i); }; this[`MANY${idx}` as "MANY"] = function (arg1) { this.manyInternalRecord(i, arg1); }; this[`MANY_SEP${idx}` as "MANY_SEP"] = function (arg1) { this.manySepFirstInternalRecord(i, arg1); }; this[`AT_LEAST_ONE${idx}` as "AT_LEAST_ONE"] = function (arg1) { this.atLeastOneInternalRecord(i, arg1); }; this[`AT_LEAST_ONE_SEP${idx}` as "AT_LEAST_ONE_SEP"] = function (arg1) { this.atLeastOneSepFirstInternalRecord(i, arg1); }; } // DSL methods with the idx(suffix) as an argument this[`consume`] = function (idx, arg1, arg2) { return this.consumeInternalRecord(arg1, idx, arg2); }; this[`subrule`] = function (idx, arg1, arg2) { return this.subruleInternalRecord(arg1, idx, arg2) as any; }; this[`option`] = function (idx, arg1) { return this.optionInternalRecord(arg1, idx); }; this[`or`] = function (idx, arg1) { return this.orInternalRecord(arg1, idx); }; this[`many`] = function (idx, arg1) { this.manyInternalRecord(idx, arg1); }; this[`atLeastOne`] = function (idx, arg1) { this.atLeastOneInternalRecord(idx, arg1); }; this.ACTION = this.ACTION_RECORD; this.BACKTRACK = this.BACKTRACK_RECORD; this.LA = this.LA_RECORD; }); } disableRecording(this: MixedInParser) { this.RECORDING_PHASE = false; // By deleting these **instance** properties, any future invocation // will be deferred to the original methods on the **prototype** object // This seems to get rid of any incorrect optimizations that V8 may // do during the recording phase. this.TRACE_INIT("Deleting Recording methods", () => { const that: any = this; for (let i = 0; i < 10; i++) { const idx = i > 0 ? i : ""; delete that[`CONSUME${idx}`]; delete that[`SUBRULE${idx}`]; delete that[`OPTION${idx}`]; delete that[`OR${idx}`]; delete that[`MANY${idx}`]; delete that[`MANY_SEP${idx}`]; delete that[`AT_LEAST_ONE${idx}`]; delete that[`AT_LEAST_ONE_SEP${idx}`]; } delete that[`consume`]; delete that[`subrule`]; delete that[`option`]; delete that[`or`]; delete that[`many`]; delete that[`atLeastOne`]; delete that.ACTION; delete that.BACKTRACK; delete that.LA; }); } // Parser methods are called inside an ACTION? // Maybe try/catch/finally on ACTIONS while disabling the recorders state changes? // @ts-expect-error -- noop place holder ACTION_RECORD<T>(this: MixedInParser, impl: () => T): T { // NO-OP during recording } // Executing backtracking logic will break our recording logic assumptions BACKTRACK_RECORD<T>( grammarRule: (...args: any[]) => T, args?: any[], ): () => boolean { return () => true; } // LA is part of the official API and may be used for custom lookahead logic // by end users who may forget to wrap it in ACTION or inside a GATE LA_RECORD(howMuch: number): IToken { // We cannot use the RECORD_PHASE_TOKEN here because someone may depend // On LA return EOF at the end of the input so an infinite loop may occur. return END_OF_FILE; } topLevelRuleRecord(name: string, def: Function): Rule { try { const newTopLevelRule = new Rule({ definition: [], name: name }); newTopLevelRule.name = name; this.recordingProdStack.push(newTopLevelRule); def.call(this); this.recordingProdStack.pop(); return newTopLevelRule; } catch (originalError) { if (originalError.KNOWN_RECORDER_ERROR !== true) { try { originalError.message = originalError.message + '\n\t This error was thrown during the "grammar recording phase" For more info see:\n\t' + "https://chevrotain.io/docs/guide/internals.html#grammar-recording"; } catch (mutabilityError) { // We may not be able to modify the original error object throw originalError; } } throw originalError; } } // Implementation of parsing DSL optionInternalRecord<OUT>( this: MixedInParser, actionORMethodDef: GrammarAction<OUT> | DSLMethodOpts<OUT>, occurrence: number, ): OUT { return recordProd.call(this, Option, actionORMethodDef, occurrence); } atLeastOneInternalRecord<OUT>( this: MixedInParser, occurrence: number, actionORMethodDef: GrammarAction<OUT> | DSLMethodOptsWithErr<OUT>, ): void { recordProd.call(this, RepetitionMandatory, actionORMethodDef, occurrence); } atLeastOneSepFirstInternalRecord<OUT>( this: MixedInParser, occurrence: number, options: AtLeastOneSepMethodOpts<OUT>, ): void { recordProd.call( this, RepetitionMandatoryWithSeparator, options, occurrence, HANDLE_SEPARATOR, ); } manyInternalRecord<OUT>( this: MixedInParser, occurrence: number, actionORMethodDef: GrammarAction<OUT> | DSLMethodOpts<OUT>, ): void { recordProd.call(this, Repetition, actionORMethodDef, occurrence); } manySepFirstInternalRecord<OUT>( this: MixedInParser, occurrence: number, options: ManySepMethodOpts<OUT>, ): void { recordProd.call( this, RepetitionWithSeparator, options, occurrence, HANDLE_SEPARATOR, ); } orInternalRecord<T>( this: MixedInParser, altsOrOpts: IOrAlt<any>[] | OrMethodOpts<unknown>, occurrence: number, ): T { return recordOrProd.call(this, altsOrOpts, occurrence); } subruleInternalRecord<ARGS extends unknown[], R>( this: MixedInParser, ruleToCall: ParserMethodInternal<ARGS, R>, occurrence: number, options?: SubruleMethodOpts<ARGS>, ): R | CstNode { assertMethodIdxIsValid(occurrence); if (!ruleToCall || !Object.hasOwn(ruleToCall, "ruleName")) { const error: any = new Error( `<SUBRULE${getIdxSuffix(occurrence)}> argument is invalid` + ` expecting a Parser method reference but got: <${JSON.stringify( ruleToCall, )}>` + `\n inside top level rule: <${ (<Rule>this.recordingProdStack[0]).name }>`, ); error.KNOWN_RECORDER_ERROR = true; throw error; } const prevProd: any = this.recordingProdStack.at(-1); const ruleName = ruleToCall.ruleName; const newNoneTerminal = new NonTerminal({ idx: occurrence, nonTerminalName: ruleName, label: options?.LABEL, // The resolving of the `referencedRule` property will be done once all the Rule's GASTs have been created referencedRule: undefined, }); prevProd.definition.push(newNoneTerminal); return this.outputCst ? RECORDING_PHASE_CSTNODE : <any>RECORDING_NULL_OBJECT; } consumeInternalRecord( this: MixedInParser, tokType: TokenType, occurrence: number, options?: ConsumeMethodOpts, ): IToken { assertMethodIdxIsValid(occurrence); if (!hasShortKeyProperty(tokType)) { const error: any = new Error( `<CONSUME${getIdxSuffix(occurrence)}> argument is invalid` + ` expecting a TokenType reference but got: <${JSON.stringify( tokType, )}>` + `\n inside top level rule: <${ (<Rule>this.recordingProdStack[0]).name }>`, ); error.KNOWN_RECORDER_ERROR = true; throw error; } const prevProd: any = this.recordingProdStack.at(-1); const newNoneTerminal = new Terminal({ idx: occurrence, terminalType: tokType, label: options?.LABEL, }); prevProd.definition.push(newNoneTerminal); return RECORDING_PHASE_TOKEN; } } function recordProd( prodConstructor: any, mainProdArg: any, occurrence: number, handleSep: boolean = false, ): any { assertMethodIdxIsValid(occurrence); const prevProd: any = this.recordingProdStack.at(-1); const grammarAction = typeof mainProdArg === "function" ? mainProdArg : mainProdArg.DEF; const newProd = new prodConstructor({ definition: [], idx: occurrence }); if (handleSep) { newProd.separator = mainProdArg.SEP; } if (Object.hasOwn(mainProdArg, "MAX_LOOKAHEAD")) { newProd.maxLookahead = mainProdArg.MAX_LOOKAHEAD; } this.recordingProdStack.push(newProd); grammarAction.call(this); prevProd.definition.push(newProd); this.recordingProdStack.pop(); return RECORDING_NULL_OBJECT; } function recordOrProd(mainProdArg: any, occurrence: number): any { assertMethodIdxIsValid(occurrence); const prevProd: any = this.recordingProdStack.at(-1); // Only an array of alternatives const hasOptions = Array.isArray(mainProdArg) === false; const alts: IOrAlt<unknown>[] = hasOptions === false ? mainProdArg : mainProdArg.DEF; const newOrProd = new Alternation({ definition: [], idx: occurrence, ignoreAmbiguities: hasOptions && mainProdArg.IGNORE_AMBIGUITIES === true, }); if (Object.hasOwn(mainProdArg, "MAX_LOOKAHEAD")) { newOrProd.maxLookahead = mainProdArg.MAX_LOOKAHEAD; } const hasPredicates = alts.some( (currAlt: any) => typeof currAlt.GATE === "function", ); newOrProd.hasPredicates = hasPredicates; prevProd.definition.push(newOrProd); alts.forEach((currAlt) => { const currAltFlat = new Alternative({ definition: [] }); newOrProd.definition.push(currAltFlat); if (Object.hasOwn(currAlt, "IGNORE_AMBIGUITIES")) { currAltFlat.ignoreAmbiguities = currAlt.IGNORE_AMBIGUITIES as boolean; // assumes end user provides the correct config value/type } // **implicit** ignoreAmbiguities due to usage of gate else if (Object.hasOwn(currAlt, "GATE")) { currAltFlat.ignoreAmbiguities = true; } this.recordingProdStack.push(currAltFlat); currAlt.ALT.call(this); this.recordingProdStack.pop(); }); return RECORDING_NULL_OBJECT; } function getIdxSuffix(idx: number): string { return idx === 0 ? "" : `${idx}`; } function assertMethodIdxIsValid(idx: number): void { if (idx < 0 || idx > MAX_METHOD_IDX) { const error: any = new Error( // The stack trace will contain all the needed details `Invalid DSL Method idx value: <${idx}>\n\t` + `Idx value must be a none negative value smaller than ${ MAX_METHOD_IDX + 1 }`, ); error.KNOWN_RECORDER_ERROR = true; throw error; } }