mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
11 KiB
302 lines
11 KiB
/******************************************************************************
|
|
* 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"
|
|
|
|
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("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
|
|
}
|
|
}
|
|
|