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.
1571 lines
51 KiB
JavaScript
1571 lines
51 KiB
JavaScript
"use strict";
|
|
|
|
/* eslint-disable no-console */
|
|
/* global console */
|
|
|
|
let peg = require("../../lib/peg");
|
|
|
|
describe("generated parser behavior", function() {
|
|
function varyOptimizationOptions(block) {
|
|
function clone(object) {
|
|
let result = {};
|
|
|
|
Object.keys(object).forEach(key => {
|
|
result[key] = object[key];
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
let 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 " + jasmine.pp(variant),
|
|
function() { block(clone(variant)); }
|
|
);
|
|
});
|
|
}
|
|
|
|
beforeEach(function() {
|
|
this.addMatchers({
|
|
toParse(input, expected, options) {
|
|
options = options !== undefined ? options : {};
|
|
|
|
let result;
|
|
|
|
try {
|
|
result = this.actual.parse(input, options);
|
|
} catch (e) {
|
|
this.message = () =>
|
|
"Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to parse" + (expected !== undefined ? " as " + jasmine.pp(expected) : "") + ", "
|
|
+ "but it failed to parse with message "
|
|
+ jasmine.pp(e.message) + ".";
|
|
|
|
return false;
|
|
}
|
|
|
|
if (expected !== undefined) {
|
|
this.message = () =>
|
|
"Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ (this.isNot ? "not " : "")
|
|
+ "to parse as " + jasmine.pp(expected) + ", "
|
|
+ "but it parsed as " + jasmine.pp(result) + ".";
|
|
|
|
return this.env.equals_(result, expected);
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
|
|
toFailToParse(input, details, options) {
|
|
options = options !== undefined ? options : {};
|
|
|
|
let result;
|
|
|
|
try {
|
|
result = this.actual.parse(input, options);
|
|
} catch (e) {
|
|
if (this.isNot) {
|
|
this.message = () =>
|
|
"Expected " + jasmine.pp(input)
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to parse, "
|
|
+ "but it failed with message "
|
|
+ jasmine.pp(e.message) + ".";
|
|
} else {
|
|
if (details) {
|
|
let keys = Object.keys(details);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
let key = keys[i];
|
|
|
|
if (!this.env.equals_(e[key], details[key])) {
|
|
this.message = () =>
|
|
"Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to fail to parse"
|
|
+ (details ? " with details " + jasmine.pp(details) : "") + ", "
|
|
+ "but " + jasmine.pp(key) + " "
|
|
+ "is " + jasmine.pp(e[key]) + ".";
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
this.message = () =>
|
|
"Expected " + jasmine.pp(input) + " "
|
|
+ "with options " + jasmine.pp(options) + " "
|
|
+ "to fail to parse"
|
|
+ (details ? " with details " + jasmine.pp(details) : "") + ", "
|
|
+ "but it parsed as " + jasmine.pp(result) + ".";
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Stub out |console.log| so that the default tracer doesn't clutter
|
|
// test output.
|
|
if (typeof console === "object") {
|
|
spyOn(console, "log");
|
|
}
|
|
});
|
|
|
|
varyOptimizationOptions(function(options) {
|
|
describe("initializer", function() {
|
|
it("executes the code before parsing starts", function() {
|
|
let parser = peg.generate([
|
|
"{ var result = 42; }",
|
|
"start = 'a' { return result; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
describe("available variables and functions", function() {
|
|
it("|options| contains options", function() {
|
|
let parser = peg.generate([
|
|
"{ var result = options; }",
|
|
"start = 'a' { return result; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("rule", function() {
|
|
if (options.cache) {
|
|
it("caches rule match results", function() {
|
|
let parser = peg.generate([
|
|
"{ var n = 0; }",
|
|
"start = (a 'b') / (a 'c') { return n; }",
|
|
"a = 'a' { n++; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("ac", 1);
|
|
});
|
|
} else {
|
|
it("doesn't cache rule match results", function() {
|
|
let parser = peg.generate([
|
|
"{ var n = 0; }",
|
|
"start = (a 'b') / (a 'c') { return n; }",
|
|
"a = 'a' { n++; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("ac", 2);
|
|
});
|
|
}
|
|
|
|
describe("when the expression matches", function() {
|
|
it("returns its match result", function() {
|
|
let parser = peg.generate("start = 'a'");
|
|
|
|
expect(parser).toParse("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() {
|
|
let parser = peg.generate("start = 'a'");
|
|
|
|
expect(parser).toFailToParse("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() {
|
|
let parser = peg.generate("start 'start' = 'a'");
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [{ type: "other", description: "start" }]
|
|
});
|
|
});
|
|
|
|
it("discards any expectations recorded when matching the expression", function() {
|
|
let parser = peg.generate("start 'start' = 'a'");
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [{ type: "other", description: "start" }]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("literal", function() {
|
|
describe("matching", function() {
|
|
it("matches empty literals", function() {
|
|
let parser = peg.generate("start = ''", options);
|
|
|
|
expect(parser).toParse("");
|
|
});
|
|
|
|
it("matches one-character literals", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("matches multi-character literals", function() {
|
|
let parser = peg.generate("start = 'abcd'", options);
|
|
|
|
expect(parser).toParse("abcd");
|
|
expect(parser).toFailToParse("efgh");
|
|
});
|
|
|
|
it("is case sensitive without the \"i\" flag", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toFailToParse("A");
|
|
});
|
|
|
|
it("is case insensitive with the \"i\" flag", function() {
|
|
let parser = peg.generate("start = 'a'i", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toParse("A");
|
|
});
|
|
});
|
|
|
|
describe("when it matches", function() {
|
|
it("returns the matched text", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("consumes the matched text", function() {
|
|
let parser = peg.generate("start = 'a' .", options);
|
|
|
|
expect(parser).toParse("ab");
|
|
});
|
|
});
|
|
|
|
describe("when it doesn't match", function() {
|
|
it("reports match failure and records an expectation of type \"literal\"", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [{ type: "literal", text: "a", ignoreCase: false }]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("character class", function() {
|
|
describe("matching", function() {
|
|
it("matches empty classes", function() {
|
|
let parser = peg.generate("start = []", options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("matches classes with a character list", function() {
|
|
let parser = peg.generate("start = [abc]", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toParse("b");
|
|
expect(parser).toParse("c");
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
|
|
it("matches classes with a character range", function() {
|
|
let parser = peg.generate("start = [a-c]", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toParse("b");
|
|
expect(parser).toParse("c");
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
|
|
it("matches inverted classes", function() {
|
|
let parser = peg.generate("start = [^a]", options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
expect(parser).toParse("b");
|
|
});
|
|
|
|
it("is case sensitive without the \"i\" flag", function() {
|
|
let parser = peg.generate("start = [a]", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toFailToParse("A");
|
|
});
|
|
|
|
it("is case insensitive with the \"i\" flag", function() {
|
|
let parser = peg.generate("start = [a]i", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toParse("A");
|
|
});
|
|
});
|
|
|
|
describe("when it matches", function() {
|
|
it("returns the matched character", function() {
|
|
let parser = peg.generate("start = [a]", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("consumes the matched character", function() {
|
|
let parser = peg.generate("start = [a] .", options);
|
|
|
|
expect(parser).toParse("ab");
|
|
});
|
|
});
|
|
|
|
describe("when it doesn't match", function() {
|
|
it("reports match failure and records an expectation of type \"class\"", function() {
|
|
let parser = peg.generate("start = [a]", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [{ type: "class", parts: ["a"], inverted: false, ignoreCase: false }]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("dot", function() {
|
|
describe("matching", function() {
|
|
it("matches any character", function() {
|
|
let parser = peg.generate("start = .", options);
|
|
|
|
expect(parser).toParse("a");
|
|
expect(parser).toParse("b");
|
|
expect(parser).toParse("c");
|
|
});
|
|
});
|
|
|
|
describe("when it matches", function() {
|
|
it("returns the matched character", function() {
|
|
let parser = peg.generate("start = .", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("consumes the matched character", function() {
|
|
let parser = peg.generate("start = . .", options);
|
|
|
|
expect(parser).toParse("ab");
|
|
});
|
|
});
|
|
|
|
describe("when it doesn't match", function() {
|
|
it("reports match failure and records an expectation of type \"any\"", function() {
|
|
let parser = peg.generate("start = .", options);
|
|
|
|
expect(parser).toFailToParse("", {
|
|
expected: [{ type: "any" }]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("rule reference", function() {
|
|
describe("when referenced rule's expression matches", function() {
|
|
it("returns its result", function() {
|
|
let parser = peg.generate([
|
|
"start = a",
|
|
"a = 'a'"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("when referenced rule's expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate([
|
|
"start = a",
|
|
"a = 'a'"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toFailToParse("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.
|
|
let parser = peg.generate("start = &{ return true; } ''", options);
|
|
|
|
expect(parser).toParse("", [undefined, ""]);
|
|
});
|
|
});
|
|
|
|
describe("when the code returns a falsey value", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = &{ return false; }", options);
|
|
|
|
expect(parser).toFailToParse("");
|
|
});
|
|
});
|
|
|
|
describe("label variables", function() {
|
|
describe("in containing sequence", function() {
|
|
it("can access variables defined by preceding labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' &{ return a === 'a'; }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a");
|
|
});
|
|
|
|
it("cannot access variable defined by labeled predicate element", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' b:&{ return b === undefined; } 'c'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("ac");
|
|
});
|
|
|
|
it("cannot access variables defined by following labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = &{ return a === 'a'; } a:'a'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("cannot access variables defined by subexpressions", function() {
|
|
let 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 => {
|
|
let parser = peg.generate(testcase.grammar, options);
|
|
expect(parser).toFailToParse(testcase.input);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("in outer sequence", function() {
|
|
it("can access variables defined by preceding labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' ('b' &{ return a === 'a'; })",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("ab");
|
|
});
|
|
|
|
it("cannot access variable defined by labeled predicate element", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' b:('b' &{ return b === undefined; }) 'c'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("abc");
|
|
});
|
|
|
|
it("cannot access variables defined by following labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = ('a' &{ return b === 'b'; }) b:'b'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("ab");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("initializer variables & functions", function() {
|
|
it("can access variables defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ var v = 42 }",
|
|
"start = &{ return v === 42; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("");
|
|
});
|
|
|
|
it("can access functions defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ function f() { return 42; } }",
|
|
"start = &{ return f() === 42; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("");
|
|
});
|
|
});
|
|
|
|
describe("available variables & functions", function() {
|
|
it("|options| contains options", function() {
|
|
let parser = peg.generate([
|
|
"{ var result; }",
|
|
"start = &{ result = options; return true; } { return result; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("", { a: 42 }, { a: 42 });
|
|
});
|
|
|
|
it("|location| returns current location info", function() {
|
|
let 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).toParse("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).toParse("1\nx", { // Unix
|
|
start: { offset: 2, line: 2, column: 1 },
|
|
end: { offset: 2, line: 2, column: 1 }
|
|
});
|
|
expect(parser).toParse("1\r\nx", { // Windows
|
|
start: { offset: 3, line: 2, column: 1 },
|
|
end: { offset: 3, line: 2, column: 1 }
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
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.
|
|
let parser = peg.generate("start = !{ return false; } ''", options);
|
|
|
|
expect(parser).toParse("", [undefined, ""]);
|
|
});
|
|
});
|
|
|
|
describe("when the code returns a truthy value", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = !{ return true; }", options);
|
|
|
|
expect(parser).toFailToParse("");
|
|
});
|
|
});
|
|
|
|
describe("label variables", function() {
|
|
describe("in containing sequence", function() {
|
|
it("can access variables defined by preceding labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' !{ return a !== 'a'; }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a");
|
|
});
|
|
|
|
it("cannot access variable defined by labeled predicate element", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' b:!{ return b !== undefined; } 'c'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("ac");
|
|
});
|
|
|
|
it("cannot access variables defined by following labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = !{ return a !== 'a'; } a:'a'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
|
|
it("cannot access variables defined by subexpressions", function() {
|
|
let 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 => {
|
|
let parser = peg.generate(testcase.grammar, options);
|
|
expect(parser).toFailToParse(testcase.input);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("in outer sequence", function() {
|
|
it("can access variables defined by preceding labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' ('b' !{ return a !== 'a'; })",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("ab");
|
|
});
|
|
|
|
it("cannot access variable defined by labeled predicate element", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' b:('b' !{ return b !== undefined; }) 'c'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("abc");
|
|
});
|
|
|
|
it("cannot access variables defined by following labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = ('a' !{ return b !== 'b'; }) b:'b'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("ab");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("initializer variables & functions", function() {
|
|
it("can access variables defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ var v = 42 }",
|
|
"start = !{ return v !== 42; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("");
|
|
});
|
|
|
|
it("can access functions defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ function f() { return 42; } }",
|
|
"start = !{ return f() !== 42; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("");
|
|
});
|
|
});
|
|
|
|
describe("available variables & functions", function() {
|
|
it("|options| contains options", function() {
|
|
let parser = peg.generate([
|
|
"{ var result; }",
|
|
"start = !{ result = options; return false; } { return result; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("", { a: 42 }, { a: 42 });
|
|
});
|
|
|
|
it("|location| returns current location info", function() {
|
|
let 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).toParse("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).toParse("1\nx", { // Unix
|
|
start: { offset: 2, line: 2, column: 1 },
|
|
end: { offset: 2, line: 2, column: 1 }
|
|
});
|
|
expect(parser).toParse("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() {
|
|
let parser = peg.generate("start = ('a')", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = ('a')", options);
|
|
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("optional", function() {
|
|
describe("when the expression matches", function() {
|
|
it("returns its match result", function() {
|
|
let parser = peg.generate("start = 'a'?", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("returns |null|", function() {
|
|
let parser = peg.generate("start = 'a'?", options);
|
|
|
|
expect(parser).toParse("", 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() {
|
|
let parser = peg.generate("start = 'a'*", options);
|
|
|
|
expect(parser).toParse("", []);
|
|
expect(parser).toParse("a", ["a"]);
|
|
expect(parser).toParse("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() {
|
|
let parser = peg.generate("start = 'a'+", options);
|
|
|
|
expect(parser).toParse("a", ["a"]);
|
|
expect(parser).toParse("aaa", ["a", "a", "a"]);
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = 'a'+", options);
|
|
|
|
expect(parser).toFailToParse("");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("text", function() {
|
|
describe("when the expression matches", function() {
|
|
it("returns the matched text", function() {
|
|
let parser = peg.generate("start = $('a' 'b' 'c')", options);
|
|
|
|
expect(parser).toParse("abc", "abc");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = $('a')", options);
|
|
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("positive simple predicate", function() {
|
|
describe("when the expression matches", function() {
|
|
it("returns |undefined|", function() {
|
|
let parser = peg.generate("start = &'a' 'a'", options);
|
|
|
|
expect(parser).toParse("a", [undefined, "a"]);
|
|
});
|
|
|
|
it("resets parse position", function() {
|
|
let parser = peg.generate("start = &'a' 'a'", options);
|
|
|
|
expect(parser).toParse("a");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = &'a'", options);
|
|
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("discards any expectations recorded when matching the expression", function() {
|
|
let parser = peg.generate("start = 'a' / &'b' / 'c'", options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
expected: [
|
|
{ type: "literal", text: "a", ignoreCase: false },
|
|
{ type: "literal", text: "c", ignoreCase: false }
|
|
]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("negative simple predicate", function() {
|
|
describe("when the expression matches", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = !'a'", options);
|
|
|
|
expect(parser).toFailToParse("a");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("returns |undefined|", function() {
|
|
let parser = peg.generate("start = !'a' 'b'", options);
|
|
|
|
expect(parser).toParse("b", [undefined, "b"]);
|
|
});
|
|
|
|
it("resets parse position", function() {
|
|
let parser = peg.generate("start = !'a' 'b'", options);
|
|
|
|
expect(parser).toParse("b");
|
|
});
|
|
|
|
it("discards any expectations recorded when matching the expression", function() {
|
|
let parser = peg.generate("start = 'a' / !'b' / 'c'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [
|
|
{ type: "literal", text: "a", ignoreCase: false },
|
|
{ type: "literal", text: "c", ignoreCase: false }
|
|
]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("label", function() {
|
|
describe("when the expression matches", function() {
|
|
it("returns its match result", function() {
|
|
let parser = peg.generate("start = a:'a'", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
|
|
describe("when the expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = a:'a'", options);
|
|
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sequence", function() {
|
|
describe("when all expressions match", function() {
|
|
it("returns an array of their match results", function() {
|
|
let parser = peg.generate("start = 'a' 'b' 'c'", options);
|
|
|
|
expect(parser).toParse("abc", ["a", "b", "c"]);
|
|
});
|
|
});
|
|
|
|
describe("when any expression doesn't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = 'a' 'b' 'c'", options);
|
|
|
|
expect(parser).toFailToParse("dbc");
|
|
expect(parser).toFailToParse("adc");
|
|
expect(parser).toFailToParse("abd");
|
|
});
|
|
|
|
it("resets parse position", function() {
|
|
let parser = peg.generate("start = 'a' 'b' / 'a'", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("action", function() {
|
|
describe("when the expression matches", function() {
|
|
it("returns the value returned by the code", function() {
|
|
let parser = peg.generate("start = 'a' { return 42; }", options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
describe("label variables", function() {
|
|
describe("in the expression", function() {
|
|
it("can access variable defined by labeled expression", function() {
|
|
let parser = peg.generate("start = a:'a' { return a; }", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("can access variables defined by labeled sequence elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' b:'b' c:'c' { return [a, b, c]; }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("abc", ["a", "b", "c"]);
|
|
});
|
|
|
|
it("cannot access variables defined by subexpressions", function() {
|
|
let 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 => {
|
|
let parser = peg.generate(testcase.grammar, options);
|
|
expect(parser).toFailToParse(testcase.input);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("in outer sequence", function() {
|
|
it("can access variables defined by preceding labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = a:'a' ('b' { return a; })",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("ab", ["a", "a"]);
|
|
});
|
|
|
|
it("cannot access variable defined by labeled action element", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' b:('b' { return b; }) c:'c'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("abc");
|
|
});
|
|
|
|
it("cannot access variables defined by following labeled elements", function() {
|
|
let parser = peg.generate(
|
|
"start = ('a' { return b; }) b:'b'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("ab");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("initializer variables & functions", function() {
|
|
it("can access variables defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ var v = 42 }",
|
|
"start = 'a' { return v; }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
|
|
it("can access functions defined in the initializer", function() {
|
|
let parser = peg.generate([
|
|
"{ function f() { return 42; } }",
|
|
"start = 'a' { return f(); }"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toParse("a", 42);
|
|
});
|
|
});
|
|
|
|
describe("available variables & functions", function() {
|
|
it("|options| contains options", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' { return options; }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", { a: 42 }, { a: 42 });
|
|
});
|
|
|
|
it("|text| returns text matched by the expression", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' { return text(); }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
});
|
|
|
|
it("|location| returns location info of the expression", function() {
|
|
let 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).toParse("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).toParse("1\nx", { // Unix
|
|
start: { offset: 2, line: 2, column: 1 },
|
|
end: { offset: 3, line: 2, column: 2 }
|
|
});
|
|
expect(parser).toParse("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() {
|
|
let parser = peg.generate(
|
|
"start = 'a' { expected('a'); }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("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() {
|
|
let 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).toFailToParse("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() {
|
|
let parser = peg.generate(
|
|
"start = 'a' { error('a'); }",
|
|
options
|
|
);
|
|
|
|
expect(parser).toFailToParse("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() {
|
|
let 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).toFailToParse("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() {
|
|
let parser = peg.generate("start = 'a' { return 42; }", options);
|
|
|
|
expect(parser).toFailToParse("b");
|
|
});
|
|
|
|
it("doesn't execute the code", function() {
|
|
let parser = peg.generate(
|
|
"start = 'a' { throw 'Boom!'; } / 'b'",
|
|
options
|
|
);
|
|
|
|
expect(parser).toParse("b");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("choice", function() {
|
|
describe("when any expression matches", function() {
|
|
it("returns its match result", function() {
|
|
let parser = peg.generate("start = 'a' / 'b' / 'c'", options);
|
|
|
|
expect(parser).toParse("a", "a");
|
|
expect(parser).toParse("b", "b");
|
|
expect(parser).toParse("c", "c");
|
|
});
|
|
});
|
|
|
|
describe("when all expressions don't match", function() {
|
|
it("reports match failure", function() {
|
|
let parser = peg.generate("start = 'a' / 'b' / 'c'", options);
|
|
|
|
expect(parser).toFailToParse("d");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("error reporting", function() {
|
|
describe("behavior", function() {
|
|
it("reports only the rightmost error", function() {
|
|
let parser = peg.generate("start = 'a' 'b' / 'a' 'c' 'd'", options);
|
|
|
|
expect(parser).toFailToParse("ace", {
|
|
expected: [{ type: "literal", text: "d", ignoreCase: false }]
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("expectations reporting", function() {
|
|
it("reports expectations correctly with no alternative", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("ab", {
|
|
expected: [{ type: "end" }]
|
|
});
|
|
});
|
|
|
|
it("reports expectations correctly with one alternative", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
expected: [{ type: "literal", text: "a", ignoreCase: false }]
|
|
});
|
|
});
|
|
|
|
it("reports expectations correctly with multiple alternatives", function() {
|
|
let parser = peg.generate("start = 'a' / 'b' / 'c'", options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
expected: [
|
|
{ type: "literal", text: "a", ignoreCase: false },
|
|
{ type: "literal", text: "b", ignoreCase: false },
|
|
{ type: "literal", text: "c", ignoreCase: false }
|
|
]
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("found string reporting", function() {
|
|
it("reports found string correctly at the end of input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("", { found: null });
|
|
});
|
|
|
|
it("reports found string correctly in the middle of input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", { found: "b" });
|
|
});
|
|
});
|
|
|
|
describe("message building", function() {
|
|
it("builds message correctly with no alternative", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("ab", {
|
|
message: "Expected end of input but \"b\" found."
|
|
});
|
|
});
|
|
|
|
it("builds message correctly with one alternative", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
message: "Expected \"a\" but \"b\" found."
|
|
});
|
|
});
|
|
|
|
it("builds message correctly with multiple alternatives", function() {
|
|
let parser = peg.generate("start = 'a' / 'b' / 'c'", options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
message: "Expected \"a\", \"b\", or \"c\" but \"d\" found."
|
|
});
|
|
});
|
|
|
|
it("builds message correctly at the end of input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("", {
|
|
message: "Expected \"a\" but end of input found."
|
|
});
|
|
});
|
|
|
|
it("builds message correctly in the middle of input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
message: "Expected \"a\" but \"b\" found."
|
|
});
|
|
});
|
|
|
|
it("removes duplicates from expectations", function() {
|
|
let parser = peg.generate("start = 'a' / 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
message: "Expected \"a\" but \"b\" found."
|
|
});
|
|
});
|
|
|
|
it("sorts expectations", function() {
|
|
let parser = peg.generate("start = 'c' / 'b' / 'a'", options);
|
|
|
|
expect(parser).toFailToParse("d", {
|
|
message: "Expected \"a\", \"b\", or \"c\" but \"d\" found."
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("position reporting", function() {
|
|
it("reports position correctly at the end of input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("", {
|
|
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() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("b", {
|
|
location: {
|
|
start: { offset: 0, line: 1, column: 1 },
|
|
end: { offset: 1, line: 1, column: 2 }
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reports position correctly with trailing input", function() {
|
|
let parser = peg.generate("start = 'a'", options);
|
|
|
|
expect(parser).toFailToParse("aa", {
|
|
location: {
|
|
start: { offset: 1, line: 1, column: 2 },
|
|
end: { offset: 2, line: 1, column: 3 }
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reports position correctly in complex cases", function() {
|
|
let parser = peg.generate([
|
|
"start = line (nl+ line)*",
|
|
"line = digit (' '+ digit)*",
|
|
"digit = [0-9]",
|
|
"nl = '\\r'? '\\n'"
|
|
].join("\n"), options);
|
|
|
|
expect(parser).toFailToParse("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).toFailToParse("1\nx", { // Old Mac
|
|
location: {
|
|
start: { offset: 2, line: 2, column: 1 },
|
|
end: { offset: 3, line: 2, column: 2 }
|
|
}
|
|
});
|
|
expect(parser).toFailToParse("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
|
|
let 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).toParse("0", 0);
|
|
expect(parser).toParse("123", 123);
|
|
expect(parser).toParse("(42+43)", 42+43);
|
|
|
|
// The "product" rule
|
|
expect(parser).toParse("42", 42);
|
|
expect(parser).toParse("42*43", 42*43);
|
|
expect(parser).toParse("42*43*44*45", 42*43*44*45);
|
|
expect(parser).toParse("42/43", 42/43);
|
|
expect(parser).toParse("42/43/44/45", 42/43/44/45);
|
|
|
|
// The "sum" rule
|
|
expect(parser).toParse("42*43", 42*43);
|
|
expect(parser).toParse("42*43+44*45", 42*43+44*45);
|
|
expect(parser).toParse("42*43+44*45+46*47+48*49", 42*43+44*45+46*47+48*49);
|
|
expect(parser).toParse("42*43-44*45", 42*43-44*45);
|
|
expect(parser).toParse("42*43-44*45-46*47-48*49", 42*43-44*45-46*47-48*49);
|
|
|
|
// The "expr" rule
|
|
expect(parser).toParse("42+43", 42+43);
|
|
|
|
// Complex test
|
|
expect(parser).toParse("(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
|
|
let 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).toParse("abc", "abc");
|
|
expect(parser).toParse("aaabbbccc", "aaabbbccc");
|
|
expect(parser).toFailToParse("aabbbccc");
|
|
expect(parser).toFailToParse("aaaabbbccc");
|
|
expect(parser).toFailToParse("aaabbccc");
|
|
expect(parser).toFailToParse("aaabbbbccc");
|
|
expect(parser).toFailToParse("aaabbbcc");
|
|
expect(parser).toFailToParse("aaabbbcccc");
|
|
});
|
|
|
|
it("handles nested comments example correctly", function() {
|
|
// Begin ← "(*"
|
|
// End ← "*)"
|
|
// C ← Begin N* End
|
|
// N ← C / (!Begin !End Z)
|
|
// Z ← any single character
|
|
let 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).toParse("(**)", "(**)");
|
|
expect(parser).toParse("(*abc*)", "(*abc*)");
|
|
expect(parser).toParse("(*(**)*)", "(*(**)*)");
|
|
expect(parser).toParse(
|
|
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)",
|
|
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|