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/compiler-test.js

705 lines
19 KiB
JavaScript

(function() {
var global = this;
/* ===== Helpers ===== */
global.throws = function(block, exceptionType, exceptionProperties) {
var exception = null;
try {
block();
} catch (e) {
exception = e;
}
ok(
exception !== null,
exception !== null ? "okay: thrown something" : "failed, nothing thrown"
);
if (exception !== null) {
ok(
exception instanceof exceptionType,
exception instanceof exceptionType
? "okay: thrown " + exceptionType.name
: "failed, thrown " + exception.name + " instead of " + exceptionType.name
);
for (var property in exceptionProperties) {
strictEqual(exception[property], exceptionProperties[property]);
}
}
};
global.parses = function(parser, input, expected) {
deepEqual(parser.parse(input), expected);
};
global.doesNotParse = function(parser, input) {
throws(function() { parser.parse(input); }, parser.SyntaxError);
};
global.doesNotParseWithMessage = function(parser, input, message) {
throws(
function() { parser.parse(input); },
parser.SyntaxError,
{ message: message }
);
};
global.doesNotParseWithPos = function(parser, input, line, column) {
var exception = throws(
function() { parser.parse(input); },
parser.SyntaxError,
{
line: line,
column: column
}
);
};
/* ===== PEG.ArrayUtils ===== */
module("PEG.ArrayUtils");
test("range", function() {
deepEqual(PEG.ArrayUtils.range(-1), []);
deepEqual(PEG.ArrayUtils.range(0), []);
deepEqual(PEG.ArrayUtils.range(1), [0]);
deepEqual(PEG.ArrayUtils.range(2), [0, 1]);
deepEqual(PEG.ArrayUtils.range(3), [0, 1, 2]);
deepEqual(PEG.ArrayUtils.range(0, -1), []);
deepEqual(PEG.ArrayUtils.range(0, 0), []);
deepEqual(PEG.ArrayUtils.range(0, 1), [0]);
deepEqual(PEG.ArrayUtils.range(0, 2), [0, 1]);
deepEqual(PEG.ArrayUtils.range(0, 3), [0, 1, 2]);
deepEqual(PEG.ArrayUtils.range(-3, 0), [-3, -2, -1]);
});
test("contains", function() {
ok(!PEG.ArrayUtils.contains([], 42));
ok(PEG.ArrayUtils.contains([1, 2, 3], 1));
ok(PEG.ArrayUtils.contains([1, 2, 3], 2));
ok(PEG.ArrayUtils.contains([1, 2, 3], 3));
ok(!PEG.ArrayUtils.contains([1, 2, 3], 42));
ok(!PEG.ArrayUtils.contains([1, 2, 3], "2")); // Does it use |===|?
});
test("each", function() {
var sum;
function increment(x) { sum += x; }
sum = 0;
PEG.ArrayUtils.each([], increment);
strictEqual(sum, 0);
sum = 0;
PEG.ArrayUtils.each([1, 2, 3], increment);
strictEqual(sum, 6);
});
test("map", function() {
function square(x) { return x * x; }
deepEqual(PEG.ArrayUtils.map([], square), []);
deepEqual(PEG.ArrayUtils.map([1, 2, 3], square), [1, 4, 9]);
});
/* ===== PEG.StringUtils ===== */
module("PEG.StringUtils");
test("quote", function() {
strictEqual(PEG.StringUtils.quote(""), '""');
strictEqual(PEG.StringUtils.quote("abcd"), '"abcd"');
strictEqual(
PEG.StringUtils.quote("\"\\\r\u2028\u2029\n\"\\\r\u2028\u2029\n"),
'"\\\"\\\\\\r\\u2028\\u2029\\n\\\"\\\\\\r\\u2028\\u2029\\n"'
);
});
/* ===== PEG.RegExpUtils ===== */
module("PEG.RegExpUtils");
test("quoteForClass", function() {
strictEqual(PEG.RegExpUtils.quoteForClass(""), '');
strictEqual(PEG.RegExpUtils.quoteForClass("abcd"), 'abcd');
strictEqual(
PEG.RegExpUtils.quoteForClass("\\\0/]-\r\u2028\u2029\n\\\0/]-\r\u2028\u2029\n"),
'\\\\\\0\\/\\]\\-\\r\\u2028\\u2029\\n\\\\\\0\\/\\]\\-\\r\\u2028\\u2029\\n'
);
});
/* ===== PEG.Compiler ===== */
module("PEG.Compiler");
test("formatCode joins parts", function() {
strictEqual(PEG.Compiler.formatCode("foo", "bar"), "foo\nbar");
});
test("formatCode interpolates variables", function() {
strictEqual(
PEG.Compiler.formatCode("foo", "${bar}", { bar: "baz" }),
"foo\nbaz"
);
throws(
function() { PEG.Compiler.formatCode("foo", "${bar}"); },
Error,
{ message: "Undefined variable: \"bar\"." }
);
});
test("formatCode filters variables", function() {
strictEqual(
PEG.Compiler.formatCode("foo", "${bar|string}", { bar: "baz" }),
"foo\n\"baz\""
);
throws(
function() { PEG.Compiler.formatCode("foo", "${bar|eeek}", { bar: "baz" }); },
Error,
{ message: "Unrecognized filter: \"eeek\"." }
);
});
test("formatCode indents multiline parts", function() {
strictEqual(
PEG.Compiler.formatCode("foo", "${bar}", { bar: " baz\nqux" }),
"foo\n baz\n qux"
);
});
test("generateUniqueIdentifier", function() {
notStrictEqual(
PEG.Compiler.generateUniqueIdentifier("prefix"),
PEG.Compiler.generateUniqueIdentifier("prefix")
);
});
/* ===== PEG ===== */
module("PEG");
test("buildParser reports syntax errors in the grammar", function() {
throws(
function() { PEG.buildParser(''); },
PEG.grammarParser.SyntaxError
);
});
test("buildParser reports missing start rule", function() {
throws(
function() { PEG.buildParser('notStart = "abcd"'); },
PEG.GrammarError,
{ message: "Missing \"start\" rule." }
);
});
test("buildParser reports missing referenced rules", function() {
var grammars = [
'start = missing',
'start = missing / "a" / "b"',
'start = "a" / "b" / missing',
'start = missing "a" "b"',
'start = "a" "b" missing',
'start = &missing',
'start = !missing',
'start = missing?',
'start = missing*',
'start = missing+',
'start = missing { }'
];
PEG.ArrayUtils.each(grammars, function(grammar) {
throws(
function() { PEG.buildParser(grammar); },
PEG.GrammarError,
{ message: "Referenced rule \"missing\" does not exist." }
);
});
});
test("buildParser reports left recursion", function() {
var grammars = [
/* Direct */
'start = start',
'start = start / "a" / "b"',
'start = "a" / "b" / start',
'start = start "a" "b"',
'start = &start',
'start = !start',
'start = start?',
'start = start*',
'start = start+',
'start = start { }',
/* Indirect */
'start = stop\nstop = start'
];
PEG.ArrayUtils.each(grammars, function(grammar) {
throws(
function() { PEG.buildParser(grammar); },
PEG.GrammarError,
{ message: "Left recursion detected for rule \"start\"." }
);
});
});
test("buildParser allows custom start rule", function() {
var parser = PEG.buildParser('s = "abcd"', "s");
parses(parser, "abcd", "abcd");
});
/* ===== Generated Parser ===== */
module("Generated Parser");
test("choices", function() {
var parser = PEG.buildParser('start = "a" / "b" / "c"');
parses(parser, "a", "a");
parses(parser, "b", "b");
parses(parser, "c", "c");
doesNotParse(parser, "");
doesNotParse(parser, "ab");
doesNotParse(parser, "d");
});
test("sequences", function() {
var emptySequenceParser = PEG.buildParser('start = ');
parses(emptySequenceParser, "", []);
doesNotParse(emptySequenceParser, "abc");
var nonEmptySequenceParser = PEG.buildParser('start = "a" "b" "c"');
parses(nonEmptySequenceParser, "abc", ["a", "b", "c"]);
doesNotParse(nonEmptySequenceParser, "");
doesNotParse(nonEmptySequenceParser, "ab");
doesNotParse(nonEmptySequenceParser, "abcd");
doesNotParse(nonEmptySequenceParser, "efg");
/*
* Test that the parsing position returns after unsuccessful parsing of a
* sequence.
*/
var posTestParser = PEG.buildParser('start = ("a" "b") / "a"');
parses(posTestParser, "a", "a");
});
test("and predicate", function() {
var parser = PEG.buildParser('start = "a" &"b" "b"');
parses(parser, "ab", ["a", "", "b"]);
doesNotParse(parser, "ac");
/*
* Test that the parsing position returns after successful parsing of a
* predicate is not needed, it is implicit in the tests above.
*/
});
test("not predicate", function() {
var parser = PEG.buildParser('start = "a" !"b"');
parses(parser, "a", ["a", ""]);
doesNotParse(parser, "ab");
/*
* Test that the parsing position returns after successful parsing of a
* predicate.
*/
var posTestParser = PEG.buildParser('start = "a" !"b" "c"');
parses(posTestParser, "ac", ["a", "", "c"]);
});
test("optional expressions", function() {
var parser = PEG.buildParser('start = "a"?');
parses(parser, "", "");
parses(parser, "a", "a");
});
test("zero or more expressions", function() {
var parser = PEG.buildParser('start = "a"*');
parses(parser, "", []);
parses(parser, "a", ["a"]);
parses(parser, "aaa", ["a", "a", "a"]);
});
test("one or more expressions", function() {
var parser = PEG.buildParser('start = "a"+');
doesNotParse(parser, "");
parses(parser, "a", ["a"]);
parses(parser, "aaa", ["a", "a", "a"]);
});
test("actions", function() {
var singleMatchParser = PEG.buildParser(
'start = "a" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }'
);
parses(singleMatchParser, "a", "A");
var multiMatchParser = PEG.buildParser(
'start = "a" "b" "c" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }'
);
parses(multiMatchParser, "abc", "ABC");
var innerMatchParser = PEG.buildParser(
'start = "a" ("b" "c" "d" { return Array.prototype.slice.call(arguments).join("").toUpperCase(); }) "e"'
);
parses(innerMatchParser, "abcde", ["a", "BCD", "e"]);
/* Test that the action is not called when its expression does not match. */
var notAMatchParser = PEG.buildParser(
'start = "a" { ok(false, "action got called when it should not be"); }'
);
doesNotParse(notAMatchParser, "b");
var variablesParser = PEG.buildParser([
'start = "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" {',
' return [$1, $2, $3, $4, $5, $6, $7, $8, $9, $10].join("").toUpperCase();',
' }'
].join("\n"));
parses(variablesParser, "abcdefghij", "ABCDEFGHIJ");
});
test("rule references", function() {
var parser = PEG.buildParser([
'start = static / dynamic',
'static = "C" / "C++" / "Java" / "C#"',
'dynamic = "Ruby" / "Python" / "JavaScript"'
].join("\n"));
parses(parser, "Java", "Java");
parses(parser, "Python", "Python");
});
test("literals", function() {
var parser = PEG.buildParser('start = "abcd"');
parses(parser, "abcd", "abcd");
doesNotParse(parser, "");
doesNotParse(parser, "abc");
doesNotParse(parser, "abcde");
doesNotParse(parser, "efgh");
/*
* Test that the parsing position moves forward after successful parsing of
* a literal.
*/
var posTestParser = PEG.buildParser('start = "a" "b"');
parses(posTestParser, "ab", ["a", "b"]);
});
test("anys", function() {
var parser = PEG.buildParser('start = .');
parses(parser, "a", "a");
doesNotParse(parser, "");
doesNotParse(parser, "ab");
/*
* Test that the parsing position moves forward after successful parsing of
* an any.
*/
var posTestParser = PEG.buildParser('start = . .');
parses(posTestParser, "ab", ["a", "b"]);
});
test("classes", function() {
var emptyClassParser = PEG.buildParser('start = []');
doesNotParse(emptyClassParser, "");
doesNotParse(emptyClassParser, "a");
doesNotParse(emptyClassParser, "ab");
var nonEmptyClassParser = PEG.buildParser('start = [ab-d]');
parses(nonEmptyClassParser, "a", "a");
parses(nonEmptyClassParser, "b", "b");
parses(nonEmptyClassParser, "c", "c");
parses(nonEmptyClassParser, "d", "d");
doesNotParse(nonEmptyClassParser, "");
doesNotParse(nonEmptyClassParser, "ab");
var invertedEmptyClassParser = PEG.buildParser('start = [^]');
doesNotParse(invertedEmptyClassParser, "");
parses(invertedEmptyClassParser, "a", "a");
doesNotParse(invertedEmptyClassParser, "ab");
var invertedNonEmptyClassParser = PEG.buildParser('start = [^ab-d]');
doesNotParse(invertedNonEmptyClassParser, "a", "a");
doesNotParse(invertedNonEmptyClassParser, "b", "b");
doesNotParse(invertedNonEmptyClassParser, "c", "c");
doesNotParse(invertedNonEmptyClassParser, "d", "d");
doesNotParse(invertedNonEmptyClassParser, "");
doesNotParse(invertedNonEmptyClassParser, "ab");
/*
* Test that the parsing position moves forward after successful parsing of
* a class.
*/
var posTestParser = PEG.buildParser('start = [ab-d] [ab-d]');
parses(posTestParser, "ab", ["a", "b"]);
});
test("cache", function() {
/*
* Should trigger a codepath where the cache is used (for the "a" rule).
*/
var parser = PEG.buildParser([
'start = (a b) / (a c)',
'a = "a"',
'b = "b"',
'c = "c"'
].join("\n"));
parses(parser, "ac", ["a", "c"]);
});
test("indempotence", function() {
var parser1 = PEG.buildParser('start = "abcd"');
var parser2 = PEG.buildParser('start = "abcd"');
strictEqual(parser1.toSource(), parser2.toSource());
});
test("error messages", function() {
var literalParser = PEG.buildParser('start = "abcd"');
doesNotParseWithMessage(
literalParser,
"",
'Expected "abcd" but end of input found.'
);
doesNotParseWithMessage(
literalParser,
"efgh",
'Expected "abcd" but "e" found.'
);
doesNotParseWithMessage(
literalParser,
"abcde",
'Expected end of input but "e" found.'
);
var classParser = PEG.buildParser('start = [a-d]');
doesNotParseWithMessage(
classParser,
"",
'Expected [a-d] but end of input found.'
);
var negativeClassParser = PEG.buildParser('start = [^a-d]');
doesNotParseWithMessage(
negativeClassParser,
"",
'Expected [^a-d] but end of input found.'
);
var anyParser = PEG.buildParser('start = .');
doesNotParseWithMessage(
anyParser,
"",
'Expected any character but end of input found.'
);
var namedRuleWithLiteralParser = PEG.buildParser('start "digit" = [0-9]');
doesNotParseWithMessage(
namedRuleWithLiteralParser,
"a",
'Expected digit but "a" found.'
);
var namedRuleWithAnyParser = PEG.buildParser('start "whatever" = .');
doesNotParseWithMessage(
namedRuleWithAnyParser,
"",
'Expected whatever but end of input found.'
);
var namedRuleWithNamedRuleParser = PEG.buildParser([
'start "digits" = digit+',
'digit "digit" = [0-9]'
].join("\n"));
doesNotParseWithMessage(
namedRuleWithNamedRuleParser,
"",
'Expected digits but end of input found.'
);
var choiceParser1 = PEG.buildParser('start = "a" / "b" / "c"');
doesNotParseWithMessage(
choiceParser1,
"def",
'Expected "a", "b" or "c" but "d" found.'
);
var choiceParser2 = PEG.buildParser('start = "a" "b" "c" / "a"');
doesNotParseWithMessage(
choiceParser2,
"abd",
'Expected "c" but "d" found.'
);
var notPredicateParser = PEG.buildParser('start = !"a" "b"');
doesNotParseWithMessage(
notPredicateParser,
"c",
'Expected "b" but "c" found.'
);
var andPredicateParser = PEG.buildParser('start = &"a" [a-b]');
doesNotParseWithMessage(
andPredicateParser,
"c",
'Expected end of input but "c" found.'
);
var emptyParser = PEG.buildParser('start = ');
doesNotParseWithMessage(
emptyParser,
"something",
'Expected end of input but "s" found.'
);
var duplicateErrorParser = PEG.buildParser('start = "a" / "a"');
doesNotParseWithMessage(
duplicateErrorParser,
"",
'Expected "a" but end of input found.'
);
var unsortedErrorsParser = PEG.buildParser('start = "b" / "a"');
doesNotParseWithMessage(
unsortedErrorsParser,
"",
'Expected "a" or "b" but end of input found.'
);
});
test("error positions", function() {
var parser = PEG.buildParser([
'start = line (("\\r" / "\\n" / "\\u2028" / "\\u2029")+ line)*',
'line = digit (" "+ digit)*',
'digit = [0-9]+ { return $1.join(""); }'
].join("\n"));
doesNotParseWithPos(parser, "a", 1, 1);
doesNotParseWithPos(parser, "1\n2\n\n3\n\n\n4 5 x", 7, 5);
/* Non-Unix newlines */
doesNotParseWithPos(parser, "1\rx", 2, 1); // Old Mac
doesNotParseWithPos(parser, "1\r\nx", 2, 1); // Windows
doesNotParseWithPos(parser, "1\n\rx", 3, 1); // mismatched
/* Strange newlines */
doesNotParseWithPos(parser, "1\u2028x", 2, 1); // line separator
doesNotParseWithPos(parser, "1\u2029x", 2, 1); // paragraph separator
});
/*
* Following examples are from Wikipedia, see
* http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938.
*/
test("arithmetics", function() {
/*
* Value ← [0-9]+ / '(' Expr ')'
* Product ← Value (('*' / '/') Value)*
* Sum ← Product (('+' / '-') Product)*
* Expr ← Sum
*/
var parser = PEG.buildParser([
'Value = [0-9]+ { return parseInt($1.join("")); }',
' / "(" Expr ")" { return $2; }',
'Product = Value (("*" / "/") Value)* {',
' var result = $1;',
' for (var i = 0; i < $2.length; i++) {',
' if ($2[i][0] == "*") { result *= $2[i][1]; }',
' if ($2[i][0] == "/") { result /= $2[i][1]; }',
' }',
' return result;',
' }',
'Sum = Product (("+" / "-") Product)* {',
' var result = $1;',
' for (var i = 0; i < $2.length; i++) {',
' if ($2[i][0] == "+") { result += $2[i][1]; }',
' if ($2[i][0] == "-") { result -= $2[i][1]; }',
' }',
' return result;',
' }',
'Expr = Sum'
].join("\n"), "Expr");
/* Test "value" rule. */
parses(parser, "0", 0);
parses(parser, "123", 123);
parses(parser, "(42+43)", 42+43);
/* Test "product" rule. */
parses(parser, "42", 42);
parses(parser, "42*43", 42*43);
parses(parser, "42*43*44*45", 42*43*44*45);
parses(parser, "42/43", 42/43);
parses(parser, "42/43/44/45", 42/43/44/45);
/* Test "sum" rule. */
parses(parser, "42*43", 42*43);
parses(parser, "42*43+44*45", 42*43+44*45);
parses(parser, "42*43+44*45+46*47+48*49", 42*43+44*45+46*47+48*49);
parses(parser, "42*43-44*45", 42*43-44*45);
parses(parser, "42*43-44*45-46*47-48*49", 42*43-44*45-46*47-48*49);
/* Test "expr" rule. */
parses(parser, "42+43", 42+43);
/* Complex test */
parses(parser, "(1+2)*(3+4)",(1+2)*(3+4));
});
test("non-context-free language", 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
*/
var parser = PEG.buildParser([
'S = &(A "c") "a"+ B !("a" / "b" / "c") { return $2.join("") + $3; }',
'A = "a" A? "b" { return $1 + $2 + $3; }',
'B = "b" B? "c" { return $1 + $2 + $3; }',
].join("\n"), "S");
parses(parser, "abc", "abc");
parses(parser, "aaabbbccc", "aaabbbccc");
doesNotParse(parser, "aabbbccc");
doesNotParse(parser, "aaaabbbccc");
doesNotParse(parser, "aaabbccc");
doesNotParse(parser, "aaabbbbccc");
doesNotParse(parser, "aaabbbcc");
doesNotParse(parser, "aaabbbcccc");
});
test("nested comments", function() {
/*
* Begin ← "(*"
* End ← "*)"
* C ← Begin N* End
* N ← C / (!Begin !End Z)
* Z ← any single character
*/
var parser = PEG.buildParser([
'Begin = "(*"',
'End = "*)"',
'C = Begin N* End { return $1 + $2.join("") + $3; }',
'N = C',
' / (!Begin !End Z) { return $3; }',
'Z = .'
].join("\n"), "C");
parses(parser, "(**)", "(**)");
parses(parser, "(*abc*)", "(*abc*)");
parses(parser, "(*(**)*)", "(*(**)*)");
parses(
parser,
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)",
"(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)"
);
});
})();