You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pegjs/test/behavior/generated-parser-behavior.s...

2210 lines
73 KiB
JavaScript

"use strict";
const chai = require( "chai" );
const peg = require( "pegjs" );
const sinon = require( "sinon" );
const expect = chai.expect;
describe( "generated parser behavior", function () {
function varyOptimizationOptions( block ) {
const optionsVariants = [
{ cache: false, optimize: "speed", trace: false },
{ cache: false, optimize: "speed", trace: true },
{ cache: false, optimize: "size", trace: false },
{ cache: false, optimize: "size", trace: true },
{ cache: true, optimize: "speed", trace: false },
{ cache: true, optimize: "speed", trace: true },
{ cache: true, optimize: "size", trace: false },
{ cache: true, optimize: "size", trace: true }
];
optionsVariants.forEach( variant => {
describe(
"with options " + chai.util.inspect( variant ),
function () {
block( peg.util.clone( variant ) );
}
);
} );
}
function withConsoleStub( block ) {
if ( typeof console === "object" ) sinon.stub( console, "log" );
try {
return block();
} finally {
if ( typeof console === "object" ) console.log.restore();
}
}
function helpers( chai, utils ) {
const Assertion = chai.Assertion;
Assertion.addMethod( "parse", function ( input, expected, options ) {
options = typeof options !== "undefined" ? options : {};
const result = withConsoleStub( () =>
utils.flag( this, "object" ).parse( input, options )
);
if ( typeof expected !== "undefined" ) {
this.assert(
utils.eql( result, expected ),
"expected #{this} to parse input as #{exp} but got #{act}",
"expected #{this} to not parse input as #{exp}",
expected,
result,
! utils.flag( this, "negate" )
);
}
} );
Assertion.addMethod( "failToParse", function ( input, props, options ) {
options = typeof options !== "undefined" ? options : {};
let passed, result;
try {
result = withConsoleStub( () =>
utils.flag( this, "object" ).parse( input, options )
);
passed = true;
} catch ( e ) {
result = e;
passed = false;
}
this.assert(
! passed,
"expected #{this} to fail to parse input but got #{act}",
"expected #{this} to not fail to parse input but #{act} was thrown",
null,
result
);
if ( ! passed && typeof props !== "undefined" ) {
Object.keys( props ).forEach( key => {
new Assertion( result ).to.have.property( key )
.that.is.deep.equal( props[ key ] );
} );
}
} );
}
// Helper activation needs to put inside a |beforeEach| block because the
// helpers conflict with the ones in test/unit/parser.spec.js.
beforeEach( function () {
chai.use( helpers );
} );
varyOptimizationOptions( function ( options ) {
describe( "initializer", function () {
it( "executes the code before parsing starts", function () {
const parser = peg.generate( [
"{ var result = 42; }",
"start = 'a' { return result; }"
].join( "\n" ), options );
expect( parser ).to.parse( "a", 42 );
} );
describe( "available variables and functions", function () {
it( "|options| contains options", function () {
const parser = peg.generate( [
"{ var result = options; }",
"start = 'a' { return result; }"
].join( "\n" ), options );
expect( parser ).to.parse( "a", { a: 42 }, { a: 42 } );
} );
} );
} );
describe( "rule", function () {
if ( options.cache ) {
it( "caches rule match results", function () {
const parser = peg.generate( [
"{ var n = 0; }",
"start = (a 'b') / (a 'c') { return n; }",
"a = 'a' { n++; }"
].join( "\n" ), options );
expect( parser ).to.parse( "ac", 1 );
} );
} else {
it( "doesn't cache rule match results", function () {
const parser = peg.generate( [
"{ var n = 0; }",
"start = (a 'b') / (a 'c') { return n; }",
"a = 'a' { n++; }"
].join( "\n" ), options );
expect( parser ).to.parse( "ac", 2 );
} );
}
describe( "when the expression matches", function () {
it( "returns its match result", function () {
const parser = peg.generate( "start = 'a'" );
expect( parser ).to.parse( "a", "a" );
} );
} );
describe( "when the expression doesn't match", function () {
describe( "without display name", function () {
it( "reports match failure and doesn't record any expectation", function () {
const parser = peg.generate( "start = 'a'" );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "literal", text: "a", ignoreCase: false } ]
} );
} );
} );
describe( "with display name", function () {
it( "reports match failure and records an expectation of type \"other\"", function () {
const parser = peg.generate( "start 'start' = 'a'" );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "other", description: "start" } ]
} );
} );
it( "discards any expectations recorded when matching the expression", function () {
const parser = peg.generate( "start 'start' = 'a'" );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "other", description: "start" } ]
} );
} );
} );
} );
} );
describe( "literal", function () {
describe( "matching", function () {
it( "matches empty literals", function () {
const parser = peg.generate( "start = ''", options );
expect( parser ).to.parse( "" );
} );
it( "matches one-character literals", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.failToParse( "b" );
} );
it( "matches multi-character literals", function () {
const parser = peg.generate( "start = 'abcd'", options );
expect( parser ).to.parse( "abcd" );
expect( parser ).to.failToParse( "efgh" );
} );
it( "is case sensitive without the \"i\" flag", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.failToParse( "A" );
} );
it( "is case insensitive with the \"i\" flag", function () {
const parser = peg.generate( "start = 'a'i", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.parse( "A" );
} );
} );
describe( "when it matches", function () {
it( "returns the matched text", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.parse( "a", "a" );
} );
it( "consumes the matched text", function () {
const parser = peg.generate( "start = 'a' .", options );
expect( parser ).to.parse( "ab" );
} );
} );
describe( "when it doesn't match", function () {
it( "reports match failure and records an expectation of type \"literal\"", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "literal", text: "a", ignoreCase: false } ]
} );
} );
} );
} );
describe( "character class", function () {
describe( "matching", function () {
it( "matches empty classes", function () {
const parser = peg.generate( "start = []", options );
expect( parser ).to.failToParse( "a" );
} );
it( "matches classes with a character list", function () {
const parser = peg.generate( "start = [abc]", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.parse( "b" );
expect( parser ).to.parse( "c" );
expect( parser ).to.failToParse( "d" );
} );
it( "matches classes with a character range", function () {
const parser = peg.generate( "start = [a-c]", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.parse( "b" );
expect( parser ).to.parse( "c" );
expect( parser ).to.failToParse( "d" );
} );
it( "matches inverted classes", function () {
const parser = peg.generate( "start = [^a]", options );
expect( parser ).to.failToParse( "a" );
expect( parser ).to.parse( "b" );
} );
it( "is case sensitive without the \"i\" flag", function () {
const parser = peg.generate( "start = [a]", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.failToParse( "A" );
} );
it( "is case insensitive with the \"i\" flag", function () {
const parser = peg.generate( "start = [a]i", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.parse( "A" );
} );
} );
describe( "when it matches", function () {
it( "returns the matched character", function () {
const parser = peg.generate( "start = [a]", options );
expect( parser ).to.parse( "a", "a" );
} );
it( "consumes the matched character", function () {
const parser = peg.generate( "start = [a] .", options );
expect( parser ).to.parse( "ab" );
} );
} );
describe( "when it doesn't match", function () {
it( "reports match failure and records an expectation of type \"class\"", function () {
const parser = peg.generate( "start = [a]", options );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "class", parts: [ "a" ], inverted: false, ignoreCase: false } ]
} );
} );
} );
} );
describe( "dot", function () {
describe( "matching", function () {
it( "matches any character", function () {
const parser = peg.generate( "start = .", options );
expect( parser ).to.parse( "a" );
expect( parser ).to.parse( "b" );
expect( parser ).to.parse( "c" );
} );
} );
describe( "when it matches", function () {
it( "returns the matched character", function () {
const parser = peg.generate( "start = .", options );
expect( parser ).to.parse( "a", "a" );
} );
it( "consumes the matched character", function () {
const parser = peg.generate( "start = . .", options );
expect( parser ).to.parse( "ab" );
} );
} );
describe( "when it doesn't match", function () {
it( "reports match failure and records an expectation of type \"any\"", function () {
const parser = peg.generate( "start = .", options );
expect( parser ).to.failToParse( "", {
expected: [ { type: "any" } ]
} );
} );
} );
} );
describe( "rule reference", function () {
describe( "when referenced rule's expression matches", function () {
it( "returns its result", function () {
const parser = peg.generate( [
"start = a",
"a = 'a'"
].join( "\n" ), options );
expect( parser ).to.parse( "a", "a" );
} );
} );
describe( "when referenced rule's expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( [
"start = a",
"a = 'a'"
].join( "\n" ), options );
expect( parser ).to.failToParse( "b" );
} );
} );
} );
describe( "positive semantic predicate", function () {
describe( "when the code returns a truthy value", function () {
it( "returns |undefined|", function () {
// The |""| is needed so that the parser doesn't return just
// |undefined| which we can't compare against in |toParse| due to the
// way optional parameters work.
const parser = peg.generate( "start = &{ return true; } ''", options );
expect( parser ).to.parse( "", [ void 0, "" ] );
} );
} );
describe( "when the code returns a falsey value", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = &{ return false; }", options );
expect( parser ).to.failToParse( "" );
} );
} );
describe( "label variables", function () {
describe( "in containing sequence", function () {
it( "can access variables defined by preceding labeled elements", function () {
const parser = peg.generate(
"start = a:'a' &{ return a === 'a'; }",
options
);
expect( parser ).to.parse( "a" );
} );
it( "cannot access variable defined by labeled predicate element", function () {
const parser = peg.generate(
"start = 'a' b:&{ return b === undefined; } 'c'",
options
);
expect( parser ).to.failToParse( "ac" );
} );
it( "cannot access variables defined by following labeled elements", function () {
const parser = peg.generate(
"start = &{ return a === 'a'; } a:'a'",
options
);
expect( parser ).to.failToParse( "a" );
} );
it( "cannot access variables defined by subexpressions", function () {
const testcases = [
{
grammar: "start = (a:'a') &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')? &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')* &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')+ &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = $(a:'a') &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = &(a:'a') 'a' &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = !(a:'a') 'b' &{ return a === 'a'; }",
input: "b"
},
{
grammar: "start = b:(a:'a') &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = ('a' b:'b' 'c') &{ return b === 'b'; }",
input: "abc"
},
{
grammar: "start = (a:'a' { return a; }) &{ return a === 'a'; }",
input: "a"
},
{
grammar: "start = ('a' / b:'b' / 'c') &{ return b === 'b'; }",
input: "b"
}
];
testcases.forEach( testcase => {
const parser = peg.generate( testcase.grammar, options );
expect( parser ).to.failToParse( testcase.input );
} );
} );
} );
describe( "in outer sequence", function () {
it( "can access variables defined by preceding labeled elements", function () {
const parser = peg.generate(
"start = a:'a' ('b' &{ return a === 'a'; })",
options
);
expect( parser ).to.parse( "ab" );
} );
it( "cannot access variable defined by labeled predicate element", function () {
const parser = peg.generate(
"start = 'a' b:('b' &{ return b === undefined; }) 'c'",
options
);
expect( parser ).to.failToParse( "abc" );
} );
it( "cannot access variables defined by following labeled elements", function () {
const parser = peg.generate(
"start = ('a' &{ return b === 'b'; }) b:'b'",
options
);
expect( parser ).to.failToParse( "ab" );
} );
} );
} );
describe( "initializer variables & functions", function () {
it( "can access variables defined in the initializer", function () {
const parser = peg.generate( [
"{ var v = 42 }",
"start = &{ return v === 42; }"
].join( "\n" ), options );
expect( parser ).to.parse( "" );
} );
it( "can access functions defined in the initializer", function () {
const parser = peg.generate( [
"{ function f() { return 42; } }",
"start = &{ return f() === 42; }"
].join( "\n" ), options );
expect( parser ).to.parse( "" );
} );
} );
describe( "available variables & functions", function () {
it( "|options| contains options", function () {
const parser = peg.generate( [
"{ var result; }",
"start = &{ result = options; return true; } { return result; }"
].join( "\n" ), options );
expect( parser ).to.parse( "", { a: 42 }, { a: 42 } );
} );
it( "|location| returns current location info", function () {
const parser = peg.generate( [
"{ var result; }",
"start = line (nl+ line)* { return result; }",
"line = thing (' '+ thing)*",
"thing = digit / mark",
"digit = [0-9]",
"mark = &{ result = location(); return true; } 'x'",
"nl = '\\r'? '\\n'"
].join( "\n" ), options );
expect( parser ).to.parse( "1\n2\n\n3\n\n\n4 5 x", {
start: { offset: 13, line: 7, column: 5 },
end: { offset: 13, line: 7, column: 5 }
} );
// Newline representations
expect( parser ).to.parse( "1\nx", { // Unix
start: { offset: 2, line: 2, column: 1 },
end: { offset: 2, line: 2, column: 1 }
} );
expect( parser ).to.parse( "1\r\nx", { // Windows
start: { offset: 3, line: 2, column: 1 },
end: { offset: 3, line: 2, column: 1 }
} );
} );
it( "|offset| returns current start offset", function () {
const parser = peg.generate( [
"start = [0-9]+ val:mark { return val; }",
"mark = 'xx' { return offset(); }"
].join( "\n" ), options );
expect( parser ).to.parse( "0123456xx", 7 );
} );
it( "|range| returns current range", function () {
const parser = peg.generate( [
"start = [0-9]+ val:mark { return val; }",
"mark = 'xx' { return range(); }"
].join( "\n" ), options );
expect( parser ).to.parse( "0123456xx", [ 7, 9 ] );
} );
} );
} );
describe( "negative semantic predicate", function () {
describe( "when the code returns a falsey value", function () {
it( "returns |undefined|", function () {
// The |""| is needed so that the parser doesn't return just
// |undefined| which we can't compare against in |toParse| due to the
// way optional parameters work.
const parser = peg.generate( "start = !{ return false; } ''", options );
expect( parser ).to.parse( "", [ void 0, "" ] );
} );
} );
describe( "when the code returns a truthy value", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = !{ return true; }", options );
expect( parser ).to.failToParse( "" );
} );
} );
describe( "label variables", function () {
describe( "in containing sequence", function () {
it( "can access variables defined by preceding labeled elements", function () {
const parser = peg.generate(
"start = a:'a' !{ return a !== 'a'; }",
options
);
expect( parser ).to.parse( "a" );
} );
it( "cannot access variable defined by labeled predicate element", function () {
const parser = peg.generate(
"start = 'a' b:!{ return b !== undefined; } 'c'",
options
);
expect( parser ).to.failToParse( "ac" );
} );
it( "cannot access variables defined by following labeled elements", function () {
const parser = peg.generate(
"start = !{ return a !== 'a'; } a:'a'",
options
);
expect( parser ).to.failToParse( "a" );
} );
it( "cannot access variables defined by subexpressions", function () {
const testcases = [
{
grammar: "start = (a:'a') !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')? !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')* !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = (a:'a')+ !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = $(a:'a') !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = &(a:'a') 'a' !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = !(a:'a') 'b' !{ return a !== 'a'; }",
input: "b"
},
{
grammar: "start = b:(a:'a') !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = ('a' b:'b' 'c') !{ return b !== 'b'; }",
input: "abc"
},
{
grammar: "start = (a:'a' { return a; }) !{ return a !== 'a'; }",
input: "a"
},
{
grammar: "start = ('a' / b:'b' / 'c') !{ return b !== 'b'; }",
input: "b"
}
];
testcases.forEach( testcase => {
const parser = peg.generate( testcase.grammar, options );
expect( parser ).to.failToParse( testcase.input );
} );
} );
} );
describe( "in outer sequence", function () {
it( "can access variables defined by preceding labeled elements", function () {
const parser = peg.generate(
"start = a:'a' ('b' !{ return a !== 'a'; })",
options
);
expect( parser ).to.parse( "ab" );
} );
it( "cannot access variable defined by labeled predicate element", function () {
const parser = peg.generate(
"start = 'a' b:('b' !{ return b !== undefined; }) 'c'",
options
);
expect( parser ).to.failToParse( "abc" );
} );
it( "cannot access variables defined by following labeled elements", function () {
const parser = peg.generate(
"start = ('a' !{ return b !== 'b'; }) b:'b'",
options
);
expect( parser ).to.failToParse( "ab" );
} );
} );
} );
describe( "initializer variables & functions", function () {
it( "can access variables defined in the initializer", function () {
const parser = peg.generate( [
"{ var v = 42 }",
"start = !{ return v !== 42; }"
].join( "\n" ), options );
expect( parser ).to.parse( "" );
} );
it( "can access functions defined in the initializer", function () {
const parser = peg.generate( [
"{ function f() { return 42; } }",
"start = !{ return f() !== 42; }"
].join( "\n" ), options );
expect( parser ).to.parse( "" );
} );
} );
describe( "available variables & functions", function () {
it( "|options| contains options", function () {
const parser = peg.generate( [
"{ var result; }",
"start = !{ result = options; return false; } { return result; }"
].join( "\n" ), options );
expect( parser ).to.parse( "", { a: 42 }, { a: 42 } );
} );
it( "|location| returns current location info", function () {
const parser = peg.generate( [
"{ var result; }",
"start = line (nl+ line)* { return result; }",
"line = thing (' '+ thing)*",
"thing = digit / mark",
"digit = [0-9]",
"mark = !{ result = location(); return false; } 'x'",
"nl = '\\r'? '\\n'"
].join( "\n" ), options );
expect( parser ).to.parse( "1\n2\n\n3\n\n\n4 5 x", {
start: { offset: 13, line: 7, column: 5 },
end: { offset: 13, line: 7, column: 5 }
} );
// Newline representations
expect( parser ).to.parse( "1\nx", { // Unix
start: { offset: 2, line: 2, column: 1 },
end: { offset: 2, line: 2, column: 1 }
} );
expect( parser ).to.parse( "1\r\nx", { // Windows
start: { offset: 3, line: 2, column: 1 },
end: { offset: 3, line: 2, column: 1 }
} );
} );
} );
} );
describe( "group", function () {
describe( "when the expression matches", function () {
it( "returns its match result", function () {
const parser = peg.generate( "start = ('a')", options );
expect( parser ).to.parse( "a", "a" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = ('a')", options );
expect( parser ).to.failToParse( "b" );
} );
} );
} );
describe( "optional", function () {
describe( "when the expression matches", function () {
it( "returns its match result", function () {
const parser = peg.generate( "start = 'a'?", options );
expect( parser ).to.parse( "a", "a" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "returns |null|", function () {
const parser = peg.generate( "start = 'a'?", options );
expect( parser ).to.parse( "", null );
} );
} );
} );
describe( "zero or more", function () {
describe( "when the expression matches zero or more times", function () {
it( "returns an array of its match results", function () {
const parser = peg.generate( "start = 'a'*", options );
expect( parser ).to.parse( "", [] );
expect( parser ).to.parse( "a", [ "a" ] );
expect( parser ).to.parse( "aaa", [ "a", "a", "a" ] );
} );
} );
} );
describe( "one or more", function () {
describe( "when the expression matches one or more times", function () {
it( "returns an array of its match results", function () {
const parser = peg.generate( "start = 'a'+", options );
expect( parser ).to.parse( "a", [ "a" ] );
expect( parser ).to.parse( "aaa", [ "a", "a", "a" ] );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = 'a'+", options );
expect( parser ).to.failToParse( "" );
} );
} );
} );
describe( "text", function () {
describe( "when the expression matches", function () {
it( "returns the matched text", function () {
const parser = peg.generate( "start = $('a' 'b' 'c')", options );
expect( parser ).to.parse( "abc", "abc" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = $('a')", options );
expect( parser ).to.failToParse( "b" );
} );
} );
} );
describe( "positive simple predicate", function () {
describe( "when the expression matches", function () {
it( "returns |undefined|", function () {
const parser = peg.generate( "start = &'a' 'a'", options );
expect( parser ).to.parse( "a", [ void 0, "a" ] );
} );
it( "resets parse position", function () {
const parser = peg.generate( "start = &'a' 'a'", options );
expect( parser ).to.parse( "a" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = &'a'", options );
expect( parser ).to.failToParse( "b" );
} );
it( "doesn't discard any expectations recorded when matching the expression", function () {
const parser = peg.generate( "start = 'a' / &'b' / 'c'", options );
expect( parser ).to.failToParse( "d", {
expected: [
{ type: "literal", text: "a", ignoreCase: false },
{ type: "literal", text: "b", ignoreCase: false },
{ type: "literal", text: "c", ignoreCase: false }
]
} );
} );
it( "records expectations from right place", function () {
const parser = peg.generate( "start = 'a' / &'b' .", options );
expect( parser ).to.failToParse( "d", {
expected: [
{ type: "literal", text: "a", ignoreCase: false },
{ type: "literal", text: "b", ignoreCase: false }
]
} );
} );
} );
} );
describe( "negative simple predicate", function () {
describe( "when the expression matches", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = !'a'", options );
expect( parser ).to.failToParse( "a" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "returns |undefined|", function () {
const parser = peg.generate( "start = !'a' 'b'", options );
expect( parser ).to.parse( "b", [ void 0, "b" ] );
} );
it( "resets parse position", function () {
const parser = peg.generate( "start = !'a' 'b'", options );
expect( parser ).to.parse( "b" );
} );
it( "inverts any expectations recorded when matching the expression", function () {
const parser = peg.generate( "start = 'a' / !'b' / 'c'", options );
expect( parser ).to.failToParse( "b", {
expected: [
{ type: "literal", text: "a", ignoreCase: false },
{ type: "not", expected: { type: "literal", text: "b", ignoreCase: false } },
{ type: "literal", text: "c", ignoreCase: false }
]
} );
} );
it( "records expectations from right place", function () {
const parser = peg.generate( "start = 'a' / !'b' .", options );
expect( parser ).to.failToParse( "b", {
expected: [
{ type: "literal", text: "a", ignoreCase: false },
{ type: "not", expected: { type: "literal", text: "b", ignoreCase: false } }
]
} );
} );
it( "reports not inverted expectations when the expression inverted twice", function () {
const parser = peg.generate( "start = !(!'a')", options );
expect( parser ).to.failToParse( "b", {
expected: [
{ type: "literal", text: "a", ignoreCase: false }
]
} );
} );
} );
} );
describe( "label", function () {
describe( "when the expression matches", function () {
it( "returns its match result", function () {
const parser = peg.generate( "start = a:'a'", options );
expect( parser ).to.parse( "a", "a" );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = a:'a'", options );
expect( parser ).to.failToParse( "b" );
} );
} );
} );
describe( "sequence", function () {
describe( "when all expressions match", function () {
function parser( description, edgecases ) {
it( description, () => {
edgecases.forEach( ( { grammar, input, output } ) => {
const parser = peg.generate( grammar, options );
expect( parser ).to.parse( input, output );
} );
} );
}
parser( "returns an array of their match results", [
{
grammar: "start = 'a' 'b' 'c'",
input: "abc",
output: [ "a", "b", "c" ]
},
] );
parser( "plucking a single value", [
{
grammar: "start = @'a'",
input: "a",
output: "a"
},
{
grammar: "start = @'a' / @'b'",
input: "a",
output: "a"
},
{
grammar: "start = @'a' / @'b'",
input: "b",
output: "b"
},
{
grammar: "start = 'a' @'b' 'c'",
input: "abc",
output: "b"
},
{
grammar: "start = 'a' ( @'b' 'c' )",
input: "abc",
output: [ "a", "b" ]
},
{
grammar: "start = 'a' @( 'b' @'c' 'd' )",
input: "abcd",
output: "c"
},
{
grammar: "start = 'a' ( @'b' 'c' ) @'d'",
input: "abcd",
output: "d"
},
{
grammar: "start = 'a' @'b' 'c' / 'd' 'e' @'f'",
input: "def",
output: "f"
},
] );
parser( "plucking multiple values", [
{
grammar: "start = 'a' @'b' @'c'",
input: "abc",
output: [ "b", "c" ]
},
{
grammar: "start = 'a' ( @'b' @'c' )",
input: "abc",
output: [ "a", [ "b", "c" ] ]
},
{
grammar: "start = 'a' @( 'b' @'c' @'d' )",
input: "abcd",
output: [ "c", "d" ]
},
{
grammar: "start = 'a' @( @'b' 'c' ) @'d' 'e'",
input: "abcde",
output: [ "b", "d" ]
},
{
grammar: "start = 'a' @'b' 'c' / @'d' 'e' @'f'",
input: "def",
output: [ "d", "f" ]
},
] );
parser( "plucking a value if a predicate doesnt fail", [
{
grammar: "start = @'a' &{ return true; }",
input: "a",
output: "a"
},
{
grammar: "start = @'a' !{ return false; }",
input: "a",
output: "a"
},
{
grammar: "start = @n:[0-9] &{ return n > 0; }",
input: "2",
output: "2"
},
] );
} );
describe( "when any expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = 'a' 'b' 'c'", options );
expect( parser ).to.failToParse( "dbc" );
expect( parser ).to.failToParse( "adc" );
expect( parser ).to.failToParse( "abd" );
} );
it( "resets parse position", function () {
const parser = peg.generate( "start = 'a' 'b' / 'a'", options );
expect( parser ).to.parse( "a", "a" );
} );
} );
} );
describe( "action", function () {
describe( "when the expression matches", function () {
it( "returns the value returned by the code", function () {
const parser = peg.generate( "start = 'a' { return 42; }", options );
expect( parser ).to.parse( "a", 42 );
} );
describe( "label variables", function () {
describe( "in the expression", function () {
it( "can access variable defined by labeled expression", function () {
const parser = peg.generate( "start = a:'a' { return a; }", options );
expect( parser ).to.parse( "a", "a" );
} );
it( "can access variables defined by labeled sequence elements", function () {
const parser = peg.generate(
"start = a:'a' b:'b' c:'c' { return [a, b, c]; }",
options
);
expect( parser ).to.parse( "abc", [ "a", "b", "c" ] );
} );
it( "cannot access variables defined by subexpressions", function () {
const testcases = [
{
grammar: "start = (a:'a') { return a; }",
input: "a"
},
{
grammar: "start = (a:'a')? { return a; }",
input: "a"
},
{
grammar: "start = (a:'a')* { return a; }",
input: "a"
},
{
grammar: "start = (a:'a')+ { return a; }",
input: "a"
},
{
grammar: "start = $(a:'a') { return a; }",
input: "a"
},
{
grammar: "start = &(a:'a') 'a' { return a; }",
input: "a"
},
{
grammar: "start = !(a:'a') 'b' { return a; }",
input: "b"
},
{
grammar: "start = b:(a:'a') { return a; }",
input: "a"
},
{
grammar: "start = ('a' b:'b' 'c') { return b; }",
input: "abc"
},
{
grammar: "start = (a:'a' { return a; }) { return a; }",
input: "a"
},
{
grammar: "start = ('a' / b:'b' / 'c') { return b; }",
input: "b"
}
];
testcases.forEach( testcase => {
const parser = peg.generate( testcase.grammar, options );
expect( parser ).to.failToParse( testcase.input );
} );
} );
} );
describe( "in outer sequence", function () {
it( "can access variables defined by preceding labeled elements", function () {
const parser = peg.generate(
"start = a:'a' ('b' { return a; })",
options
);
expect( parser ).to.parse( "ab", [ "a", "a" ] );
} );
it( "cannot access variable defined by labeled action element", function () {
const parser = peg.generate(
"start = 'a' b:('b' { return b; }) c:'c'",
options
);
expect( parser ).to.failToParse( "abc" );
} );
it( "cannot access variables defined by following labeled elements", function () {
const parser = peg.generate(
"start = ('a' { return b; }) b:'b'",
options
);
expect( parser ).to.failToParse( "ab" );
} );
} );
} );
describe( "initializer variables & functions", function () {
it( "can access variables defined in the initializer", function () {
const parser = peg.generate( [
"{ var v = 42 }",
"start = 'a' { return v; }"
].join( "\n" ), options );
expect( parser ).to.parse( "a", 42 );
} );
it( "can access functions defined in the initializer", function () {
const parser = peg.generate( [
"{ function f() { return 42; } }",
"start = 'a' { return f(); }"
].join( "\n" ), options );
expect( parser ).to.parse( "a", 42 );
} );
} );
describe( "available variables & functions", function () {
it( "|options| contains options", function () {
const parser = peg.generate(
"start = 'a' { return options; }",
options
);
expect( parser ).to.parse( "a", { a: 42 }, { a: 42 } );
} );
it( "|text| returns text matched by the expression", function () {
const parser = peg.generate(
"start = 'a' { return text(); }",
options
);
expect( parser ).to.parse( "a", "a" );
} );
it( "|location| returns location info of the expression", function () {
const parser = peg.generate( [
"{ var result; }",
"start = line (nl+ line)* { return result; }",
"line = thing (' '+ thing)*",
"thing = digit / mark",
"digit = [0-9]",
"mark = 'x' { result = location(); }",
"nl = '\\r'? '\\n'"
].join( "\n" ), options );
expect( parser ).to.parse( "1\n2\n\n3\n\n\n4 5 x", {
start: { offset: 13, line: 7, column: 5 },
end: { offset: 14, line: 7, column: 6 }
} );
// Newline representations
expect( parser ).to.parse( "1\nx", { // Unix
start: { offset: 2, line: 2, column: 1 },
end: { offset: 3, line: 2, column: 2 }
} );
expect( parser ).to.parse( "1\r\nx", { // Windows
start: { offset: 3, line: 2, column: 1 },
end: { offset: 4, line: 2, column: 2 }
} );
} );
describe( "|expected|", function () {
it( "terminates parsing and throws an exception", function () {
const parser = peg.generate(
"start = 'a' { expected('a'); }",
options
);
expect( parser ).to.failToParse( "a", {
message: "Expected a but \"a\" found.",
expected: [ { type: "other", description: "a" } ],
found: "a",
location: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 1, line: 1, column: 2 }
}
} );
} );
it( "allows to set custom location info", function () {
const parser = peg.generate( [
"start = 'a' {",
" expected('a', {",
" start: { offset: 1, line: 1, column: 2 },",
" end: { offset: 2, line: 1, column: 3 }",
" });",
"}"
].join( "\n" ), options );
expect( parser ).to.failToParse( "a", {
message: "Expected a but \"a\" found.",
expected: [ { type: "other", description: "a" } ],
found: "a",
location: {
start: { offset: 1, line: 1, column: 2 },
end: { offset: 2, line: 1, column: 3 }
}
} );
} );
} );
describe( "|error|", function () {
it( "terminates parsing and throws an exception", function () {
const parser = peg.generate(
"start = 'a' { error('a'); }",
options
);
expect( parser ).to.failToParse( "a", {
message: "a",
found: null,
expected: null,
location: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 1, line: 1, column: 2 }
}
} );
} );
it( "allows to set custom location info", function () {
const parser = peg.generate( [
"start = 'a' {",
" error('a', {",
" start: { offset: 1, line: 1, column: 2 },",
" end: { offset: 2, line: 1, column: 3 }",
" });",
"}"
].join( "\n" ), options );
expect( parser ).to.failToParse( "a", {
message: "a",
expected: null,
found: null,
location: {
start: { offset: 1, line: 1, column: 2 },
end: { offset: 2, line: 1, column: 3 }
}
} );
} );
} );
} );
} );
describe( "when the expression doesn't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = 'a' { return 42; }", options );
expect( parser ).to.failToParse( "b" );
} );
it( "doesn't execute the code", function () {
const parser = peg.generate(
"start = 'a' { throw 'Boom!'; } / 'b'",
options
);
expect( parser ).to.parse( "b" );
} );
} );
} );
describe( "choice", function () {
describe( "when any expression matches", function () {
it( "returns its match result", function () {
const parser = peg.generate( "start = 'a' / 'b' / 'c'", options );
expect( parser ).to.parse( "a", "a" );
expect( parser ).to.parse( "b", "b" );
expect( parser ).to.parse( "c", "c" );
} );
} );
describe( "when all expressions don't match", function () {
it( "reports match failure", function () {
const parser = peg.generate( "start = 'a' / 'b' / 'c'", options );
expect( parser ).to.failToParse( "d" );
} );
} );
} );
describe( "error reporting", function () {
describe( "behavior", function () {
it( "reports only the rightmost error", function () {
const parser = peg.generate( "start = 'a' 'b' / 'a' 'c' 'd'", options );
expect( parser ).to.failToParse( "ace", {
expected: [ { type: "literal", text: "d", ignoreCase: false } ]
} );
} );
} );
describe( "expectations reporting", function () {
it( "reports expectations correctly with no alternative", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "ab", {
expected: [ { type: "end" } ]
} );
} );
it( "reports expectations correctly with one alternative", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", {
expected: [ { type: "literal", text: "a", ignoreCase: false } ]
} );
} );
it( "reports expectations correctly with multiple alternatives", function () {
const parser = peg.generate( "start = 'a' / 'b' / 'c'", options );
expect( parser ).to.failToParse( "d", {
expected: [
{ type: "literal", text: "a", ignoreCase: false },
{ type: "literal", text: "b", ignoreCase: false },
{ type: "literal", text: "c", ignoreCase: false }
]
} );
} );
it( "reports expectations correctly with look ahead", function () {
// Case 1, by https://github.com/dmajda
let parser = peg.generate( [
"Statement = '{' __ !Statement Statement __ '}'",
"__ = [ ]*"
].join( "\n" ), options );
expect( parser ).to.failToParse( "{x}", {
expected: [
{ type: "class", parts: [ " " ], inverted: false, ignoreCase: false },
{ type: "not", expected: { type: "literal", text: "{", ignoreCase: false } },
{ type: "literal", text: "{", ignoreCase: false }
]
} );
// Case 2, by https://github.com/nikku
parser = peg.generate( [
"Start = Char+ End",
"End = 'e'",
"Char = !End [a-z]"
].join( "\n" ), options );
expect( parser ).to.failToParse( "a", {
expected: [
{ type: "class", parts: [ [ "a", "z" ] ], inverted: false, ignoreCase: false },
{ type: "literal", text: "e", ignoreCase: false }
]
} );
} );
it( "reports expectations correctly with look ahead + grouped names", function () {
const parser = peg.generate( [
"Start = Char+ End",
"End 'end' = 'e'",
"Char = !End [a-z]"
].join( "\n" ), options );
expect( parser ).to.failToParse( "a", {
expected: [
{ type: "class", parts: [ [ "a", "z" ] ], inverted: false, ignoreCase: false },
{ type: "other", description: "end" }
]
} );
} );
} );
describe( "found string reporting", function () {
it( "reports found string correctly at the end of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "", { found: null } );
} );
it( "reports found string correctly in the middle of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", { found: "b" } );
} );
} );
describe( "message building", function () {
it( "builds message correctly with no alternative", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "ab", {
message: "Expected end of input but \"b\" found."
} );
} );
it( "builds message correctly with one alternative", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", {
message: "Expected \"a\" but \"b\" found."
} );
} );
it( "builds message correctly with multiple alternatives", function () {
const parser = peg.generate( "start = 'a' / 'b' / 'c'", options );
expect( parser ).to.failToParse( "d", {
message: "Expected \"a\", \"b\", or \"c\" but \"d\" found."
} );
} );
it( "builds message correctly at the end of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "", {
message: "Expected \"a\" but end of input found."
} );
} );
it( "builds message correctly in the middle of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", {
message: "Expected \"a\" but \"b\" found."
} );
} );
it( "removes duplicates from expectations", function () {
const parser = peg.generate( "start = 'a' / 'a'", options );
expect( parser ).to.failToParse( "b", {
message: "Expected \"a\" but \"b\" found."
} );
} );
it( "sorts expectations", function () {
const parser = peg.generate( "start = 'c' / 'b' / 'a'", options );
expect( parser ).to.failToParse( "d", {
message: "Expected \"a\", \"b\", or \"c\" but \"d\" found."
} );
} );
} );
describe( "position reporting", function () {
it( "reports position correctly at the end of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "", {
location: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 0, line: 1, column: 1 }
}
} );
} );
it( "reports position correctly in the middle of input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "b", {
location: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 1, line: 1, column: 2 }
}
} );
} );
it( "reports position correctly with trailing input", function () {
const parser = peg.generate( "start = 'a'", options );
expect( parser ).to.failToParse( "aa", {
location: {
start: { offset: 1, line: 1, column: 2 },
end: { offset: 2, line: 1, column: 3 }
}
} );
} );
it( "reports position correctly in complex cases", function () {
const parser = peg.generate( [
"start = line (nl+ line)*",
"line = digit (' '+ digit)*",
"digit = [0-9]",
"nl = '\\r'? '\\n'"
].join( "\n" ), options );
expect( parser ).to.failToParse( "1\n2\n\n3\n\n\n4 5 x", {
location: {
start: { offset: 13, line: 7, column: 5 },
end: { offset: 14, line: 7, column: 6 }
}
} );
// Newline representations
expect( parser ).to.failToParse( "1\nx", { // Old Mac
location: {
start: { offset: 2, line: 2, column: 1 },
end: { offset: 3, line: 2, column: 2 }
}
} );
expect( parser ).to.failToParse( "1\r\nx", { // Windows
location: {
start: { offset: 3, line: 2, column: 1 },
end: { offset: 4, line: 2, column: 2 }
}
} );
} );
} );
} );
// Following examples are from Wikipedia, see
// http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938.
describe( "complex examples", function () {
it( "handles arithmetics example correctly", function () {
// Value ← [0-9]+ / '(' Expr ')'
// Product ← Value (('*' / '/') Value)*
// Sum ← Product (('+' / '-') Product)*
// Expr ← Sum
const parser = peg.generate( [
"Expr = Sum",
"Sum = head:Product tail:(('+' / '-') Product)* {",
" return tail.reduce(function(result, element) {",
" if (element[0] === '+') { return result + element[1]; }",
" if (element[0] === '-') { return result - element[1]; }",
" }, head);",
" }",
"Product = head:Value tail:(('*' / '/') Value)* {",
" return tail.reduce(function(result, element) {",
" if (element[0] === '*') { return result * element[1]; }",
" if (element[0] === '/') { return result / element[1]; }",
" }, head);",
" }",
"Value = digits:[0-9]+ { return parseInt(digits.join(''), 10); }",
" / '(' expr:Expr ')' { return expr; }"
].join( "\n" ), options );
// The "value" rule
expect( parser ).to.parse( "0", 0 );
expect( parser ).to.parse( "123", 123 );
expect( parser ).to.parse( "(42+43)", 42 + 43 );
// The "product" rule
expect( parser ).to.parse( "42", 42 );
expect( parser ).to.parse( "42*43", 42 * 43 );
expect( parser ).to.parse( "42*43*44*45", 42 * 43 * 44 * 45 );
expect( parser ).to.parse( "42/43", 42 / 43 );
expect( parser ).to.parse( "42/43/44/45", 42 / 43 / 44 / 45 );
// The "sum" rule
expect( parser ).to.parse( "42*43", 42 * 43 );
expect( parser ).to.parse( "42*43+44*45", 42 * 43 + 44 * 45 );
expect( parser ).to.parse( "42*43+44*45+46*47+48*49", 42 * 43 + 44 * 45 + 46 * 47 + 48 * 49 );
expect( parser ).to.parse( "42*43-44*45", 42 * 43 - 44 * 45 );
expect( parser ).to.parse( "42*43-44*45-46*47-48*49", 42 * 43 - 44 * 45 - 46 * 47 - 48 * 49 );
// The "expr" rule
expect( parser ).to.parse( "42+43", 42 + 43 );
// Complex test
expect( parser ).to.parse( "(1+2)*(3+4)", ( 1 + 2 ) * ( 3 + 4 ) );
} );
it( "handles non-context-free language correctly", function () {
// The following parsing expression grammar describes the classic
// non-context-free language { a^n b^n c^n : n >= 1 }:
//
// S ← &(A c) a+ B !(a/b/c)
// A ← a A? b
// B ← b B? c
const parser = peg.generate( [
"S = &(A 'c') a:'a'+ B:B !('a' / 'b' / 'c') { return a.join('') + B; }",
"A = a:'a' A:A? b:'b' { return [a, A, b].join(''); }",
"B = b:'b' B:B? c:'c' { return [b, B, c].join(''); }"
].join( "\n" ), options );
expect( parser ).to.parse( "abc", "abc" );
expect( parser ).to.parse( "aaabbbccc", "aaabbbccc" );
expect( parser ).to.failToParse( "aabbbccc" );
expect( parser ).to.failToParse( "aaaabbbccc" );
expect( parser ).to.failToParse( "aaabbccc" );
expect( parser ).to.failToParse( "aaabbbbccc" );
expect( parser ).to.failToParse( "aaabbbcc" );
expect( parser ).to.failToParse( "aaabbbcccc" );
} );
it( "handles nested comments example correctly", function () {
// Begin ← "(*"
// End ← "*)"
// C ← Begin N* End
// N ← C / (!Begin !End Z)
// Z ← any single character
const parser = peg.generate( [
"C = begin:Begin ns:N* end:End { return begin + ns.join('') + end; }",
"N = C",
" / !Begin !End z:Z { return z; }",
"Z = .",
"Begin = '(*'",
"End = '*)'"
].join( "\n" ), options );
expect( parser ).to.parse( "(**)", "(**)" );
expect( parser ).to.parse( "(*abc*)", "(*abc*)" );
expect( parser ).to.parse( "(*(**)*)", "(*(**)*)" );
expect( parser ).to.parse(
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)",
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)"
);
} );
} );
} );
} );