diff --git a/src/compiler/passes.js b/src/compiler/passes.js index 773c40f..9ca5205 100644 --- a/src/compiler/passes.js +++ b/src/compiler/passes.js @@ -5,408 +5,10 @@ * or modify it as needed. If the pass encounters a semantic error, it throws * |PEG.GrammarError|. */ -PEG.compiler.passes = { - /* Checks that all referenced rules exist. */ - reportMissingRules: function(ast) { - function nop() {} +PEG.compiler.passes = {}; - function checkExpression(node) { check(node.expression); } - - function checkSubnodes(propertyName) { - return function(node) { each(node[propertyName], check); }; - } - - var check = buildNodeVisitor({ - grammar: checkSubnodes("rules"), - rule: checkExpression, - choice: checkSubnodes("alternatives"), - sequence: checkSubnodes("elements"), - labeled: checkExpression, - simple_and: checkExpression, - simple_not: checkExpression, - semantic_and: nop, - semantic_not: nop, - optional: checkExpression, - zero_or_more: checkExpression, - one_or_more: checkExpression, - action: checkExpression, - - rule_ref: - function(node) { - if (!findRuleByName(ast, node.name)) { - throw new PEG.GrammarError( - "Referenced rule \"" + node.name + "\" does not exist." - ); - } - }, - - literal: nop, - any: nop, - "class": nop - }); - - check(ast); - }, - - /* Checks that no left recursion is present. */ - reportLeftRecursion: function(ast) { - function nop() {} - - function checkExpression(node, appliedRules) { - check(node.expression, appliedRules); - } - - function checkSubnodes(propertyName) { - return function(node, appliedRules) { - each(node[propertyName], function(subnode) { - check(subnode, appliedRules); - }); - }; - } - - var check = buildNodeVisitor({ - grammar: checkSubnodes("rules"), - - rule: - function(node, appliedRules) { - check(node.expression, appliedRules.concat(node.name)); - }, - - choice: checkSubnodes("alternatives"), - - sequence: - function(node, appliedRules) { - if (node.elements.length > 0) { - check(node.elements[0], appliedRules); - } - }, - - labeled: checkExpression, - simple_and: checkExpression, - simple_not: checkExpression, - semantic_and: nop, - semantic_not: nop, - optional: checkExpression, - zero_or_more: checkExpression, - one_or_more: checkExpression, - action: checkExpression, - - rule_ref: - function(node, appliedRules) { - if (contains(appliedRules, node.name)) { - throw new PEG.GrammarError( - "Left recursion detected for rule \"" + node.name + "\"." - ); - } - check(findRuleByName(ast, node.name), appliedRules); - }, - - literal: nop, - any: nop, - "class": nop - }); - - check(ast, []); - }, - - /* - * Removes proxy rules -- that is, rules that only delegate to other rule. - */ - removeProxyRules: function(ast) { - function isProxyRule(node) { - return node.type === "rule" && node.expression.type === "rule_ref"; - } - - function replaceRuleRefs(ast, from, to) { - function nop() {} - - function replaceInExpression(node, from, to) { - replace(node.expression, from, to); - } - - function replaceInSubnodes(propertyName) { - return function(node, from, to) { - each(node[propertyName], function(subnode) { - replace(subnode, from, to); - }); - }; - } - - var replace = buildNodeVisitor({ - grammar: replaceInSubnodes("rules"), - rule: replaceInExpression, - choice: replaceInSubnodes("alternatives"), - sequence: replaceInSubnodes("elements"), - labeled: replaceInExpression, - simple_and: replaceInExpression, - simple_not: replaceInExpression, - semantic_and: nop, - semantic_not: nop, - optional: replaceInExpression, - zero_or_more: replaceInExpression, - one_or_more: replaceInExpression, - action: replaceInExpression, - - rule_ref: - function(node, from, to) { - if (node.name === from) { - node.name = to; - } - }, - - literal: nop, - any: nop, - "class": nop - }); - - replace(ast, from, to); - } - - var indices = []; - - each(ast.rules, function(rule, i) { - if (isProxyRule(rule)) { - replaceRuleRefs(ast, rule.name, rule.expression.name); - if (rule.name === ast.startRule) { - ast.startRule = rule.expression.name; - } - indices.push(i); - } - }); - - indices.reverse(); - - each(indices, function(index) { - ast.rules.splice(index, 1); - }); - }, - - /* - * Computes names of variables used for storing match results and parse - * positions in generated code. These variables are organized as two stacks. - * The following will hold after running this pass: - * - * * All nodes except "grammar" and "rule" nodes will have a |resultVar| - * property. It will contain a name of the variable that will store a - * match result of the expression represented by the node in generated - * code. - * - * * Some nodes will have a |posVar| property. It will contain a name of the - * variable that will store a parse position in generated code. - * - * * All "rule" nodes will contain |resultVars| and |posVars| properties. - * They will contain a list of values of |resultVar| and |posVar| - * properties used in rule's subnodes. (This is useful to declare - * variables in generated code.) - */ - computeVarNames: function(ast) { - function resultVar(index) { return "result" + index; } - function posVar(index) { return "pos" + index; } - - function computeLeaf(node, index) { - node.resultVar = resultVar(index.result); - - return { result: 0, pos: 0 }; - } - - function computeFromExpression(delta) { - return function(node, index) { - var depth = compute( - node.expression, - { - result: index.result + delta.result, - pos: index.pos + delta.pos - } - ); - - node.resultVar = resultVar(index.result); - if (delta.pos !== 0) { - node.posVar = posVar(index.pos); - } - - return { - result: depth.result + delta.result, - pos: depth.pos + delta.pos - }; - }; - } - - var compute = buildNodeVisitor({ - grammar: - function(node, index) { - each(node.rules, function(node) { - compute(node, index); - }); - }, - - rule: - function(node, index) { - var depth = compute(node.expression, index); - - node.resultVar = resultVar(index.result); - node.resultVars = map(range(depth.result + 1), resultVar); - node.posVars = map(range(depth.pos), posVar); - }, - - choice: - function(node, index) { - var depths = map(node.alternatives, function(alternative) { - return compute(alternative, index); - }); - - node.resultVar = resultVar(index.result); - - return { - result: Math.max.apply(null, pluck(depths, "result")), - pos: Math.max.apply(null, pluck(depths, "pos")) - }; - }, - - sequence: - function(node, index) { - var depths = map(node.elements, function(element, i) { - return compute( - element, - { result: index.result + i, pos: index.pos + 1 } - ); - }); - - node.resultVar = resultVar(index.result); - node.posVar = posVar(index.pos); - - return { - result: - node.elements.length > 0 - ? Math.max.apply( - null, - map(depths, function(d, i) { return i + d.result; }) - ) - : 0, - - pos: - node.elements.length > 0 - ? 1 + Math.max.apply(null, pluck(depths, "pos")) - : 1 - }; - }, - - labeled: computeFromExpression({ result: 0, pos: 0 }), - simple_and: computeFromExpression({ result: 0, pos: 1 }), - simple_not: computeFromExpression({ result: 0, pos: 1 }), - semantic_and: computeLeaf, - semantic_not: computeLeaf, - optional: computeFromExpression({ result: 0, pos: 0 }), - zero_or_more: computeFromExpression({ result: 1, pos: 0 }), - one_or_more: computeFromExpression({ result: 1, pos: 0 }), - action: computeFromExpression({ result: 0, pos: 1 }), - rule_ref: computeLeaf, - literal: computeLeaf, - any: computeLeaf, - "class": computeLeaf - }); - - compute(ast, { result: 0, pos: 0 }); - }, - - /* - * This pass walks through the AST and tracks what labels are visible at each - * point. For "action", "semantic_and" and "semantic_or" nodes it computes - * parameter names and values for the function used in generated code. (In the - * emitter, user's code is wrapped into a function that is immediately - * executed. Its parameter names correspond to visible labels and its - * parameter values to their captured values). Implicitly, this pass defines - * scoping rules for labels. - * - * After running this pass, all "action", "semantic_and" and "semantic_or" - * nodes will have a |params| property containing an object mapping parameter - * names to the expressions that will be used as their values. - */ - computeParams: function(ast) { - var envs = []; - - function scoped(f) { - envs.push({}); - f(); - envs.pop(); - } - - function nop() {} - - function computeForScopedExpression(node) { - scoped(function() { compute(node.expression); }); - } - - function computeParams(node) { - var env = envs[envs.length - 1], params = {}, name; - - for (name in env) { - params[name] = env[name]; - } - node.params = params; - } - - var compute = buildNodeVisitor({ - grammar: - function(node) { - each(node.rules, compute); - }, - - rule: computeForScopedExpression, - - choice: - function(node) { - scoped(function() { each(node.alternatives, compute); }); - }, - - sequence: - function(node) { - var env = envs[envs.length - 1], name; - - function fixup(name) { - each(pluck(node.elements, "resultVar"), function(resultVar, i) { - if ((new RegExp("^" + resultVar + "(\\[\\d+\\])*$")).test(env[name])) { - env[name] = node.resultVar + "[" + i + "]" - + env[name].substr(resultVar.length); - } - }); - } - - each(node.elements, compute); - - for (name in env) { - fixup(name); - } - }, - - labeled: - function(node) { - envs[envs.length - 1][node.label] = node.resultVar; - - scoped(function() { compute(node.expression); }); - }, - - simple_and: computeForScopedExpression, - simple_not: computeForScopedExpression, - semantic_and: computeParams, - semantic_not: computeParams, - optional: computeForScopedExpression, - zero_or_more: computeForScopedExpression, - one_or_more: computeForScopedExpression, - - action: - function(node) { - scoped(function() { - compute(node.expression); - computeParams(node); - }); - }, - - rule_ref: nop, - literal: nop, - any: nop, - "class": nop - }); - - compute(ast); - } -}; +// @include "passes/report-missing-rules.js" +// @include "passes/report-left-recursion.js" +// @include "passes/remove-proxy-rules.js" +// @include "passes/compute-var-names.js" +// @include "passes/compute-params.js" diff --git a/src/compiler/passes/compute-params.js b/src/compiler/passes/compute-params.js new file mode 100644 index 0000000..7c48ecd --- /dev/null +++ b/src/compiler/passes/compute-params.js @@ -0,0 +1,101 @@ +/* + * This pass walks through the AST and tracks what labels are visible at each + * point. For "action", "semantic_and" and "semantic_or" nodes it computes + * parameter names and values for the function used in generated code. (In the + * emitter, user's code is wrapped into a function that is immediately executed. + * Its parameter names correspond to visible labels and its parameter values to + * their captured values). Implicitly, this pass defines scoping rules for + * labels. + * + * After running this pass, all "action", "semantic_and" and "semantic_or" nodes + * will have a |params| property containing an object mapping parameter names to + * the expressions that will be used as their values. + */ +PEG.compiler.passes.computeParams = function(ast) { + var envs = []; + + function scoped(f) { + envs.push({}); + f(); + envs.pop(); + } + + function nop() {} + + function computeForScopedExpression(node) { + scoped(function() { compute(node.expression); }); + } + + function computeParams(node) { + var env = envs[envs.length - 1], params = {}, name; + + for (name in env) { + params[name] = env[name]; + } + node.params = params; + } + + var compute = buildNodeVisitor({ + grammar: + function(node) { + each(node.rules, compute); + }, + + rule: computeForScopedExpression, + + choice: + function(node) { + scoped(function() { each(node.alternatives, compute); }); + }, + + sequence: + function(node) { + var env = envs[envs.length - 1], name; + + function fixup(name) { + each(pluck(node.elements, "resultVar"), function(resultVar, i) { + if ((new RegExp("^" + resultVar + "(\\[\\d+\\])*$")).test(env[name])) { + env[name] = node.resultVar + "[" + i + "]" + + env[name].substr(resultVar.length); + } + }); + } + + each(node.elements, compute); + + for (name in env) { + fixup(name); + } + }, + + labeled: + function(node) { + envs[envs.length - 1][node.label] = node.resultVar; + + scoped(function() { compute(node.expression); }); + }, + + simple_and: computeForScopedExpression, + simple_not: computeForScopedExpression, + semantic_and: computeParams, + semantic_not: computeParams, + optional: computeForScopedExpression, + zero_or_more: computeForScopedExpression, + one_or_more: computeForScopedExpression, + + action: + function(node) { + scoped(function() { + compute(node.expression); + computeParams(node); + }); + }, + + rule_ref: nop, + literal: nop, + any: nop, + "class": nop + }); + + compute(ast); +}; diff --git a/src/compiler/passes/compute-var-names.js b/src/compiler/passes/compute-var-names.js new file mode 100644 index 0000000..d7f1613 --- /dev/null +++ b/src/compiler/passes/compute-var-names.js @@ -0,0 +1,125 @@ +/* + * Computes names of variables used for storing match results and parse + * positions in generated code. These variables are organized as two stacks. + * The following will hold after running this pass: + * + * * All nodes except "grammar" and "rule" nodes will have a |resultVar| + * property. It will contain a name of the variable that will store a match + * result of the expression represented by the node in generated code. + * + * * Some nodes will have a |posVar| property. It will contain a name of the + * variable that will store a parse position in generated code. + * + * * All "rule" nodes will contain |resultVars| and |posVars| properties. + * They will contain a list of values of |resultVar| and |posVar| properties + * used in rule's subnodes. (This is useful to declare variables in + * generated code.) + */ +PEG.compiler.passes.computeVarNames = function(ast) { + function resultVar(index) { return "result" + index; } + function posVar(index) { return "pos" + index; } + + function computeLeaf(node, index) { + node.resultVar = resultVar(index.result); + + return { result: 0, pos: 0 }; + } + + function computeFromExpression(delta) { + return function(node, index) { + var depth = compute( + node.expression, + { + result: index.result + delta.result, + pos: index.pos + delta.pos + } + ); + + node.resultVar = resultVar(index.result); + if (delta.pos !== 0) { + node.posVar = posVar(index.pos); + } + + return { + result: depth.result + delta.result, + pos: depth.pos + delta.pos + }; + }; + } + + var compute = buildNodeVisitor({ + grammar: + function(node, index) { + each(node.rules, function(node) { + compute(node, index); + }); + }, + + rule: + function(node, index) { + var depth = compute(node.expression, index); + + node.resultVar = resultVar(index.result); + node.resultVars = map(range(depth.result + 1), resultVar); + node.posVars = map(range(depth.pos), posVar); + }, + + choice: + function(node, index) { + var depths = map(node.alternatives, function(alternative) { + return compute(alternative, index); + }); + + node.resultVar = resultVar(index.result); + + return { + result: Math.max.apply(null, pluck(depths, "result")), + pos: Math.max.apply(null, pluck(depths, "pos")) + }; + }, + + sequence: + function(node, index) { + var depths = map(node.elements, function(element, i) { + return compute( + element, + { result: index.result + i, pos: index.pos + 1 } + ); + }); + + node.resultVar = resultVar(index.result); + node.posVar = posVar(index.pos); + + return { + result: + node.elements.length > 0 + ? Math.max.apply( + null, + map(depths, function(d, i) { return i + d.result; }) + ) + : 0, + + pos: + node.elements.length > 0 + ? 1 + Math.max.apply(null, pluck(depths, "pos")) + : 1 + }; + }, + + labeled: computeFromExpression({ result: 0, pos: 0 }), + simple_and: computeFromExpression({ result: 0, pos: 1 }), + simple_not: computeFromExpression({ result: 0, pos: 1 }), + semantic_and: computeLeaf, + semantic_not: computeLeaf, + optional: computeFromExpression({ result: 0, pos: 0 }), + zero_or_more: computeFromExpression({ result: 1, pos: 0 }), + one_or_more: computeFromExpression({ result: 1, pos: 0 }), + action: computeFromExpression({ result: 0, pos: 1 }), + rule_ref: computeLeaf, + literal: computeLeaf, + any: computeLeaf, + "class": computeLeaf + }); + + compute(ast, { result: 0, pos: 0 }); +}; diff --git a/src/compiler/passes/remove-proxy-rules.js b/src/compiler/passes/remove-proxy-rules.js new file mode 100644 index 0000000..862118f --- /dev/null +++ b/src/compiler/passes/remove-proxy-rules.js @@ -0,0 +1,71 @@ +/* + * Removes proxy rules -- that is, rules that only delegate to other rule. + */ +PEG.compiler.passes.removeProxyRules = function(ast) { + function isProxyRule(node) { + return node.type === "rule" && node.expression.type === "rule_ref"; + } + + function replaceRuleRefs(ast, from, to) { + function nop() {} + + function replaceInExpression(node, from, to) { + replace(node.expression, from, to); + } + + function replaceInSubnodes(propertyName) { + return function(node, from, to) { + each(node[propertyName], function(subnode) { + replace(subnode, from, to); + }); + }; + } + + var replace = buildNodeVisitor({ + grammar: replaceInSubnodes("rules"), + rule: replaceInExpression, + choice: replaceInSubnodes("alternatives"), + sequence: replaceInSubnodes("elements"), + labeled: replaceInExpression, + simple_and: replaceInExpression, + simple_not: replaceInExpression, + semantic_and: nop, + semantic_not: nop, + optional: replaceInExpression, + zero_or_more: replaceInExpression, + one_or_more: replaceInExpression, + action: replaceInExpression, + + rule_ref: + function(node, from, to) { + if (node.name === from) { + node.name = to; + } + }, + + literal: nop, + any: nop, + "class": nop + }); + + replace(ast, from, to); + } + + var indices = []; + + each(ast.rules, function(rule, i) { + if (isProxyRule(rule)) { + replaceRuleRefs(ast, rule.name, rule.expression.name); + if (rule.name === ast.startRule) { + ast.startRule = rule.expression.name; + } + indices.push(i); + } + }); + + indices.reverse(); + + each(indices, function(index) { + ast.rules.splice(index, 1); + }); +}; diff --git a/src/compiler/passes/report-left-recursion.js b/src/compiler/passes/report-left-recursion.js new file mode 100644 index 0000000..8c46b22 --- /dev/null +++ b/src/compiler/passes/report-left-recursion.js @@ -0,0 +1,60 @@ +/* Checks that no left recursion is present. */ +PEG.compiler.passes.reportLeftRecursion = function(ast) { + function nop() {} + + function checkExpression(node, appliedRules) { + check(node.expression, appliedRules); + } + + function checkSubnodes(propertyName) { + return function(node, appliedRules) { + each(node[propertyName], function(subnode) { + check(subnode, appliedRules); + }); + }; + } + + var check = buildNodeVisitor({ + grammar: checkSubnodes("rules"), + + rule: + function(node, appliedRules) { + check(node.expression, appliedRules.concat(node.name)); + }, + + choice: checkSubnodes("alternatives"), + + sequence: + function(node, appliedRules) { + if (node.elements.length > 0) { + check(node.elements[0], appliedRules); + } + }, + + labeled: checkExpression, + simple_and: checkExpression, + simple_not: checkExpression, + semantic_and: nop, + semantic_not: nop, + optional: checkExpression, + zero_or_more: checkExpression, + one_or_more: checkExpression, + action: checkExpression, + + rule_ref: + function(node, appliedRules) { + if (contains(appliedRules, node.name)) { + throw new PEG.GrammarError( + "Left recursion detected for rule \"" + node.name + "\"." + ); + } + check(findRuleByName(ast, node.name), appliedRules); + }, + + literal: nop, + any: nop, + "class": nop + }); + + check(ast, []); +}; diff --git a/src/compiler/passes/report-missing-rules.js b/src/compiler/passes/report-missing-rules.js new file mode 100644 index 0000000..d0a37ae --- /dev/null +++ b/src/compiler/passes/report-missing-rules.js @@ -0,0 +1,41 @@ +/* Checks that all referenced rules exist. */ +PEG.compiler.passes.reportMissingRules = function(ast) { + function nop() {} + + function checkExpression(node) { check(node.expression); } + + function checkSubnodes(propertyName) { + return function(node) { each(node[propertyName], check); }; + } + + var check = buildNodeVisitor({ + grammar: checkSubnodes("rules"), + rule: checkExpression, + choice: checkSubnodes("alternatives"), + sequence: checkSubnodes("elements"), + labeled: checkExpression, + simple_and: checkExpression, + simple_not: checkExpression, + semantic_and: nop, + semantic_not: nop, + optional: checkExpression, + zero_or_more: checkExpression, + one_or_more: checkExpression, + action: checkExpression, + + rule_ref: + function(node) { + if (!findRuleByName(ast, node.name)) { + throw new PEG.GrammarError( + "Referenced rule \"" + node.name + "\" does not exist." + ); + } + }, + + literal: nop, + any: nop, + "class": nop + }); + + check(ast); +}; diff --git a/test/compiler/passes-test.js b/test/compiler/passes-test.js deleted file mode 100644 index 9e9104b..0000000 --- a/test/compiler/passes-test.js +++ /dev/null @@ -1,607 +0,0 @@ -(function() { - -module("PEG.compiler.passes"); - -test("reports missing referenced rules", function() { - function testGrammar(grammar) { - raises( - function() { - var ast = PEG.parser.parse(grammar); - PEG.compiler.passes.reportMissingRules(ast); - }, - function(e) { - return e instanceof PEG.GrammarError - && e.message === "Referenced rule \"missing\" does not exist."; - } - ); - } - - var grammars = [ - 'start = missing', - 'start = missing / "a" / "b"', - 'start = "a" / "b" / missing', - 'start = missing "a" "b"', - 'start = "a" "b" missing', - 'start = label:missing', - 'start = &missing', - 'start = !missing', - 'start = missing?', - 'start = missing*', - 'start = missing+', - 'start = missing { }' - ]; - - for (var i = 0; i < grammars.length; i++) { testGrammar(grammars[i]); } -}); - -test("reports left recursion", function() { - function testGrammar(grammar) { - raises( - function() { - var ast = PEG.parser.parse(grammar); - PEG.compiler.passes.reportLeftRecursion(ast); - }, - function(e) { - return e instanceof PEG.GrammarError - && e.message === "Left recursion detected for rule \"start\"."; - } - ); - } - - var grammars = [ - /* Direct */ - 'start = start', - 'start = start / "a" / "b"', - 'start = "a" / "b" / start', - 'start = start "a" "b"', - 'start = label:start', - 'start = &start', - 'start = !start', - 'start = start?', - 'start = start*', - 'start = start+', - 'start = start { }', - - /* Indirect */ - 'start = stop; stop = start' - ]; - - for (var i = 0; i < grammars.length; i++) { testGrammar(grammars[i]); } -}); - -test("removes proxy rules", function() { - function simpleGrammar(rules, startRule) { - return { - type: "grammar", - initializer: null, - rules: rules, - startRule: startRule - }; - } - - var proxiedRule = { - type: "rule", - name: "proxied", - displayName: null, - expression: { type: "literal", value: "a", ignoreCase: false } - }; - - var proxiedRuleRef = { - type: "rule_ref", - name: "proxied" - }; - - function simpleGrammarWithStartAndProxied(startRuleExpression) { - return simpleGrammar( - [ - { - type: "rule", - name: "start", - displayName: null, - expression: startRuleExpression - }, - proxiedRule - ], - "start" - ); - } - - var cases = [ - { - grammar: 'start = proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammar([proxiedRule], "proxied") - }, - { - grammar: 'start = proxy / "a" / "b"; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "choice", - alternatives: [ - proxiedRuleRef, - { type: "literal", value: "a", ignoreCase: false }, - { type: "literal", value: "b", ignoreCase: false } - ] - }) - }, - { - grammar: 'start = "a" / "b" / proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "choice", - alternatives: [ - { type: "literal", value: "a", ignoreCase: false }, - { type: "literal", value: "b", ignoreCase: false }, - proxiedRuleRef - ] - }) - }, - { - grammar: 'start = proxy "a" "b"; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "sequence", - elements: [ - proxiedRuleRef, - { type: "literal", value: "a", ignoreCase: false }, - { type: "literal", value: "b", ignoreCase: false } - ] - }) - }, - { - grammar: 'start = "a" "b" proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "sequence", - elements: [ - { type: "literal", value: "a", ignoreCase: false }, - { type: "literal", value: "b", ignoreCase: false }, - proxiedRuleRef - ] - }) - }, - { - grammar: 'start = label:proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "labeled", - label: "label", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = &proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "simple_and", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = !proxy; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "simple_not", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = proxy?; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "optional", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = proxy*; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "zero_or_more", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = proxy+; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "one_or_more", - expression: proxiedRuleRef - }) - }, - { - grammar: 'start = proxy { }; proxy = proxied; proxied = "a"', - ast: simpleGrammarWithStartAndProxied({ - type: "action", - code: " ", - expression: proxiedRuleRef - }) - } - ]; - - for (var i = 0; i < cases.length; i++) { - var ast = PEG.parser.parse(cases[i].grammar); - PEG.compiler.passes.removeProxyRules(ast); - - deepEqual(ast, cases[i].ast); - } -}); - -test("computes variable names", function() { - var leafDetails = { resultVar: "result0" }, - choiceDetails = { - resultVar: "result0", - alternatives: [ - { resultVar: "result0", posVar: "pos0" }, - { resultVar: "result0", posVar: "pos0" }, - { resultVar: "result0", posVar: "pos0" } - ] - }, - sequenceDetails = { - resultVar: "result0", - posVar: "pos0", - elements: [ - { resultVar: "result0", posVar: "pos1" }, - { resultVar: "result1", posVar: "pos1" }, - { resultVar: "result2", posVar: "pos1" } - ] - }; - - var cases = [ - /* Choice */ - { - grammar: 'start = &"a" / &"b" / &"c"', - resultVars: ["result0"], - posVars: ["pos0"], - details: choiceDetails - }, - { - grammar: 'start = &"a" / &"b"* / &"c"', - resultVars: ["result0", "result1"], - posVars: ["pos0"], - details: choiceDetails - }, - { - grammar: 'start = &"a" / &(&"b") / &"c"', - resultVars: ["result0"], - posVars: ["pos0", "pos1"], - details: choiceDetails - }, - - /* Sequence */ - { - grammar: 'start = ', - resultVars: ["result0"], - posVars: ["pos0"], - details: { resultVar: "result0", posVar: "pos0" } - }, - { - grammar: 'start = &"a" &"b" &"c"', - resultVars: ["result0", "result1", "result2"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &"a" &"b" &"c"*', - resultVars: ["result0", "result1", "result2", "result3"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &"a" &"b"* &"c"', - resultVars: ["result0", "result1", "result2"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &"a" &("b"*)* &"c"', - resultVars: ["result0", "result1", "result2", "result3"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &"a"* &"b" &"c"', - resultVars: ["result0", "result1", "result2"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &("a"*)* &"b" &"c"', - resultVars: ["result0", "result1", "result2"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &(("a"*)*)* &"b" &"c"', - resultVars: ["result0", "result1", "result2", "result3"], - posVars: ["pos0", "pos1"], - details: sequenceDetails - }, - { - grammar: 'start = &"a" &(&"b") &"c"', - resultVars: ["result0", "result1", "result2"], - posVars: ["pos0", "pos1", "pos2"], - details: sequenceDetails - }, - - /* Others */ - { - grammar: 'start = label:&"a"', - resultVars: ["result0"], - posVars: ["pos0"], - details: { - resultVar: "result0", - expression: { resultVar: "result0", posVar: "pos0" } - } - }, - { - grammar: 'start = &(&"a")', - resultVars: ["result0"], - posVars: ["pos0", "pos1"], - details: { - resultVar: "result0", - posVar: "pos0", - expression: { resultVar: "result0", posVar: "pos1" } - } - }, - { - grammar: 'start = !(&"a")', - resultVars: ["result0"], - posVars: ["pos0", "pos1"], - details: { - resultVar: "result0", - posVar: "pos0", - expression: { resultVar: "result0", posVar: "pos1" } - } - }, - { - grammar: 'start = &{ code }', - resultVars: ["result0"], - posVars: [], - details: leafDetails - }, - { - grammar: 'start = !{ code }', - resultVars: ["result0"], - posVars: [], - details: leafDetails - }, - { - grammar: 'start = (&"a")?', - resultVars: ["result0"], - posVars: ["pos0"], - details: { - resultVar: "result0", - expression: { resultVar: "result0", posVar: "pos0" } - } - }, - { - grammar: 'start = (&"a")*', - resultVars: ["result0", "result1"], - posVars: ["pos0"], - details: { - resultVar: "result0", - expression: { resultVar: "result1", posVar: "pos0" } - } - }, - { - grammar: 'start = (&"a")+', - resultVars: ["result0", "result1"], - posVars: ["pos0"], - details: { - resultVar: "result0", - expression: { resultVar: "result1", posVar: "pos0" } - } - }, - { - grammar: 'start = &"a" { code }', - resultVars: ["result0"], - posVars: ["pos0", "pos1"], - details: { - resultVar: "result0", - posVar: "pos0", - expression: { resultVar: "result0", posVar: "pos1" } - } - }, - { - grammar: 'start = a', - resultVars: ["result0"], - posVars: [], - details: leafDetails - }, - { - grammar: 'start = "a"', - resultVars: ["result0"], - posVars: [], - details: leafDetails - }, - { - grammar: 'start = .', - resultVars: ["result0"], - posVars: [], - details: leafDetails - }, - { - grammar: 'start = [a-z]', - resultVars: ["result0"], - posVars: [], - details: leafDetails - } - ]; - - function checkDetails(node, details) { - for (var key in details) { - if (Object.prototype.toString.call(details[key]) === "[object Array]") { - for (var i = 0; i < details[key].length; i++) { - checkDetails(node[key][i], details[key][i]); - } - } else if (typeof details[key] === "object") { - checkDetails(node[key], details[key]); - } else { - strictEqual(node[key], details[key]); - } - } - } - - for (var i = 0; i < cases.length; i++) { - var ast = PEG.parser.parse(cases[i].grammar); - PEG.compiler.passes.computeVarNames(ast); - - deepEqual(ast.rules[0].resultVars, cases[i].resultVars); - deepEqual(ast.rules[0].posVars, cases[i].posVars); - checkDetails(ast.rules[0].expression, cases[i].details); - } -}); - -test("computes params", function() { - function extractNode(node) { return node; } - function extractExpression(node) { return node.expression; } - - var cases = [ - /* Bacics */ - { - grammar: 'start = a:"a" { }', - extractor: extractNode, - params: { a: "result0" } - }, - { - grammar: 'start = a:"a" &{ }', - extractor: function(node) { return node.elements[1]; }, - params: { a: "result0" } - }, - { - grammar: 'start = a:"a" !{ }', - extractor: function(node) { return node.elements[1]; }, - params: { a: "result0" } - }, - - /* Recursive walk */ - { - grammar: 'start = a:"a" { } / "b" / "c"', - extractor: function(node) { return node.alternatives[0]; }, - params: { a: "result0" } - }, - { - grammar: 'start = "a" / "b" / c:"c" { }', - extractor: function(node) { return node.alternatives[2]; }, - params: { c: "result0" } - }, - { - grammar: 'start = (a:"a" { }) "b" "c"', - extractor: function(node) { return node.elements[0]; }, - params: { a: "result0" } - }, - { - grammar: 'start = "a" "b" (c:"c" { })', - extractor: function(node) { return node.elements[2]; }, - params: { c: "result2" } - }, - { - grammar: 'start = a:(b:"b" { })', - extractor: extractExpression, - params: { b: "result0" } - }, - { - grammar: 'start = &(a:"a" { })', - extractor: extractExpression, - params: { a: "result0" } - }, - { - grammar: 'start = !(a:"a" { })', - extractor: extractExpression, - params: { a: "result0" } - }, - { - grammar: 'start = (a:"a" { })?', - extractor: extractExpression, - params: { a: "result0" } - }, - { - grammar: 'start = (a:"a" { })*', - extractor: extractExpression, - params: { a: "result1" } - }, - { - grammar: 'start = (a:"a" { })+', - extractor: extractExpression, - params: { a: "result1" } - }, - { - grammar: 'start = (a:"a" { }) { }', - extractor: extractExpression, - params: { a: "result0" } - }, - - /* Scoping */ - { - grammar: 'start = (a:"a" / b:"b" / c:"c") { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = a:(b:"b") { }', - extractor: extractNode, - params: { a: "result0" } - }, - { - grammar: 'start = &(a:"a") { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = !(a:"a") { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = (a:"a")? { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = (a:"a")* { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = (a:"a")+ { }', - extractor: extractNode, - params: { } - }, - { - grammar: 'start = (a:"a" { }) { }', - extractor: extractNode, - params: { } - }, - - /* Sequences */ - { - grammar: 'start = a:"a" b:"b" c:"c" { }', - extractor: extractNode, - params: { a: "result0[0]", b: "result0[1]", c: "result0[2]" } - }, - { - grammar: 'start = a:"a" (b:"b" c:"c" d:"d") e:"e"{ }', - extractor: extractNode, - params: { - a: "result0[0]", - b: "result0[1][0]", - c: "result0[1][1]", - d: "result0[1][2]", - e: "result0[2]" - } - }, - /* - * Regression tests for a bug where e.g. resultVar names like |result10| - * were incorrectly treated as names derived from |result1|, leading to - * incorrect substitution. - */ - { - grammar: 'start = ("a" "b" "c" "d" "e" "f" "g" "h" "i" j:"j" { })*', - extractor: extractExpression, - params: { j: "result1[9]" } // Buggy code put "result1[0]0" here. - } - ]; - - for (var i = 0; i < cases.length; i++) { - var ast = PEG.parser.parse(cases[i].grammar); - PEG.compiler.passes.computeVarNames(ast); - PEG.compiler.passes.computeParams(ast); - - deepEqual( - cases[i].extractor(ast.rules[0].expression).params, - cases[i].params - ); - } -}); - -})(); diff --git a/test/compiler/passes/compute-params-test.js b/test/compiler/passes/compute-params-test.js new file mode 100644 index 0000000..2f02afe --- /dev/null +++ b/test/compiler/passes/compute-params-test.js @@ -0,0 +1,167 @@ +(function() { + +module("PEG.compiler.passes.computeParams"); + +test("computes params", function() { + function extractNode(node) { return node; } + function extractExpression(node) { return node.expression; } + + var cases = [ + /* Bacics */ + { + grammar: 'start = a:"a" { }', + extractor: extractNode, + params: { a: "result0" } + }, + { + grammar: 'start = a:"a" &{ }', + extractor: function(node) { return node.elements[1]; }, + params: { a: "result0" } + }, + { + grammar: 'start = a:"a" !{ }', + extractor: function(node) { return node.elements[1]; }, + params: { a: "result0" } + }, + + /* Recursive walk */ + { + grammar: 'start = a:"a" { } / "b" / "c"', + extractor: function(node) { return node.alternatives[0]; }, + params: { a: "result0" } + }, + { + grammar: 'start = "a" / "b" / c:"c" { }', + extractor: function(node) { return node.alternatives[2]; }, + params: { c: "result0" } + }, + { + grammar: 'start = (a:"a" { }) "b" "c"', + extractor: function(node) { return node.elements[0]; }, + params: { a: "result0" } + }, + { + grammar: 'start = "a" "b" (c:"c" { })', + extractor: function(node) { return node.elements[2]; }, + params: { c: "result2" } + }, + { + grammar: 'start = a:(b:"b" { })', + extractor: extractExpression, + params: { b: "result0" } + }, + { + grammar: 'start = &(a:"a" { })', + extractor: extractExpression, + params: { a: "result0" } + }, + { + grammar: 'start = !(a:"a" { })', + extractor: extractExpression, + params: { a: "result0" } + }, + { + grammar: 'start = (a:"a" { })?', + extractor: extractExpression, + params: { a: "result0" } + }, + { + grammar: 'start = (a:"a" { })*', + extractor: extractExpression, + params: { a: "result1" } + }, + { + grammar: 'start = (a:"a" { })+', + extractor: extractExpression, + params: { a: "result1" } + }, + { + grammar: 'start = (a:"a" { }) { }', + extractor: extractExpression, + params: { a: "result0" } + }, + + /* Scoping */ + { + grammar: 'start = (a:"a" / b:"b" / c:"c") { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = a:(b:"b") { }', + extractor: extractNode, + params: { a: "result0" } + }, + { + grammar: 'start = &(a:"a") { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = !(a:"a") { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = (a:"a")? { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = (a:"a")* { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = (a:"a")+ { }', + extractor: extractNode, + params: { } + }, + { + grammar: 'start = (a:"a" { }) { }', + extractor: extractNode, + params: { } + }, + + /* Sequences */ + { + grammar: 'start = a:"a" b:"b" c:"c" { }', + extractor: extractNode, + params: { a: "result0[0]", b: "result0[1]", c: "result0[2]" } + }, + { + grammar: 'start = a:"a" (b:"b" c:"c" d:"d") e:"e"{ }', + extractor: extractNode, + params: { + a: "result0[0]", + b: "result0[1][0]", + c: "result0[1][1]", + d: "result0[1][2]", + e: "result0[2]" + } + }, + /* + * Regression tests for a bug where e.g. resultVar names like |result10| + * were incorrectly treated as names derived from |result1|, leading to + * incorrect substitution. + */ + { + grammar: 'start = ("a" "b" "c" "d" "e" "f" "g" "h" "i" j:"j" { })*', + extractor: extractExpression, + params: { j: "result1[9]" } // Buggy code put "result1[0]0" here. + } + ]; + + for (var i = 0; i < cases.length; i++) { + var ast = PEG.parser.parse(cases[i].grammar); + PEG.compiler.passes.computeVarNames(ast); + PEG.compiler.passes.computeParams(ast); + + deepEqual( + cases[i].extractor(ast.rules[0].expression).params, + cases[i].params + ); + } +}); + +})(); diff --git a/test/compiler/passes/compute-var-names-test.js b/test/compiler/passes/compute-var-names-test.js new file mode 100644 index 0000000..1f75016 --- /dev/null +++ b/test/compiler/passes/compute-var-names-test.js @@ -0,0 +1,231 @@ +(function() { + +module("PEG.compiler.passes.computeVarNames"); + +test("computes variable names", function() { + var leafDetails = { resultVar: "result0" }, + choiceDetails = { + resultVar: "result0", + alternatives: [ + { resultVar: "result0", posVar: "pos0" }, + { resultVar: "result0", posVar: "pos0" }, + { resultVar: "result0", posVar: "pos0" } + ] + }, + sequenceDetails = { + resultVar: "result0", + posVar: "pos0", + elements: [ + { resultVar: "result0", posVar: "pos1" }, + { resultVar: "result1", posVar: "pos1" }, + { resultVar: "result2", posVar: "pos1" } + ] + }; + + var cases = [ + /* Choice */ + { + grammar: 'start = &"a" / &"b" / &"c"', + resultVars: ["result0"], + posVars: ["pos0"], + details: choiceDetails + }, + { + grammar: 'start = &"a" / &"b"* / &"c"', + resultVars: ["result0", "result1"], + posVars: ["pos0"], + details: choiceDetails + }, + { + grammar: 'start = &"a" / &(&"b") / &"c"', + resultVars: ["result0"], + posVars: ["pos0", "pos1"], + details: choiceDetails + }, + + /* Sequence */ + { + grammar: 'start = ', + resultVars: ["result0"], + posVars: ["pos0"], + details: { resultVar: "result0", posVar: "pos0" } + }, + { + grammar: 'start = &"a" &"b" &"c"', + resultVars: ["result0", "result1", "result2"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &"a" &"b" &"c"*', + resultVars: ["result0", "result1", "result2", "result3"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &"a" &"b"* &"c"', + resultVars: ["result0", "result1", "result2"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &"a" &("b"*)* &"c"', + resultVars: ["result0", "result1", "result2", "result3"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &"a"* &"b" &"c"', + resultVars: ["result0", "result1", "result2"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &("a"*)* &"b" &"c"', + resultVars: ["result0", "result1", "result2"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &(("a"*)*)* &"b" &"c"', + resultVars: ["result0", "result1", "result2", "result3"], + posVars: ["pos0", "pos1"], + details: sequenceDetails + }, + { + grammar: 'start = &"a" &(&"b") &"c"', + resultVars: ["result0", "result1", "result2"], + posVars: ["pos0", "pos1", "pos2"], + details: sequenceDetails + }, + + /* Others */ + { + grammar: 'start = label:&"a"', + resultVars: ["result0"], + posVars: ["pos0"], + details: { + resultVar: "result0", + expression: { resultVar: "result0", posVar: "pos0" } + } + }, + { + grammar: 'start = &(&"a")', + resultVars: ["result0"], + posVars: ["pos0", "pos1"], + details: { + resultVar: "result0", + posVar: "pos0", + expression: { resultVar: "result0", posVar: "pos1" } + } + }, + { + grammar: 'start = !(&"a")', + resultVars: ["result0"], + posVars: ["pos0", "pos1"], + details: { + resultVar: "result0", + posVar: "pos0", + expression: { resultVar: "result0", posVar: "pos1" } + } + }, + { + grammar: 'start = &{ code }', + resultVars: ["result0"], + posVars: [], + details: leafDetails + }, + { + grammar: 'start = !{ code }', + resultVars: ["result0"], + posVars: [], + details: leafDetails + }, + { + grammar: 'start = (&"a")?', + resultVars: ["result0"], + posVars: ["pos0"], + details: { + resultVar: "result0", + expression: { resultVar: "result0", posVar: "pos0" } + } + }, + { + grammar: 'start = (&"a")*', + resultVars: ["result0", "result1"], + posVars: ["pos0"], + details: { + resultVar: "result0", + expression: { resultVar: "result1", posVar: "pos0" } + } + }, + { + grammar: 'start = (&"a")+', + resultVars: ["result0", "result1"], + posVars: ["pos0"], + details: { + resultVar: "result0", + expression: { resultVar: "result1", posVar: "pos0" } + } + }, + { + grammar: 'start = &"a" { code }', + resultVars: ["result0"], + posVars: ["pos0", "pos1"], + details: { + resultVar: "result0", + posVar: "pos0", + expression: { resultVar: "result0", posVar: "pos1" } + } + }, + { + grammar: 'start = a', + resultVars: ["result0"], + posVars: [], + details: leafDetails + }, + { + grammar: 'start = "a"', + resultVars: ["result0"], + posVars: [], + details: leafDetails + }, + { + grammar: 'start = .', + resultVars: ["result0"], + posVars: [], + details: leafDetails + }, + { + grammar: 'start = [a-z]', + resultVars: ["result0"], + posVars: [], + details: leafDetails + } + ]; + + function checkDetails(node, details) { + for (var key in details) { + if (Object.prototype.toString.call(details[key]) === "[object Array]") { + for (var i = 0; i < details[key].length; i++) { + checkDetails(node[key][i], details[key][i]); + } + } else if (typeof details[key] === "object") { + checkDetails(node[key], details[key]); + } else { + strictEqual(node[key], details[key]); + } + } + } + + for (var i = 0; i < cases.length; i++) { + var ast = PEG.parser.parse(cases[i].grammar); + PEG.compiler.passes.computeVarNames(ast); + + deepEqual(ast.rules[0].resultVars, cases[i].resultVars); + deepEqual(ast.rules[0].posVars, cases[i].posVars); + checkDetails(ast.rules[0].expression, cases[i].details); + } +}); + +})(); diff --git a/test/compiler/passes/remove-proxy-rules-test.js b/test/compiler/passes/remove-proxy-rules-test.js new file mode 100644 index 0000000..ecb61f9 --- /dev/null +++ b/test/compiler/passes/remove-proxy-rules-test.js @@ -0,0 +1,152 @@ +(function() { + +module("PEG.compiler.passes.removeProxyRules"); + +test("removes proxy rules", function() { + function simpleGrammar(rules, startRule) { + return { + type: "grammar", + initializer: null, + rules: rules, + startRule: startRule + }; + } + + var proxiedRule = { + type: "rule", + name: "proxied", + displayName: null, + expression: { type: "literal", value: "a", ignoreCase: false } + }; + + var proxiedRuleRef = { + type: "rule_ref", + name: "proxied" + }; + + function simpleGrammarWithStartAndProxied(startRuleExpression) { + return simpleGrammar( + [ + { + type: "rule", + name: "start", + displayName: null, + expression: startRuleExpression + }, + proxiedRule + ], + "start" + ); + } + + var cases = [ + { + grammar: 'start = proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammar([proxiedRule], "proxied") + }, + { + grammar: 'start = proxy / "a" / "b"; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "choice", + alternatives: [ + proxiedRuleRef, + { type: "literal", value: "a", ignoreCase: false }, + { type: "literal", value: "b", ignoreCase: false } + ] + }) + }, + { + grammar: 'start = "a" / "b" / proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "choice", + alternatives: [ + { type: "literal", value: "a", ignoreCase: false }, + { type: "literal", value: "b", ignoreCase: false }, + proxiedRuleRef + ] + }) + }, + { + grammar: 'start = proxy "a" "b"; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "sequence", + elements: [ + proxiedRuleRef, + { type: "literal", value: "a", ignoreCase: false }, + { type: "literal", value: "b", ignoreCase: false } + ] + }) + }, + { + grammar: 'start = "a" "b" proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "sequence", + elements: [ + { type: "literal", value: "a", ignoreCase: false }, + { type: "literal", value: "b", ignoreCase: false }, + proxiedRuleRef + ] + }) + }, + { + grammar: 'start = label:proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "labeled", + label: "label", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = &proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "simple_and", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = !proxy; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "simple_not", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = proxy?; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "optional", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = proxy*; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "zero_or_more", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = proxy+; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "one_or_more", + expression: proxiedRuleRef + }) + }, + { + grammar: 'start = proxy { }; proxy = proxied; proxied = "a"', + ast: simpleGrammarWithStartAndProxied({ + type: "action", + code: " ", + expression: proxiedRuleRef + }) + } + ]; + + for (var i = 0; i < cases.length; i++) { + var ast = PEG.parser.parse(cases[i].grammar); + PEG.compiler.passes.removeProxyRules(ast); + + deepEqual(ast, cases[i].ast); + } +}); + +})(); diff --git a/test/compiler/passes/report-left-recursion-test.js b/test/compiler/passes/report-left-recursion-test.js new file mode 100644 index 0000000..d41b12c --- /dev/null +++ b/test/compiler/passes/report-left-recursion-test.js @@ -0,0 +1,40 @@ +(function() { + +module("PEG.compiler.passes.reportLeftRecursion"); + +test("reports left recursion", function() { + function testGrammar(grammar) { + raises( + function() { + var ast = PEG.parser.parse(grammar); + PEG.compiler.passes.reportLeftRecursion(ast); + }, + function(e) { + return e instanceof PEG.GrammarError + && e.message === "Left recursion detected for rule \"start\"."; + } + ); + } + + var grammars = [ + /* Direct */ + 'start = start', + 'start = start / "a" / "b"', + 'start = "a" / "b" / start', + 'start = start "a" "b"', + 'start = label:start', + 'start = &start', + 'start = !start', + 'start = start?', + 'start = start*', + 'start = start+', + 'start = start { }', + + /* Indirect */ + 'start = stop; stop = start' + ]; + + for (var i = 0; i < grammars.length; i++) { testGrammar(grammars[i]); } +}); + +})(); diff --git a/test/compiler/passes/report-missing-rules-test.js b/test/compiler/passes/report-missing-rules-test.js new file mode 100644 index 0000000..17b6200 --- /dev/null +++ b/test/compiler/passes/report-missing-rules-test.js @@ -0,0 +1,37 @@ +(function() { + +module("PEG.compiler.passes.reportMissingRules"); + +test("reports missing referenced rules", function() { + function testGrammar(grammar) { + raises( + function() { + var ast = PEG.parser.parse(grammar); + PEG.compiler.passes.reportMissingRules(ast); + }, + function(e) { + return e instanceof PEG.GrammarError + && e.message === "Referenced rule \"missing\" does not exist."; + } + ); + } + + var grammars = [ + 'start = missing', + 'start = missing / "a" / "b"', + 'start = "a" / "b" / missing', + 'start = missing "a" "b"', + 'start = "a" "b" missing', + 'start = label:missing', + 'start = &missing', + 'start = !missing', + 'start = missing?', + 'start = missing*', + 'start = missing+', + 'start = missing { }' + ]; + + for (var i = 0; i < grammars.length; i++) { testGrammar(grammars[i]); } +}); + +})(); diff --git a/test/index.html b/test/index.html index 262b103..e6beba0 100644 --- a/test/index.html +++ b/test/index.html @@ -9,7 +9,11 @@ - + + + + +

PEG.js Test Suite

diff --git a/test/run b/test/run index 34d9159..d8fd56a 100755 --- a/test/run +++ b/test/run @@ -77,7 +77,11 @@ QUnit.done(function(details) { "helpers.js", "parser-test.js", "compiler-test.js", - "compiler/passes-test.js" + "compiler/passes/report-missing-rules-test.js", + "compiler/passes/report-left-recursion-test.js", + "compiler/passes/remove-proxy-rules-test.js", + "compiler/passes/compute-var-names-test.js", + "compiler/passes/compute-params-test.js" ].forEach(function(file) { eval("with (QUnit) {" + fs.readFileSync(__dirname + "/" + file, "utf8") + "}"); });