Skip to content

Commit

Permalink
Support empty alternative in alternation (OR)
Browse files Browse the repository at this point in the history
fixes #41
  • Loading branch information
Shahar Soel committed Dec 8, 2015
1 parent 6d336a8 commit bc2798c
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ if (!testMode) {
API.extendToken = chevrotain.extendToken
API.tokenName = chevrotain.tokenName

// Other Utilities
API.EMPTY_ALT = chevrotain.EMPTY_ALT

API.exceptions = {}
API.exceptions.isRecognitionException = chevrotain.exceptions.isRecognitionException
API.exceptions.EarlyExitException = chevrotain.exceptions.EarlyExitException
Expand Down
48 changes: 36 additions & 12 deletions src/parse/grammar/lookahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,45 @@ namespace chevrotain.lookahead {
}
}

/**
* This will return the Index of the alternative to take or -1 if none of the alternatives match
*/
return function ():number {
let nextToken = this.NEXT_TOKEN()
for (let i = 0; i < alternativesTokens.length; i++) {
let currAltTokens = alternativesTokens[i]
for (let j = 0; j < (<any>currAltTokens).length; j++) {
if (nextToken instanceof currAltTokens[j]) {
return i
let hasLastAnEmptyAlt = _.isEmpty(_.last(alternativesTokens))
if (hasLastAnEmptyAlt) {
let lastIdx = alternativesTokens.length - 1
/**
* This will return the Index of the alternative to take or the <lastidx> if only the empty alternative matched
*/
return function chooseAlternativeWithEmptyAlt():number {
let nextToken = this.NEXT_TOKEN()
// checking only until length - 1 because there is nothing to check in an empty alternative, it is always valid
for (let i = 0; i < lastIdx; i++) {
let currAltTokens = alternativesTokens[i]
// 'for' loop for performance reasons.
for (let j = 0; j < (<any>currAltTokens).length; j++) {
if (nextToken instanceof currAltTokens[j]) {
return i
}
}
}
// an OR(alternation) with an empty alternative will always match
return lastIdx;
}
}
else {
/**
* This will return the Index of the alternative to take or -1 if none of the alternatives match
*/
return function chooseAlternative():number {
let nextToken = this.NEXT_TOKEN()
for (let i = 0; i < alternativesTokens.length; i++) {
let currAltTokens = alternativesTokens[i]
// 'for' loop for performance reasons.
for (let j = 0; j < (<any>currAltTokens).length; j++) {
if (nextToken instanceof currAltTokens[j]) {
return i
}
}
}
return -1;
}
return -1;
}
}

Expand Down Expand Up @@ -128,5 +153,4 @@ namespace chevrotain.lookahead {
return false
}
}

}
46 changes: 45 additions & 1 deletion src/parse/parser_public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,50 @@ namespace chevrotain {
export type LookAheadFunc = () => boolean
export type GrammarAction = () => void

/**
* convenience used to express an empty alternative in an OR (alternation).
* can be used to more clearly describe the intent in a case of empty alternation.
*
* for example:
*
* 1. without using EMPTY_ALT:
*
* this.OR([
* {ALT: () => {
* this.CONSUME1(OneTok)
* return "1"
* }},
* {ALT: () => {
* this.CONSUME1(TwoTok)
* return "2"
* }},
* {ALT: () => { // implicitly empty because there are no invoked grammar rules (OR/MANY/CONSUME...) inside this alternative.
* return "666"
* }},
* ])
*
*
* * 2. using EMPTY_ALT:
*
* this.OR([
* {ALT: () => {
* this.CONSUME1(OneTok)
* return "1"
* }},
* {ALT: () => {
* this.CONSUME1(TwoTok)
* return "2"
* }},
* {ALT: EMPTY_ALT("666")}, // explicitly empty, clearer intent
* ])
*
*/
export let EMPTY_ALT = function emptyAlt<T>(value:T):() => T {
return function () {
return value
}
}

let EOF_FOLLOW_KEY:any = {}

/**
Expand Down Expand Up @@ -557,7 +601,7 @@ namespace chevrotain {
*
* using the short form is recommended as it will compute the lookahead function
* automatically. however this currently has one limitation:
* It only works if the lookahead for the grammar is one.
* It only works if the lookahead for the grammar is one LL(1).
*
* As in CONSUME the index in the method name indicates the occurrence
* of the alternation production in it's top rule.
Expand Down
16 changes: 10 additions & 6 deletions test/parse/grammar/lookahead_spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

namespace chevrotain.lookahead.spec {

import samples = specs.samples
Expand Down Expand Up @@ -41,7 +40,6 @@ namespace chevrotain.lookahead.spec {
}
}


describe("The Grammar Lookahead namespace", function () {
"use strict"

Expand All @@ -66,7 +64,7 @@ namespace chevrotain.lookahead.spec {
expect(laFunc.call(new IdentParserMock([], []))).to.equal(false)
})

it("can compute the lookahead function for the first MANY in ActionDec", function () {
it("can compute the lookahead function for lots of ORs sample", function () {
let laFunc = lookahead.buildLookaheadForOr(1, samples.lotsOfOrs)

expect(laFunc.call(new CommaParserMock([], []))).to.equal(0)
Expand All @@ -75,6 +73,15 @@ namespace chevrotain.lookahead.spec {
expect(laFunc.call(new ColonParserMock([], []))).to.equal(-1)
})

it("can compute the lookahead function for EMPTY OR sample", function () {
let laFunc = lookahead.buildLookaheadForOr(1, samples.emptyAltOr)

expect(laFunc.call(new KeyParserMock([], []))).to.equal(0)
expect(laFunc.call(new EntityParserMock([], []))).to.equal(1)
// none matches so the last empty alternative should be taken (idx 2)
expect(laFunc.call(new CommaParserMock([], []))).to.equal(2)
})

it("can compute the lookahead function for a Top Level Rule", function () {
let laFunc = lookahead.buildLookaheadForTopLevel(samples.actionDec)

Expand All @@ -92,7 +99,6 @@ namespace chevrotain.lookahead.spec {
})
})


class A extends Token {}
class B extends Token {}
class C extends Token {}
Expand All @@ -109,6 +115,4 @@ namespace chevrotain.lookahead.spec {
expect(ambiguities[0].alts).to.deep.equal([2, 3])
})
})


}
11 changes: 11 additions & 0 deletions test/parse/grammar/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ namespace specs.samples {
]),
])

export let emptyAltOr = new gast.Rule("emptyAltOr", [
new gast.Alternation([
new gast.Flat([
new gast.Terminal(KeyTok, 1)
]),
new gast.Flat([
new gast.Terminal(EntityTok, 1)
]),
new gast.Flat([]) // an empty alternative
])
])

export let callArguments = new gast.Rule("callArguments", [
new gast.RepetitionWithSeparator([
Expand Down
47 changes: 47 additions & 0 deletions test/parse/recognizer_lookahead_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,53 @@ namespace chevrotain.recognizer.lookahead.spec {
})
})

class OrImplicitEmptyAltLookAheadParser extends Parser {

public getLookAheadCache():lang.HashTable<Function> {
return cache.getLookaheadFuncsForClass(this.className)
}

constructor(input:Token[] = []) {
super(input, <any>chevrotain.recognizer.lookahead.spec)
Parser.performSelfAnalysis(this)
}

public orRule = this.RULE("orRule", this.parseOrRule, () => { return "-666" })

private parseOrRule():string {
// @formatter:off
return this.OR1([
{ALT: () => {
this.CONSUME1(OneTok)
return "1"
}},
{ALT: () => {
this.CONSUME1(TwoTok)
return "2"
}},
{ALT: chevrotain.EMPTY_ALT("EMPTY_ALT")
},
])
// @formatter:on
}
}

describe("The support for EMPTY alternative implicit lookahead in OR", function () {

it("can match an non-empty alternative in an OR with an empty alternative", function () {
let input = [new OneTok()]
let parser = new OrImplicitEmptyAltLookAheadParser(input)
expect(parser.orRule()).to.equal("1")
})

it("can match an empty alternative", function () {
let input = []
let parser = new OrImplicitEmptyAltLookAheadParser(input)
expect(parser.orRule()).to.equal("EMPTY_ALT")
})

})

class OrExplicitLookAheadParser extends Parser {

public getLookAheadCache():lang.HashTable<Function> {
Expand Down

0 comments on commit bc2798c

Please sign in to comment.