First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+800
View File
@@ -0,0 +1,800 @@
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import {
IToken,
TokenType,
tokenMatcher,
tokenLabel,
Rule,
IProductionWithOccurrence,
NonTerminal,
Alternation,
Option,
RepetitionMandatory,
RepetitionMandatoryWithSeparator,
RepetitionWithSeparator,
Repetition,
Terminal,
BaseParser,
LLkLookaheadStrategy,
ILookaheadValidationError,
IOrAlt,
getLookaheadPaths,
OptionalProductionType,
EOF
} from "chevrotain";
import {
ATN,
ATNState,
ATN_RULE_STOP,
AtomTransition,
buildATNKey,
createATN,
DecisionState,
EpsilonTransition,
RuleTransition,
Transition
} from "./atn.js";
import {
ATNConfig,
ATNConfigSet,
DFA,
DFAState,
DFA_ERROR,
getATNConfigKey
} from "./dfa.js";
import min from "lodash-es/min.js";
import flatMap from "lodash-es/flatMap.js";
import uniqBy from "lodash-es/uniqBy.js";
import map from "lodash-es/map.js";
import flatten from "lodash-es/flatten.js";
import forEach from "lodash-es/forEach.js";
import isEmpty from "lodash-es/isEmpty.js";
import reduce from "lodash-es/reduce.js";
type DFACache = (predicateSet: PredicateSet) => DFA
export type AmbiguityReport = (message: string) => void;
function createDFACache(startState: DecisionState, decision: number): DFACache {
const map: Record<string, DFA | undefined> = {}
return (predicateSet) => {
const key = predicateSet.toString()
let existing = map[key]
if (existing !== undefined) {
return existing
} else {
existing = {
atnStartState: startState,
decision,
states: {}
}
map[key] = existing
return existing
}
}
}
class PredicateSet {
private predicates: boolean[] = []
is(index: number): boolean {
return index >= this.predicates.length || this.predicates[index]
}
set(index: number, value: boolean) {
this.predicates[index] = value
}
toString(): string {
let value = ""
const size = this.predicates.length
for (let i = 0; i < size; i++) {
value += this.predicates[i] === true ? "1" : "0"
}
return value
}
}
interface AdaptivePredictError {
tokenPath: IToken[]
possibleTokenTypes: TokenType[]
actualToken: IToken
}
const EMPTY_PREDICATES = new PredicateSet()
export interface LLStarLookaheadOptions {
logging?: AmbiguityReport
// The incomplete flag is used when the input is very likely to be incomplete (e.g. during code completion)
// In this case, the lookahead strategy will try to make educated guesses on which alternatives are more likely to be correct, even if they don't fully match the input
incomplete?: boolean
}
export class LLStarLookaheadStrategy extends LLkLookaheadStrategy {
private atn: ATN;
private dfas: DFACache[];
private logging: AmbiguityReport;
private incomplete: boolean;
constructor(options?: LLStarLookaheadOptions) {
super();
this.logging = options?.logging ?? ((message) => console.log(message));
this.incomplete = options?.incomplete ?? false;
}
override initialize(options: { rules: Rule[] }): void {
this.atn = createATN(options.rules);
this.dfas = initATNSimulator(this.atn);
}
override validateAmbiguousAlternationAlternatives(): ILookaheadValidationError[] {
return [];
}
override validateEmptyOrAlternatives(): ILookaheadValidationError[] {
return [];
}
override buildLookaheadForAlternation(options: {
prodOccurrence: number;
rule: Rule;
maxLookahead: number;
hasPredicates: boolean;
dynamicTokensEnabled: boolean
}): (this: BaseParser, orAlts?: IOrAlt<any>[] | undefined) => number | undefined {
const { prodOccurrence, rule, hasPredicates, dynamicTokensEnabled } = options;
const dfas = this.dfas;
const logging = this.logging;
const incomplete = this.incomplete;
const key = buildATNKey(rule, 'Alternation', prodOccurrence);
const decisionState = this.atn.decisionMap[key];
const decisionIndex = decisionState.decision;
const partialAlts: (TokenType | undefined)[][] = map(
getLookaheadPaths({
maxLookahead: 1,
occurrence: prodOccurrence,
prodType: "Alternation",
rule: rule
}),
(currAlt) => map(currAlt, (path) => path[0])
)
if (isLL1Sequence(partialAlts, false) && !dynamicTokensEnabled) {
const choiceToAlt = reduce(
partialAlts,
(result, currAlt, idx) => {
forEach(currAlt, (currTokType) => {
if (currTokType) {
result[currTokType.tokenTypeIdx!] = idx
forEach(currTokType.categoryMatches!, (currExtendingType) => {
result[currExtendingType] = idx
})
}
})
return result
},
{} as Record<number, number>
)
if (hasPredicates) {
return function (this: BaseParser, orAlts) {
const nextToken = this.LA_FAST(1)
const prediction: number | undefined = choiceToAlt[nextToken.tokenTypeIdx]
if (orAlts !== undefined && prediction !== undefined) {
const gate = orAlts[prediction]?.GATE
if (gate !== undefined && gate.call(this) === false) {
return undefined;
}
}
return prediction
}
} else {
return function (this: BaseParser): number | undefined {
const nextToken = this.LA_FAST(1)
return choiceToAlt[nextToken.tokenTypeIdx];
}
}
} else if (hasPredicates) {
return function (this: BaseParser, orAlts) {
const predicates = new PredicateSet()
const length = orAlts === undefined ? 0 : orAlts.length
for (let i = 0; i < length; i++) {
const gate = orAlts?.[i].GATE
predicates.set(i, gate === undefined || gate.call(this))
}
const result = adaptivePredict.call(this, dfas, decisionIndex, predicates, logging, incomplete);
return typeof result === 'number' ? result : undefined;
}
} else {
return function (this: BaseParser) {
const result = adaptivePredict.call(this, dfas, decisionIndex, EMPTY_PREDICATES, logging, incomplete);
return typeof result === 'number' ? result : undefined;
}
}
}
override buildLookaheadForOptional(options: {
prodOccurrence: number;
prodType: OptionalProductionType;
rule: Rule;
maxLookahead: number;
dynamicTokensEnabled: boolean
}): (this: BaseParser) => boolean {
const { prodOccurrence, rule, prodType, dynamicTokensEnabled } = options;
const dfas = this.dfas;
const logging = this.logging;
const incomplete = this.incomplete;
const key = buildATNKey(rule, prodType, prodOccurrence);
const decisionState = this.atn.decisionMap[key];
const decisionIndex = decisionState.decision;
const alts = map(
getLookaheadPaths({
maxLookahead: 1,
occurrence: prodOccurrence,
prodType,
rule
}),
(e) => {
return map(e, (g) => g[0])
}
)
if (isLL1Sequence(alts) && alts[0][0] && !dynamicTokensEnabled) {
const alt = alts[0]
const singleTokensTypes = flatten(alt)
if (
singleTokensTypes.length === 1 &&
isEmpty(singleTokensTypes[0].categoryMatches)
) {
const expectedTokenType = singleTokensTypes[0]
const expectedTokenUniqueKey = expectedTokenType.tokenTypeIdx
return function (this: BaseParser): boolean {
return this.LA_FAST(1).tokenTypeIdx === expectedTokenUniqueKey
}
} else {
const choiceToAlt = reduce(
singleTokensTypes,
(result, currTokType) => {
if (currTokType !== undefined) {
result[currTokType.tokenTypeIdx!] = true
forEach(currTokType.categoryMatches, (currExtendingType) => {
result[currExtendingType] = true
})
}
return result
},
{} as Record<number, boolean>
)
return function (this: BaseParser): boolean {
const nextToken = this.LA_FAST(1)
return choiceToAlt[nextToken.tokenTypeIdx] === true
}
}
}
return function (this: BaseParser) {
const result = adaptivePredict.call(this, dfas, decisionIndex, EMPTY_PREDICATES, logging, incomplete)
return typeof result === "object" ? false : result === 0;
}
}
}
function isLL1Sequence(sequences: (TokenType | undefined)[][], allowEmpty = true): boolean {
const fullSet = new Set<number>()
for (const alt of sequences) {
const altSet = new Set<number>()
for (const tokType of alt) {
if (tokType === undefined) {
if (allowEmpty) {
// Epsilon production encountered
break
} else {
return false;
}
}
const indices = [tokType.tokenTypeIdx!].concat(tokType.categoryMatches!)
for (const index of indices) {
if (fullSet.has(index)) {
if (!altSet.has(index)) {
return false
}
} else {
fullSet.add(index)
altSet.add(index)
}
}
}
}
return true
}
function initATNSimulator(atn: ATN): DFACache[] {
const decisionLength = atn.decisionStates.length
const decisionToDFA: DFACache[] = Array(decisionLength)
for (let i = 0; i < decisionLength; i++) {
decisionToDFA[i] = createDFACache(atn.decisionStates[i], i)
}
return decisionToDFA;
}
function adaptivePredict(
this: BaseParser,
dfaCaches: DFACache[],
decision: number,
predicateSet: PredicateSet,
logging: AmbiguityReport,
incomplete: boolean
): number | AdaptivePredictError {
const dfa = dfaCaches[decision](predicateSet)
let start = dfa.start
if (start === undefined) {
const closure = computeStartState(dfa.atnStartState as ATNState)
start = addDFAState(dfa, newDFAState(closure))
dfa.start = start
}
const alt = performLookahead.apply(this, [dfa, start, predicateSet, logging, incomplete])
return alt
}
function performLookahead(
this: BaseParser,
dfa: DFA,
s0: DFAState,
predicateSet: PredicateSet,
logging: AmbiguityReport,
incomplete: boolean,
): number | AdaptivePredictError {
let previousD = s0
let i = 1
const path: IToken[] = []
// Using LA_FAST is fine here
let t = this.LA_FAST(i++)
while (true) {
let d = getExistingTargetState(previousD, t)
if (d === undefined) {
d = computeLookaheadTarget.apply(this, [dfa, previousD, t, i, predicateSet, logging])
}
if (d === DFA_ERROR) {
return buildAdaptivePredictError(path, previousD, t)
}
if (d.isAcceptState === true) {
if (incomplete === true && tokenMatcher(t, EOF)) {
// We run into this case, when we reached the end of the input, but we are in the middle of evaluating a lookahead sequence
// The sequence is incomplete, but we can still make an educated guess on which alternative is more likely to be correct
const bestGuess = getBestGuess(previousD, predicateSet)
if (bestGuess !== undefined) {
return bestGuess
}
}
return d.prediction
}
previousD = d
path.push(t)
// We might be reading out of bounds
// Therefore, no LA_FAST here
t = this.LA(i++)
}
}
function computeLookaheadTarget(
this: BaseParser,
dfa: DFA,
previousD: DFAState,
token: IToken,
lookahead: number,
predicateSet: PredicateSet,
logging: AmbiguityReport
): DFAState {
const reach = computeReachSet(previousD.configs, token, predicateSet)
if (reach.size === 0) {
addDFAEdge(dfa, previousD, token, DFA_ERROR)
return DFA_ERROR
}
let newState = newDFAState(reach)
const predictedAlt = getUniqueAlt(reach, predicateSet)
if (predictedAlt !== undefined) {
newState.isAcceptState = true
newState.prediction = predictedAlt
newState.configs.uniqueAlt = predictedAlt
} else if (hasConflictTerminatingPrediction(reach)) {
const prediction = min(reach.alts)!
newState.isAcceptState = true
newState.prediction = prediction
newState.configs.uniqueAlt = prediction
reportLookaheadAmbiguity.apply(this, [dfa, lookahead, reach.alts, logging])
}
newState = addDFAEdge(dfa, previousD, token, newState)
return newState
}
function reportLookaheadAmbiguity(
this: BaseParser,
dfa: DFA,
lookahead: number,
ambiguityIndices: number[],
logging: AmbiguityReport
) {
const prefixPath: TokenType[] = []
for (let i = 1; i <= lookahead; i++) {
prefixPath.push(this.LA(i).tokenType)
}
const atnState = dfa.atnStartState
const topLevelRule = atnState.rule
const production = atnState.production
const message = buildAmbiguityError({
topLevelRule,
ambiguityIndices,
production,
prefixPath
})
logging(message)
}
function buildAmbiguityError(options: {
topLevelRule: Rule
prefixPath: TokenType[]
ambiguityIndices: number[]
production: IProductionWithOccurrence
}): string {
const pathMsg = map(options.prefixPath, (currtok) =>
tokenLabel(currtok)
).join(", ")
const occurrence =
options.production.idx === 0 ? "" : options.production.idx
let currMessage =
`Ambiguous Alternatives Detected: <${options.ambiguityIndices.join(
", "
)}> in <${getProductionDslName(options.production)}${occurrence}>` +
` inside <${options.topLevelRule.name}> Rule,\n` +
`<${pathMsg}> may appears as a prefix path in all these alternatives.\n`
currMessage =
currMessage +
`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES\n` +
`For Further details.`
return currMessage
}
function getProductionDslName(prod: IProductionWithOccurrence): string {
if (prod instanceof NonTerminal) {
return "SUBRULE"
} else if (prod instanceof Option) {
return "OPTION"
} else if (prod instanceof Alternation) {
return "OR"
} else if (prod instanceof RepetitionMandatory) {
return "AT_LEAST_ONE"
} else if (prod instanceof RepetitionMandatoryWithSeparator) {
return "AT_LEAST_ONE_SEP"
} else if (prod instanceof RepetitionWithSeparator) {
return "MANY_SEP"
} else if (prod instanceof Repetition) {
return "MANY"
} else if (prod instanceof Terminal) {
return "CONSUME"
} else {
throw Error("non exhaustive match")
}
}
function buildAdaptivePredictError(
path: IToken[],
previous: DFAState,
current: IToken
): AdaptivePredictError {
const nextTransitions = flatMap(
previous.configs.elements,
(e) => e.state.transitions
)
const nextTokenTypes = uniqBy(
nextTransitions
.filter((e): e is AtomTransition => e instanceof AtomTransition)
.map((e) => e.tokenType),
(e) => e.tokenTypeIdx
)
return {
actualToken: current,
possibleTokenTypes: nextTokenTypes,
tokenPath: path
}
}
function getExistingTargetState(
state: DFAState,
token: IToken
): DFAState | undefined {
return state.edges[token.tokenTypeIdx]
}
function computeReachSet(
configs: ATNConfigSet,
token: IToken,
predicateSet: PredicateSet
): ATNConfigSet {
const intermediate = new ATNConfigSet()
const skippedStopStates: ATNConfig[] = []
for (const c of configs.elements) {
if (predicateSet.is(c.alt) === false) {
continue
}
if (c.state.type === ATN_RULE_STOP) {
skippedStopStates.push(c)
continue
}
const transitionLength = c.state.transitions.length
for (let i = 0; i < transitionLength; i++) {
const transition = c.state.transitions[i]
const target = getReachableTarget(transition, token)
if (target !== undefined) {
intermediate.add({
state: target,
alt: c.alt,
stack: c.stack
})
}
}
}
let reach: ATNConfigSet | undefined
if (skippedStopStates.length === 0 && intermediate.size === 1) {
reach = intermediate
}
if (reach === undefined) {
reach = new ATNConfigSet()
for (const c of intermediate.elements) {
closure(c, reach)
}
}
if (skippedStopStates.length > 0 && !hasConfigInRuleStopState(reach)) {
for (const c of skippedStopStates) {
reach.add(c)
}
}
return reach
}
function getReachableTarget(
transition: Transition,
token: IToken
): ATNState | undefined {
if (
transition instanceof AtomTransition &&
tokenMatcher(token, transition.tokenType)
) {
return transition.target
}
return undefined
}
function getUniqueAlt(
configs: ATNConfigSet,
predicateSet: PredicateSet
): number | undefined {
let alt: number | undefined
for (const c of configs.elements) {
if (predicateSet.is(c.alt) === true) {
if (alt === undefined) {
alt = c.alt
} else if (alt !== c.alt) {
return undefined
}
}
}
return alt
}
function newDFAState(closure: ATNConfigSet): DFAState {
return {
configs: closure,
edges: {},
isAcceptState: false,
prediction: -1
}
}
function addDFAEdge(
dfa: DFA,
from: DFAState,
token: IToken,
to: DFAState
): DFAState {
to = addDFAState(dfa, to)
from.edges[token.tokenTypeIdx] = to
return to
}
function addDFAState(dfa: DFA, state: DFAState): DFAState {
if (state === DFA_ERROR) {
return state
}
// Repetitions have the same config set
// Therefore, storing the key of the config in a map allows us to create a loop in our DFA
const mapKey = state.configs.key
const existing = dfa.states[mapKey]
if (existing !== undefined) {
return existing
}
state.configs.finalize()
dfa.states[mapKey] = state
return state
}
function computeStartState(atnState: ATNState): ATNConfigSet {
const configs = new ATNConfigSet()
const numberOfTransitions = atnState.transitions.length
for (let i = 0; i < numberOfTransitions; i++) {
const target = atnState.transitions[i].target
const config: ATNConfig = {
state: target,
alt: i,
stack: []
}
closure(config, configs)
}
return configs
}
function closure(config: ATNConfig, configs: ATNConfigSet): void {
const p = config.state
if (p.type === ATN_RULE_STOP) {
if (config.stack.length > 0) {
const atnStack = [...config.stack]
const followState = atnStack.pop()!
const followConfig: ATNConfig = {
state: followState,
alt: config.alt,
stack: atnStack
}
closure(followConfig, configs)
} else {
// Dipping into outer context, simply add the config
// This will stop computation once every config is at the rule stop state
configs.add(config)
}
return
}
if (!p.epsilonOnlyTransitions) {
configs.add(config)
}
const transitionLength = p.transitions.length
for (let i = 0; i < transitionLength; i++) {
const transition = p.transitions[i]
const c = getEpsilonTarget(config, transition)
if (c !== undefined) {
closure(c, configs)
}
}
}
function getEpsilonTarget(
config: ATNConfig,
transition: Transition
): ATNConfig | undefined {
if (transition instanceof EpsilonTransition) {
return {
state: transition.target,
alt: config.alt,
stack: config.stack
}
} else if (transition instanceof RuleTransition) {
const stack = [...config.stack, transition.followState]
return {
state: transition.target,
alt: config.alt,
stack
}
}
return undefined
}
function hasConfigInRuleStopState(configs: ATNConfigSet): boolean {
for (const c of configs.elements) {
if (c.state.type === ATN_RULE_STOP) {
return true
}
}
return false
}
function allConfigsInRuleStopStates(configs: ATNConfigSet): boolean {
for (const c of configs.elements) {
if (c.state.type !== ATN_RULE_STOP) {
return false
}
}
return true
}
function hasConflictTerminatingPrediction(configs: ATNConfigSet): boolean {
if (allConfigsInRuleStopStates(configs)) {
return true
}
const altSets = getConflictingAltSets(configs.elements)
const heuristic =
hasConflictingAltSet(altSets) && !hasStateAssociatedWithOneAlt(altSets)
return heuristic
}
function getConflictingAltSets(
configs: readonly ATNConfig[]
): Map<string, Record<number, boolean>> {
const configToAlts = new Map<string, Record<number, boolean>>()
for (const c of configs) {
const key = getATNConfigKey(c, false)
let alts = configToAlts.get(key)
if (alts === undefined) {
alts = {}
configToAlts.set(key, alts)
}
alts[c.alt] = true
}
return configToAlts
}
function hasConflictingAltSet(
altSets: Map<string, Record<number, boolean>>
): boolean {
for (const value of Array.from(altSets.values())) {
if (Object.keys(value).length > 1) {
return true
}
}
return false
}
function hasStateAssociatedWithOneAlt(
altSets: Map<string, Record<number, boolean>>
): boolean {
for (const value of Array.from(altSets.values())) {
if (Object.keys(value).length === 1) {
return true
}
}
return false
}
function getBestGuess(state: DFAState, predicateSet: PredicateSet): number | undefined {
let bestAlt: number | undefined = undefined
for (const c of state.configs.elements) {
// Ignore invalid and stop states
if (predicateSet.is(c.alt) === false || c.state.type === ATN_RULE_STOP) {
continue
}
if (bestAlt === undefined) {
bestAlt = c.alt
} else if (bestAlt !== c.alt) {
return undefined;
}
}
return bestAlt;
}
+411
View File
@@ -0,0 +1,411 @@
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import { createToken, EmbeddedActionsParser, EOF, IToken, TokenType } from "chevrotain"
import { LLStarLookaheadStrategy } from "./all-star-lookahead"
import { describe, expect, it } from "vitest"
describe("ATN Simulator", () => {
describe("LL(*) lookahead", () => {
const A = createToken({ name: "A", pattern: "a" })
const B = createToken({ name: "B", pattern: "b" })
class UnboundedLookaheadParser extends EmbeddedActionsParser {
constructor() {
super([A, B], {
lookaheadStrategy: new LLStarLookaheadStrategy()
})
this.performSelfAnalysis()
}
LongRule = this.RULE("LongRule", () => {
return this.OR([
{
ALT: () => {
return 0
}
},
{
ALT: () => {
this.AT_LEAST_ONE1(() => this.CONSUME1(A))
return 1
}
},
{
ALT: () => {
this.AT_LEAST_ONE2(() => this.CONSUME2(A))
this.CONSUME(B)
return 2
}
}
])
})
}
it("Should pick longest alternative instead of first #1", () => {
const parser = new UnboundedLookaheadParser()
parser.input = [
createRegularToken(A),
createRegularToken(A),
createRegularToken(A)
]
const result = parser.LongRule()
expect(result).toBe(1);
})
it("Should pick longest alternative instead of first #2", () => {
const parser = new UnboundedLookaheadParser()
parser.input = [
createRegularToken(A),
createRegularToken(A),
createRegularToken(B)
]
const result = parser.LongRule()
expect(result).toBe(2);
})
it("Should pick shortest fitting alternative", () => {
const parser = new UnboundedLookaheadParser()
parser.input = []
const result = parser.LongRule()
expect(result).toBe(0);
})
})
describe("Incomplete alternative", () => {
const A = createToken({ name: "A", pattern: "a" })
const B = createToken({ name: "B", pattern: "b" })
const C = createToken({ name: "C", pattern: "c" })
class IncompleteLookaheadParser extends EmbeddedActionsParser {
constructor() {
super([A, B, C], {
lookaheadStrategy: new LLStarLookaheadStrategy({
incomplete: true
})
})
this.performSelfAnalysis()
}
result = -1;
Rule = this.RULE("Rule", () => {
return this.OR([
{
ALT: () => {
this.result = 0;
// ONLY A
this.CONSUME1(A)
}
},
{
ALT: () => {
this.result = 1;
// A B C
this.CONSUME2(A)
this.CONSUME1(B)
this.CONSUME1(C)
}
}
])
})
}
it("Should pick the longer, but incomplete alternative", () => {
const parser = new IncompleteLookaheadParser()
parser.input = [
createRegularToken(A),
createRegularToken(B)
]
parser.Rule()
expect(parser.result).toBe(1);
})
it("Should pick shorter alternative when it makes sense", () => {
const parser = new IncompleteLookaheadParser()
parser.input = [
createRegularToken(A),
createRegularToken(C)
]
parser.Rule()
expect(parser.result).toBe(0);
})
})
describe("Incomplete options", () => {
const A = createToken({ name: "A", pattern: "a" })
const B = createToken({ name: "B", pattern: "b" })
const C = createToken({ name: "C", pattern: "c" })
class IncompleteLookaheadParser extends EmbeddedActionsParser {
constructor() {
super([A, B, C], {
lookaheadStrategy: new LLStarLookaheadStrategy({
incomplete: true
})
})
this.performSelfAnalysis()
}
result = -1;
Rule = this.RULE("Rule", () => {
this.result = 0;
this.CONSUME1(A)
this.OPTION(() => {
this.result = 1;
this.CONSUME1(B)
this.CONSUME1(C)
});
})
}
it("Should pick the longer, but incomplete option", () => {
const parser = new IncompleteLookaheadParser()
parser.input = [
createRegularToken(A),
createRegularToken(B)
]
parser.Rule()
expect(parser.result).toBe(1);
})
it("Should not pick the option when no indicator exists", () => {
const parser = new IncompleteLookaheadParser()
parser.input = [
createRegularToken(A),
// B would be required to pick the option
createRegularToken(C)
]
parser.Rule()
expect(parser.result).toBe(0);
})
})
describe("Ambiguity Detection", () => {
const A = createToken({ name: "A" })
const B = createToken({ name: "B" })
class AmbigiousParser extends EmbeddedActionsParser {
ambiguityReports: string[] = []
constructor() {
super([A, B], {
lookaheadStrategy: new LLStarLookaheadStrategy({
logging: (message) => this.ambiguityReports.push(message)
})
});
this.performSelfAnalysis()
}
OptionRule = this.RULE("OptionRule", () => {
let usedOption = false
this.OPTION(() => {
this.AT_LEAST_ONE1(() => this.CONSUME1(A))
usedOption = true
})
this.AT_LEAST_ONE2(() => this.CONSUME2(A))
return usedOption
})
AltRule = this.RULE("AltRule", () => {
return this.OR([
{
ALT: () => {
this.SUBRULE(this.RuleB)
return 0
}
},
{
ALT: () => {
this.SUBRULE(this.RuleC)
return 1
}
}
])
})
RuleB = this.RULE("RuleB", () => {
this.MANY(() => this.CONSUME(A))
})
RuleC = this.RULE("RuleC", () => {
this.MANY(() => this.CONSUME(A))
this.OPTION(() => this.CONSUME(B))
})
AltRuleWithEOF = this.RULE("AltRuleWithEOF", () => {
return this.OR([
{
ALT: () => {
this.SUBRULE1(this.RuleEOF)
return 0
}
},
{
ALT: () => {
this.SUBRULE2(this.RuleEOF)
return 1
}
}
])
})
RuleEOF = this.RULE("RuleEOF", () => {
this.MANY1(() => this.CONSUME(A))
this.CONSUME(EOF)
})
AltRuleWithPred = this.RULE("AltRuleWithPred", (pred?: boolean) => {
return this.OR([
{
ALT: () => {
this.CONSUME1(A)
return 0
},
GATE: () => (pred === undefined ? true : pred)
},
{
ALT: () => {
this.CONSUME2(A)
return 1
},
GATE: () => (pred === undefined ? true : !pred)
},
{
ALT: () => {
this.CONSUME(B)
return 2
}
}
])
})
AltWithOption = this.RULE("AltWithOption", () => {
const intermediate = this.OR([
{
ALT: () => {
this.CONSUME1(A)
return 2
}
},
{
ALT: () => {
this.CONSUME2(B)
return 4
}
}
])
const option = this.OPTION(() => {
this.CONSUME3(A);
return 1;
})
return (option ?? 0) + intermediate;
})
}
it("Should pick option on ambiguity", () => {
const parser = new AmbigiousParser()
parser.input = [
createRegularToken(A),
createRegularToken(A),
createRegularToken(A)
]
const result = parser.OptionRule()
expect(result).toBeTruthy();
// The rule nests a `AT_LEAST_ONE` inside and outside the OPTION
// Both productions produce lookahead ambiguities
expect(parser.ambiguityReports[0]).toMatch("<0, 1> in <OPTION>")
expect(parser.ambiguityReports[1]).toMatch("<0, 1> in <AT_LEAST_ONE1>")
})
it("Should pick first alternative on ambiguity", () => {
const parser = new AmbigiousParser()
parser.input = [
createRegularToken(A),
createRegularToken(A),
createRegularToken(A)
]
const result = parser.AltRule()
expect(result).toBe(0);
expect(parser.ambiguityReports[0]).toMatch("<0, 1> in <OR>")
})
it("Should pick first alternative on EOF ambiguity", () => {
const parser = new AmbigiousParser()
parser.input = []
const result = parser.AltRuleWithEOF()
expect(result).toBe(0);
expect(parser.ambiguityReports[0]).toMatch("<0, 1> in <OR>")
})
it("Should pick correct alternative on long prefix", () => {
const parser = new AmbigiousParser()
parser.input = [
createRegularToken(A),
createRegularToken(A),
createRegularToken(B)
]
const result = parser.AltRule()
expect(result).toBe(1);
expect(parser.ambiguityReports).toHaveLength(0);
})
it("Should resolve ambiguity using predicate", () => {
const parser = new AmbigiousParser()
parser.input = [createRegularToken(A)]
const resultAutomatic = parser.AltRuleWithPred(undefined)
// Automatically resolving the ambiguity should return `0`
expect(resultAutomatic).toBe(0);
// It should also create an ambiguity report
expect(parser.ambiguityReports[0]).toMatch("<0, 1> in <OR>")
parser.ambiguityReports = []
parser.input = [createRegularToken(A)]
const resultTrue = parser.AltRuleWithPred(true)
expect(resultTrue).toBe(0);
parser.input = [createRegularToken(A)]
const resultFalse = parser.AltRuleWithPred(false)
expect(resultFalse).toBe(1),
expect(parser.ambiguityReports).toHaveLength(0);
})
it("Should pick non-ambigious alternative inside of ambigious, predicated alternation", () => {
const parser = new AmbigiousParser()
parser.input = [createRegularToken(B)]
const result = parser.AltRuleWithPred(undefined)
expect(result).toBe(2);
expect(parser.ambiguityReports).toHaveLength(0);
})
it("Should work with alternatives followed by optional elements", () => {
const parser = new AmbigiousParser();
parser.input = [createRegularToken(B), createRegularToken(A)];
const result = parser.AltWithOption();
expect(result).toBe(5);
})
})
})
function createRegularToken(
tokType: TokenType,
image = "",
startOffset = 1,
startLine?: number,
startColumn?: number,
endOffset?: number,
endLine?: number,
endColumn?: number
): IToken {
return {
image: image,
startOffset: startOffset,
startLine: startLine,
startColumn: startColumn,
endOffset: endOffset,
endLine: endLine,
endColumn: endColumn,
tokenTypeIdx: tokType.tokenTypeIdx!,
tokenType: tokType
}
}
+642
View File
@@ -0,0 +1,642 @@
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import map from "lodash-es/map.js"
import filter from "lodash-es/filter.js"
import {
IProduction,
IProductionWithOccurrence,
TokenType,
Alternation,
NonTerminal,
Rule,
Option,
RepetitionMandatory,
Repetition,
Terminal,
Alternative,
RepetitionWithSeparator,
RepetitionMandatoryWithSeparator,
LookaheadProductionType
} from "chevrotain"
export function buildATNKey(rule: Rule, type: LookaheadProductionType, occurrence: number): string {
return `${rule.name}_${type}_${occurrence}`;
}
export interface ATN {
decisionMap: Record<string, DecisionState>
states: ATNState[]
decisionStates: DecisionState[]
ruleToStartState: Map<Rule, RuleStartState>
ruleToStopState: Map<Rule, RuleStopState>
}
export const ATN_INVALID_TYPE = 0
export const ATN_BASIC = 1
export const ATN_RULE_START = 2
export const ATN_PLUS_BLOCK_START = 4
export const ATN_STAR_BLOCK_START = 5
// Currently unused as the ATN is not used for lexing
export const ATN_TOKEN_START = 6
export const ATN_RULE_STOP = 7
export const ATN_BLOCK_END = 8
export const ATN_STAR_LOOP_BACK = 9
export const ATN_STAR_LOOP_ENTRY = 10
export const ATN_PLUS_LOOP_BACK = 11
export const ATN_LOOP_END = 12
export type ATNState =
| BasicState
| BasicBlockStartState
| PlusBlockStartState
| PlusLoopbackState
| StarBlockStartState
| StarLoopbackState
| StarLoopEntryState
| BlockEndState
| RuleStartState
| RuleStopState
| LoopEndState
export interface ATNBaseState {
atn: ATN
production: IProductionWithOccurrence
stateNumber: number
rule: Rule
epsilonOnlyTransitions: boolean
transitions: Transition[]
nextTokenWithinRule: number[]
}
export interface BasicState extends ATNBaseState {
type: typeof ATN_BASIC
}
export interface BlockStartState extends DecisionState {
end: BlockEndState
}
export interface BasicBlockStartState extends BlockStartState {
type: typeof ATN_BASIC
}
export interface PlusBlockStartState extends BlockStartState {
loopback: PlusLoopbackState
type: typeof ATN_PLUS_BLOCK_START
}
export interface PlusLoopbackState extends DecisionState {
type: typeof ATN_PLUS_LOOP_BACK
}
export interface StarBlockStartState extends BlockStartState {
type: typeof ATN_STAR_BLOCK_START
}
export interface StarLoopbackState extends ATNBaseState {
type: typeof ATN_STAR_LOOP_BACK
}
export interface StarLoopEntryState extends DecisionState {
loopback: StarLoopbackState
type: typeof ATN_STAR_LOOP_ENTRY
}
export interface BlockEndState extends ATNBaseState {
start: BlockStartState
type: typeof ATN_BLOCK_END
}
export interface DecisionState extends ATNBaseState {
decision: number
}
export interface LoopEndState extends ATNBaseState {
loopback: ATNState
type: typeof ATN_LOOP_END
}
export interface RuleStartState extends ATNBaseState {
stop: RuleStopState
type: typeof ATN_RULE_START
}
export interface RuleStopState extends ATNBaseState {
type: typeof ATN_RULE_STOP
}
export interface Transition {
target: ATNState
isEpsilon(): boolean
}
export abstract class AbstractTransition implements Transition {
target: ATNState
constructor(target: ATNState) {
this.target = target
}
isEpsilon() {
return false
}
}
export class AtomTransition extends AbstractTransition {
tokenType: TokenType
constructor(target: ATNState, tokenType: TokenType) {
super(target)
this.tokenType = tokenType
}
}
export class EpsilonTransition extends AbstractTransition {
constructor(target: ATNState) {
super(target)
}
isEpsilon() {
return true
}
}
export class RuleTransition extends AbstractTransition {
rule: Rule
followState: ATNState
constructor(ruleStart: RuleStartState, rule: Rule, followState: ATNState) {
super(ruleStart)
this.rule = rule
this.followState = followState
}
isEpsilon() {
return true
}
}
interface ATNHandle {
left: ATNState
right: ATNState
}
export function createATN(rules: Rule[]): ATN {
const atn: ATN = {
decisionMap: {},
decisionStates: [],
ruleToStartState: new Map(),
ruleToStopState: new Map(),
states: []
}
createRuleStartAndStopATNStates(atn, rules)
const ruleLength = rules.length
for (let i = 0; i < ruleLength; i++) {
const rule = rules[i]
const ruleBlock = block(atn, rule, rule)
if (ruleBlock === undefined) {
continue
}
buildRuleHandle(atn, rule, ruleBlock)
}
return atn
}
function createRuleStartAndStopATNStates(atn: ATN, rules: Rule[]): void {
const ruleLength = rules.length
for (let i = 0; i < ruleLength; i++) {
const rule = rules[i]
const start = newState<RuleStartState>(atn, rule, undefined, {
type: ATN_RULE_START
})
const stop = newState<RuleStopState>(atn, rule, undefined, {
type: ATN_RULE_STOP
})
start.stop = stop
atn.ruleToStartState.set(rule, start)
atn.ruleToStopState.set(rule, stop)
}
}
function atom(
atn: ATN,
rule: Rule,
production: IProduction
): ATNHandle | undefined {
if (production instanceof Terminal) {
return tokenRef(atn, rule, production.terminalType, production)
} else if (production instanceof NonTerminal) {
return ruleRef(atn, rule, production)
} else if (production instanceof Alternation) {
return alternation(atn, rule, production)
} else if (production instanceof Option) {
return option(atn, rule, production)
} else if (production instanceof Repetition) {
return repetition(atn, rule, production)
} else if (production instanceof RepetitionWithSeparator) {
return repetitionSep(atn, rule, production)
} else if (production instanceof RepetitionMandatory) {
return repetitionMandatory(atn, rule, production)
} else if (production instanceof RepetitionMandatoryWithSeparator) {
return repetitionMandatorySep(atn, rule, production)
} else {
return block(atn, rule, production as Alternative)
}
}
function repetition(atn: ATN, rule: Rule, repetition: Repetition): ATNHandle {
const starState = newState<StarBlockStartState>(atn, rule, repetition, {
type: ATN_STAR_BLOCK_START
})
defineDecisionState(atn, starState)
const handle = makeAlts(
atn,
rule,
starState,
repetition,
block(atn, rule, repetition)
)
return star(atn, rule, repetition, handle)
}
function repetitionSep(
atn: ATN,
rule: Rule,
repetition: RepetitionWithSeparator
): ATNHandle {
const starState = newState<StarBlockStartState>(atn, rule, repetition, {
type: ATN_STAR_BLOCK_START
})
defineDecisionState(atn, starState)
const handle = makeAlts(
atn,
rule,
starState,
repetition,
block(atn, rule, repetition)
)
const sep = tokenRef(atn, rule, repetition.separator, repetition)
return star(atn, rule, repetition, handle, sep)
}
function repetitionMandatory(
atn: ATN,
rule: Rule,
repetition: RepetitionMandatory
): ATNHandle {
const plusState = newState<PlusBlockStartState>(atn, rule, repetition, {
type: ATN_PLUS_BLOCK_START
})
defineDecisionState(atn, plusState)
const handle = makeAlts(
atn,
rule,
plusState,
repetition,
block(atn, rule, repetition)
)
return plus(atn, rule, repetition, handle)
}
function repetitionMandatorySep(
atn: ATN,
rule: Rule,
repetition: RepetitionMandatoryWithSeparator
): ATNHandle {
const plusState = newState<PlusBlockStartState>(atn, rule, repetition, {
type: ATN_PLUS_BLOCK_START
})
defineDecisionState(atn, plusState)
const handle = makeAlts(
atn,
rule,
plusState,
repetition,
block(atn, rule, repetition)
)
const sep = tokenRef(atn, rule, repetition.separator, repetition)
return plus(atn, rule, repetition, handle, sep)
}
function alternation(
atn: ATN,
rule: Rule,
alternation: Alternation
): ATNHandle {
const start = newState<BasicBlockStartState>(atn, rule, alternation, {
type: ATN_BASIC
})
defineDecisionState(atn, start)
const alts = map(alternation.definition, (e) => atom(atn, rule, e))
const handle = makeAlts(atn, rule, start, alternation, ...alts)
return handle
}
function option(atn: ATN, rule: Rule, option: Option): ATNHandle {
const start = newState<BasicBlockStartState>(atn, rule, option, {
type: ATN_BASIC
})
defineDecisionState(atn, start)
const handle = makeAlts(atn, rule, start, option, block(atn, rule, option))
return optional(atn, rule, option, handle)
}
function block(
atn: ATN,
rule: Rule,
block: { definition: IProduction[] }
): ATNHandle | undefined {
const handles = filter(
map(block.definition, (e) => atom(atn, rule, e)),
(e) => e !== undefined
) as ATNHandle[]
if (handles.length === 1) {
return handles[0]
} else if (handles.length === 0) {
return undefined
} else {
return makeBlock(atn, handles)
}
}
function plus(
atn: ATN,
rule: Rule,
plus: IProductionWithOccurrence,
handle: ATNHandle,
sep?: ATNHandle
): ATNHandle {
const blkStart = handle.left as PlusBlockStartState
const blkEnd = handle.right
const loop = newState<PlusLoopbackState>(atn, rule, plus, {
type: ATN_PLUS_LOOP_BACK
})
defineDecisionState(atn, loop)
const end = newState<LoopEndState>(atn, rule, plus, {
type: ATN_LOOP_END
})
blkStart.loopback = loop
end.loopback = loop
atn.decisionMap[buildATNKey(rule, sep ? 'RepetitionMandatoryWithSeparator' : 'RepetitionMandatory', plus.idx)] = loop;
epsilon(blkEnd, loop) // block can see loop back
// Depending on whether we have a separator we put the exit transition at index 1 or 0
// This influences the chosen option in the lookahead DFA
if (sep === undefined) {
epsilon(loop, blkStart) // loop back to start
epsilon(loop, end) // exit
} else {
epsilon(loop, end) // exit
// loop back to start with separator
epsilon(loop, sep.left)
epsilon(sep.right, blkStart)
}
return {
left: blkStart,
right: end
}
}
function star(
atn: ATN,
rule: Rule,
star: IProductionWithOccurrence,
handle: ATNHandle,
sep?: ATNHandle
): ATNHandle {
const start = handle.left
const end = handle.right
const entry = newState<StarLoopEntryState>(atn, rule, star, {
type: ATN_STAR_LOOP_ENTRY
})
defineDecisionState(atn, entry)
const loopEnd = newState<LoopEndState>(atn, rule, star, {
type: ATN_LOOP_END
})
const loop = newState<StarLoopbackState>(atn, rule, star, {
type: ATN_STAR_LOOP_BACK
})
entry.loopback = loop
loopEnd.loopback = loop
epsilon(entry, start) // loop enter edge (alt 2)
epsilon(entry, loopEnd) // bypass loop edge (alt 1)
epsilon(end, loop) // block end hits loop back
if (sep !== undefined) {
epsilon(loop, loopEnd) // end loop
// loop back to start of handle using separator
epsilon(loop, sep.left)
epsilon(sep.right, start)
} else {
epsilon(loop, entry) // loop back to entry/exit decision
}
atn.decisionMap[buildATNKey(rule, sep ? 'RepetitionWithSeparator' : 'Repetition', star.idx)] = entry;
return {
left: entry,
right: loopEnd
}
}
function optional(atn: ATN, rule: Rule, optional: Option, handle: ATNHandle): ATNHandle {
const start = handle.left as DecisionState
const end = handle.right
epsilon(start, end)
atn.decisionMap[buildATNKey(rule, 'Option', optional.idx)] = start;
return handle
}
function defineDecisionState(atn: ATN, state: DecisionState): number {
atn.decisionStates.push(state)
state.decision = atn.decisionStates.length - 1
return state.decision
}
function makeAlts(
atn: ATN,
rule: Rule,
start: BlockStartState,
production: IProductionWithOccurrence,
...alts: (ATNHandle | undefined)[]
): ATNHandle {
const end = newState<BlockEndState>(atn, rule, production, {
type: ATN_BLOCK_END,
start
})
start.end = end
for (const alt of alts) {
if (alt !== undefined) {
// hook alts up to decision block
epsilon(start, alt.left)
epsilon(alt.right, end)
} else {
epsilon(start, end)
}
}
const handle: ATNHandle = {
left: start as ATNState,
right: end
}
atn.decisionMap[buildATNKey(rule, getProdType(production), production.idx)] = start
return handle
}
function getProdType(production: IProduction): LookaheadProductionType {
if (production instanceof Alternation) {
return 'Alternation';
} else if (production instanceof Option) {
return 'Option';
} else if (production instanceof Repetition) {
return 'Repetition';
} else if (production instanceof RepetitionWithSeparator) {
return 'RepetitionWithSeparator';
} else if (production instanceof RepetitionMandatory) {
return 'RepetitionMandatory';
} else if (production instanceof RepetitionMandatoryWithSeparator) {
return 'RepetitionMandatoryWithSeparator';
} else {
throw new Error('Invalid production type encountered');
}
}
function makeBlock(atn: ATN, alts: ATNHandle[]): ATNHandle {
const altsLength = alts.length
for (let i = 0; i < altsLength - 1; i++) {
const handle = alts[i]
let transition: Transition | undefined
if (handle.left.transitions.length === 1) {
transition = handle.left.transitions[0]
}
const isRuleTransition = transition instanceof RuleTransition
const ruleTransition = transition as RuleTransition
const next = alts[i + 1].left
if (
handle.left.type === ATN_BASIC &&
handle.right.type === ATN_BASIC &&
transition !== undefined &&
((isRuleTransition && ruleTransition.followState === handle.right) ||
transition.target === handle.right)
) {
// we can avoid epsilon edge to next element
if (isRuleTransition) {
ruleTransition.followState = next
} else {
transition.target = next
}
removeState(atn, handle.right) // we skipped over this state
} else {
// need epsilon if previous block's right end node is complex
epsilon(handle.right, next)
}
}
const first = alts[0]
const last = alts[altsLength - 1]
return {
left: first.left,
right: last.right
}
}
function tokenRef(
atn: ATN,
rule: Rule,
tokenType: TokenType,
production: IProductionWithOccurrence
): ATNHandle {
const left = newState<BasicState>(atn, rule, production, {
type: ATN_BASIC
})
const right = newState<BasicState>(atn, rule, production, {
type: ATN_BASIC
})
addTransition(left, new AtomTransition(right, tokenType))
return {
left,
right
}
}
function ruleRef(
atn: ATN,
currentRule: Rule,
nonTerminal: NonTerminal
): ATNHandle {
const rule = nonTerminal.referencedRule
const start = atn.ruleToStartState.get(rule)!
const left = newState<BasicBlockStartState>(atn, currentRule, nonTerminal, {
type: ATN_BASIC
})
const right = newState<BasicBlockStartState>(atn, currentRule, nonTerminal, {
type: ATN_BASIC
})
const call = new RuleTransition(start, rule, right)
addTransition(left, call)
return {
left,
right
}
}
function buildRuleHandle(atn: ATN, rule: Rule, block: ATNHandle): ATNHandle {
const start = atn.ruleToStartState.get(rule)!
epsilon(start, block.left)
const stop = atn.ruleToStopState.get(rule)!
epsilon(block.right, stop)
const handle: ATNHandle = {
left: start,
right: stop
}
return handle
}
function epsilon(a: ATNBaseState, b: ATNBaseState): void {
const transition = new EpsilonTransition(b as ATNState)
addTransition(a, transition)
}
function newState<T extends ATNState>(
atn: ATN,
rule: Rule,
production: IProductionWithOccurrence | undefined,
partial: Partial<T>
): T {
const t: T = {
atn,
production,
epsilonOnlyTransitions: false,
rule,
transitions: [],
nextTokenWithinRule: [],
stateNumber: atn.states.length,
...partial
} as unknown as T
atn.states.push(t)
return t
}
function addTransition(state: ATNBaseState, transition: Transition) {
// A single ATN state can only contain epsilon transitions or non-epsilon transitions
// Because they are never mixed, only setting the property for the first transition is fine
if (state.transitions.length === 0) {
state.epsilonOnlyTransitions = transition.isEpsilon()
}
state.transitions.push(transition)
}
function removeState(atn: ATN, state: ATNState): void {
atn.states.splice(atn.states.indexOf(state), 1)
}
+78
View File
@@ -0,0 +1,78 @@
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import map from "lodash-es/map.js"
import { ATNState, DecisionState } from "./atn.js"
export interface DFA {
start?: DFAState
states: Record<string, DFAState>
decision: number
atnStartState: DecisionState
}
export interface DFAState {
configs: ATNConfigSet
edges: Record<number, DFAState>
isAcceptState: boolean
prediction: number
}
export const DFA_ERROR = {} as DFAState
export interface ATNConfig {
state: ATNState
alt: number
stack: ATNState[]
}
export class ATNConfigSet {
private map: Record<string, number> = {}
private configs: ATNConfig[] = []
uniqueAlt: number | undefined
get size(): number {
return this.configs.length
}
finalize(): void {
// Empties the map to free up memory
this.map = {}
}
add(config: ATNConfig): void {
const key = getATNConfigKey(config)
// Only add configs which don't exist in our map already
// While this does not influence the actual algorithm, adding them anyway would massively increase memory consumption
if (!(key in this.map)) {
this.map[key] = this.configs.length
this.configs.push(config)
}
}
get elements(): readonly ATNConfig[] {
return this.configs
}
get alts(): number[] {
return map(this.configs, (e) => e.alt)
}
get key(): string {
let value = ""
for (const k in this.map) {
value += k + ":"
}
return value
}
}
export function getATNConfigKey(config: ATNConfig, alt = true) {
return `${alt ? `a${config.alt}` : ""}s${
config.state.stateNumber
}:${config.stack.map((e) => e.stateNumber.toString()).join("_")}`
}
+11
View File
@@ -0,0 +1,11 @@
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
export {
AmbiguityReport,
LLStarLookaheadOptions,
LLStarLookaheadStrategy
} from './all-star-lookahead.js';