"use strict";
/** @fileOverview Implements `class page.LL1`.
* @author Axel T. Schreiner <ats@cs.rit.edu>
* @version 1.5.0
*/
/** The result of {@link page.trace}, {@link page.LL1#reflect}, or {@link page.SLR1#reflect}: a function which can observe a parser.
* @typedef Observer
* @type {function(number, page.Tuple, string, (page.BNF.Rule|page.BNF.T|string), object): object}
* @param {number} _at where the parser is, e.g., a rule or state number.
* @param {page.Tuple} _tuple current input.
* @param {string} _action `'rule'`, `'shift'`, or `'reduce'` for LL(1),
* `'shift'`, `'reduce'`, `'goto'`, or `'accept'` for SLR(1), or `'error'`.
* @param {page.BNF.Rule|page.BNF.T|number|string} _info a reference to the relevant rule, terminal,
* or state number, or an error message.
* @param {object} _sender of this message, e.g., {@link page.LL1} or {@link page.SLR1}.
* @returns {object} anything on success.
* @throws {string|Error} a string with an error message to continue parsing
* or an `Error` with an error message to abort further parsing.
*/
/** Builder function implementing a semantic action for {@link page.LL1#reflect} and {@link page.SLR1#reflect}.
* @typedef Builder
* @type {function (object[], object, page.Tuple, object) : object}
* @param {object[]} _values top-level value list, collected for this semantic action.
* @param {object} _env environment, originally connected to the observer.
* @param {page.Tuple} _tuple current input.
* @param {object} _sender of this message, e.g., {@link page.LL1} or {@link page.SLR1}.
* @returns {object|object[]} a value which will be pushed, or concatenated by the observer.
* @throws {string|Error} a string with an error message to continue parsing
* or an `Error` with an error message to abort further parsing.
*/
/** Wraps a suitable grammar for top-down parsing; checks if a grammar is suitable, i.e., LL(1).
* @class Tests and wraps a LL(1) grammar for top-down parsing.
* An object of this class supports one call to a parsing algorithm at any one time.
* The object is serially reusable.
*
* {@link page.LL1#parser} is a recursive parsing algorithm for {@link page.BNF} and {@link page.EBNF} grammars.
* {@link page.LL1#parse} is a stack-based parsing algorithm for {@link page.BNF} grammars (which can be derived from {@link page.EBNF} grammars).
* The algorithms have different strategies for error recovery.
*
* Either algorithm can pull input from a callback function. Successive pieces of input can be pushed
* to successive calls of {@link page.LL1#parse}.
*
* A BNF grammar undergoes the following tests:
*
* | node | check action |
* |------|--------------|
* | {@link page.BNF} | delegate to all non-terminals.
* | {@link page.BNF.NT} | `first` of alternative right-hand sides in rules should be pairwise disjoint, `first` and `follow` should not overlap if empty input is acceptable.
* | {@link page.BNF.T} | nothing to do.
*
* If a non-terminal is defined with left recursion, it will have ambiguous alternatives.
* Such a grammar should not be used here.
*
* An EBNF grammar could be checked by first converting to BNF.
* However, the following tests relate errors better to the original grammar:
*
* | node | check action |
* |------|--------------|
* | {@link page.EBNF} | delegate to rules' right-hand sides.
* | {@link page.EBNF.NT} | `first` and `follow` should not overlap if it accepts empty input.
* | {@link page.BNF.T } | nothing to do.
* | {@link page.EBNF.Alt} | `first` and `follow` should not overlap if it accepts empty input; `first` of choices should be pairwise disjoint; delegate to choices.
* | {@link page.EBNF.Lst} | `first` and `follow` should not overlap; `first` of body and delimiter should be pairwise disjoint if either accepts empty input; delegate to body and delimiter.
* | {@link page.EBNF.Rep} | `first` and `follow` should not overlap; delegate to body.
* | {@link page.EBNF.Seq} | `first` and `follow` should not overlap if it accepts empty input; delegate to terms.
*
* @param {page.BNF|page.EBNF} _grammar to be used for parsing; it should not be left-recursive.
* Either grammar is checked whether it is LL(1).
* @param {Observer} [_observer] to observe recognition.
*
* @property {page.BNF} bnf BNF grammar to control recognition.
* @property {page.EBNF} ebnf EBNF grammar to control recognition, if available.
* @property {Observer} observer to observe recognition.
* @property {number} greedy number of `first`/`follow` overlaps,
* would be resolved in favor of `first`.
* @property {number} ambiguous number of `first` overlaps in alternatives
* of the same non-terminal, would be resolved in favor of the first alternative
* but usually makes the grammar unsuitable because it can indicate left recursion.
* @property {number} errors counted by {@link page.error} method,
* reset just before parsing is started.
*
* @property {Array<{rule: page.BNF.Rule, position: number}>} states currently active rules,
* exists only while {@link page.LL1#parse} is used.
*
* @property {Array<Array<object>>} values currently active value lists,
* exists only while {@link page.LL1#reflect} is used.
*/
page.LL1 = function (_grammar, _observer) {
var self = this;
self.assert('__WHERE__', _grammar instanceof page.BNF);
!_observer || self.assert('__WHERE__', typeof _observer == 'function');
self.bnf = undefined;
self.ebnf = undefined;
if (_observer) self.observer = _observer;
self.greedy = 0;
self.ambiguous = 0;
self.errors = 0;
if (_grammar instanceof page.EBNF) {
self.ebnf = _grammar;
self.bnf = _grammar.bnf;
// import error count if any
self.errors = self.ebnf.errors; self.ebnf.errors = 0;
var visit = function (node) {
if (node instanceof page.EBNF.Alt) { // first and follow should not overlap if it accepts empty input
// first of choices should be pairwise disjoint
// delegate to choices.
if (node.empty && page.overlap(node.first, node.follow)) {
self.message(node.nt.name, 'has overlapping first and follow sets (alt)');
++ self.greedy;
}
// choices disjoint?
if (node.nodes.some(function (nodeA, a) {
return node.nodes.slice(a + 1).some(function (nodeB) {
return page.overlap(nodeA.first, nodeB.first);
})
})) {
self.message(node.nt.name, 'has ambiguous alternatives (alt)');
++ self.ambiguous;
}
// choices?
node.nodes.forEach(function (choice) { visit(choice); });
}
else if (node instanceof page.EBNF.Lst) { // first and follow should not overlap
// first of descendants should be disjoint if either accepts empty input
// delegate to descendants.
if (page.overlap(node.first, node.follow)) {
self.message(node.nt.name, 'has overlapping first and follow sets (list)');
++ self.greedy;
}
if ((node.node.empty || node.delim.empty) && page.overlap(node.node.first, node.delim.first)) {
self.message(node.nt.name, 'has overlapping body and delimiter');
++ self.greedy;
}
visit(node.node);
visit(node.delim);
}
else if (node instanceof page.EBNF.NT) { // first and follow should not overlap if it accepts empty input.
if (node.empty && page.overlap(node.first, node.follow)) {
self.message(node.name, 'has overlapping first and follow sets (nt)');
++ self.greedy;
}
}
else if (node instanceof page.EBNF.Rep) { // first and follow should not overlap
// delegate to descendant.
if (page.overlap(node.first, node.follow)) {
self.message(node.nt.name, 'has overlapping first and follow sets (rep)');
++ self.greedy;
}
visit(node.node);
}
else if (node instanceof page.EBNF.Seq) { // first and follow should not overlap if it accepts empty input
// delegate to descendants.
if (node.empty && page.overlap(node.first, node.follow)) {
self.message(node.nt.name, 'has overlapping first and follow sets (seq)');
++ self.greedy;
}
node.nodes.forEach(function (symbol) { visit(symbol); });
}
else if (node instanceof page.BNF.T) { // nothing to do
}
else self.assert('__WHERE__');
};
// check
// delegate to rules' right-hand sides.
self.ebnf.rules.forEach(function (rule) { visit(rule.symbol); });
} else {
self.bnf = _grammar;
// import error count if any
self.errors = self.bnf.errors; self.bnf.errors = 0;
// check if BNF grammar is LL(1)
self.bnf.nts.forEach(function (nt) {
// first/follow if empty?
if (nt.empty && page.overlap(nt.first, nt.follow)) {
self.message(nt.name, 'has overlapping first and follow sets');
++ self.greedy;
}
// alternatives?
if (nt.rules.some(function (ruleA, a) {
return nt.rules.slice(a + 1).some(function (ruleB) {
return page.overlap(ruleA.first, ruleB.first);
})
})) {
self.message(nt.name, 'has ambiguous alternatives');
++ self.ambiguous;
}
});
}
};
page.baseclass(page.LL1, 'page.LL1');
/** Delegates to `.bnf` to create a function which tokenizes a string.
*
* @param {string|RegExp} [_skip] a pattern to define ignorable character sequences;
* this pattern must not accept empty input and it must use `(:? )`
* rather than `( )` for grouping; the default is to skip white space.
*
* @returns {Tokenizer} a function which takes a string and returns a list of {@link page.Tuple} elements.
* The list contains one tuple with the `error` token for each
* character which is neither ignorable nor a literal or part of a token.
* The function is based on a regular expression which can be displayed as a member `.pattern`.
*
* @see page.BNF#tokenizer
*/
page.LL1.prototype.tokenizer = function (_skip) {
return this.bnf.tokenizer(_skip);
};
/** Displays number of errors and overlaps, if any.
* @override
*
* @returns {string}
*/
page.LL1.prototype.toString = function () {
var result = this.errors ? 'errors: ' + this.errors : '';
if (this.greedy || this.ambiguous) {
if (this.greedy > 0)
result += '\nfirst/follow overlaps: ' + this.greedy;
if (this.ambiguous > 0)
result += '\nambiguous alternatives: ' + this.ambiguous;
}
return result;
};
/** Displays errors, overlaps, and active rules and delegates to the grammar.
* @override
*
* @returns {string}
*/
page.LL1.prototype.dump = function () {
var result = this.toString();
if (result.length > 0) result += '\n\n';
if ('states' in this)
result += 'active rules\n ' +
this.states.slice(1).concat([ this.top ]).map(function (state) {
return state.rule.toString(state.position);
}).join('\n ') + '\n\n';
if ('values' in this)
result += 'collected values\n ' +
this.values.map(function (values) {
return '[ ' + values.join(', ') + ' ]';
}).join('\n ') + '\n\n';
return result + (this.ebnf ? this.ebnf.dump() : this.bnf.dump());
};
/** Checks top-down if input conforms to a BNF grammar.
* For an EBNF grammar this method uses the corresponding BNF grammar.
* This method can be called repeatedly with portions of input.
*
* | node | parse action |
* |------|--------------|
* | {@link page.BNF.NT} | compare next input and `first` to visit the first possible rule for the non-terminal and return the result. It is an error if no rule can be visited and the non-terminal cannot accept no input.
* | {@link page.BNF.Rule} | send a `'rule'` message to the observer if any; visit the descendants; send a `'reduce'` message. It is an error if the observer returns a string.
* | {@link page.BNF.T} | send a `'shift'` message to the observer if any and advance in input. It is an error if the observer returns a string.
*
* The method uses a stack of active rules and attempts error recovery.
* If an input symbol cannot be matched, the parser function will search the rest of the current
* and all outer active rules for a possible match with the input symbol.
* If there is no match, the input symbol is discarded and the search is tried again
* until an `irrecoverable error` is reported at end of input.
*
* The method is greedy, i.e., it will match input to a rule as long as possible;
* therefore, some types of ambiguous grammars are still suitable for parsing.
*
* ##### Create parsers and scanner
* var parser = new page.LL1(page.BNF.from(' ... grammar described in BNF ...'));
* var parser = new page.LL1(page.EBNF.from(' ... grammar described in EBNF ...'));
* var scanner = parser.tokenizer(/ skip pattern /);
*
* ##### Parse -- all input
* parser.parse(scanner('... input ...').concat([ null ]));
*
* ##### Parse -- push tuples
* var result = parser.parse(scanner('... start of input ...'));
* while (result === true)
* result = parser.parse(scanner('... more input ...')); // eventually null input
*
* ##### Parse a stream -- pull tuples
* function stream () { return scanner('... start/more input/null ...'); }
* parser.parse(stream);
*
* ##### Trace
* parser.parse(stream, page.trace());
*
* ##### Collect token values
* var result = parser.parse(stream, parser.reflect());
* print(result); // array with token values
*
* ##### Create observer for traced building
* var environment = ... something useful for the builder ...;
* var builder = {
* ruleName: // matched by reflection
* function (_values, _environment, _tuple, _sender) {
* return ... anything
* throw string for error message
* throw Error with message to abort
* }
* ...
* };
* var observer = page.trace(parser.reflect(environment, builder));
*
* ##### Build
* var result = parser.parse(stream, observer);
* print(result); // array with builder value(s)
*
* @param {page.Tuple[]|Tokenizer} _input either the next portion of input
* and `null` or an empty array will match `$eof`, or a {@link Tokenizer} which will be called *without arguments*
* and each time must return the next portion of input, `null`, or an empty array.
* @param {Observer} [_observer] to observe recognition;
* cannot be specified once parsing is in progress.
*
* @returns {boolean|object} `true` to request another input array,
* or the top-level value (which could be `null`) returned by the observer, if any, when the end of input is reached.
*
* @throws {Error} from the observer to abort parsing.
*
* @see page.trace
*/
page.LL1.prototype.parse = function (_input, _observer) {
var self = this; // closure
var current = null; // current input tuple, if any
var tuples = []; // available tuples
var index = 0; // index of next tuple
// set current to a new tuple, but not past $eof
// throw true to ask for more input
function next () {
if (index >= tuples.length) { // no tuples left
if (typeof _input == 'function') // call back for more
tuples = _input();
else if (_input) { // consume argument
tuples = _input;
_input = false;
} else if (_input === false) // throw true for more input
throw true;
else // null input: EOF
tuples = null;
if (!tuples || tuples.length == 0) // EOF
tuples = [ new page.Tuple(0, self.bnf.lit(), '') ];
index = 0;
}
// use 'index' tuple
if ((current = tuples[index]) == null)
current = tuples[index] = new page.Tuple(0, self.bnf.lit(), '');
// advance index (to later tokenize more) but don't run past $eof tuple
if (current.t != self.bnf.lit()) ++ index;
}
// inform observer, if any
// if error action: print result or info if any; return null
// rethrow Error; return result or null
function observe (_action, _info) {
var result = _action == 'error' ? _info : null;
var at = _action == 'rule' ? _info.index : self.top.rule.index;
if (self.observer)
try {
result = self.observer(at, current, _action, _info, self);
} catch (e) {
if (typeof e == 'string')
self.error(e);
else {
self.assert('__WHERE__', e instanceof Error)
throw 'lineNumber' in e ? new Error('('+e.lineNumber+') ' + e.message) : e;
}
}
if (_action == 'error') {
if (result) self.error(result);
return null;
}
return result;
}
// if current input matches somewhere in
// _config state stack element
// set elements position accordingly and return true.
function recover (_config) {
for (var pos = _config.position; pos < _config.rule.symbols.length; ++ pos)
if (current.t.ord in _config.rule.symbols[pos].first) {
_config.position = pos;
return true;
}
return false;
}
self.assert('__WHERE__', typeof _input != 'boolean');
if ('states' in self) {
// multiple calls in progress, only for tuple array, no new observer
!_input || self.assert('__WHERE__', _input instanceof Array);
self.assert('__WHERE__', !_observer);
} else {
!_input || self.assert('__WHERE__', _input instanceof Array || typeof _input == 'function');
!_observer || self.assert('__WHERE__', typeof _observer == 'function');
if (_observer) self.observer = _observer;
// begin parsing: create stack and initialize top to rule 0
self.states = [ null ];
self.top = { rule: self.bnf.rules[0], position: 0 };
next();
observe('rule', self.bnf.rules[0]);
}
try {
var result = null, throttle = false;
loop: while (self.top) { // any active rule?
if (! current) // input available?
next();
if (self.top.position < self.top.rule.symbols.length) { // symbol on rhs left?
var symbol = self.top.rule.symbols[self.top.position];
if (symbol instanceof page.BNF.T) { // terminal?
if (symbol == current.t) { // equal to input?
observe('shift', symbol);
++ self.top.position; // done with symbol
current = null; // done with input
throttle = false; // allow messages
continue loop;
}
} else if (current.t.ord in symbol.first) { // non-terminal, input in first?
if (symbol.rules.some(function (rule) { // input in first of some rule?
if (current.t.ord in rule.first) {
observe('rule', rule);
++ self.top.position; // done with symbol
self.states.push(self.top); // activate rule
self.top = { rule: rule, position: 0 };
return true;
}
return false;
}))
continue loop;
self.assert('__WHERE__'); // cannot happen: input must be in one of the first sets
} else if (symbol.empty && current.t.ord in symbol.follow) { // empty input ok?
++ self.top.position; // done with symbol
continue loop;
}
if (!throttle) {
// produce a message listing expected symbols in symbol.first along the rule
var msg = current + ' is not allowed in "'+ self.top.rule + '"\nexpecting:';
var expect = {};
for (var p = self.top.position; p < self.top.rule.symbols.length; ++ p) {
var s = self.top.rule.symbols[p];
page.merge(expect, s.first);
if (!s.empty) break;
}
if (p >= self.top.rule.symbols.length) page.merge(expect, self.top.rule.nt.follow);
for (var e in expect) msg += ' ' + expect[e];
// run the message past the observer (which may throw a string).
observe('error', msg);
throttle = true;
}
// try to recover back to the bottom of the stack.
if (recover(self.top)) continue loop;
for (var s = self.states.length; -- s > 0; )
if (recover(self.states[s])) {
self.top = self.states[s];
self.states = self.states.slice(0, s);
continue loop;
}
// if unable, discard current input (which may cause subsequent messages).
self.message('discarding ' + current);
current = null;
} else { // no symbol on rhs left, done with rule
result = observe('reduce', self.top.rule);
self.top = self.states.pop();
}
}
throw [ result ];
} catch (outcome) {
if (outcome === true) return true;
// end parsing: remove stack
delete self.states;
delete self.top;
delete self.values;
if (outcome instanceof Array) return outcome[0];
self.assert('__WHERE__', outcome instanceof Error);
throw 'lineNumber' in outcome ? new Error('('+outcome.lineNumber+') ' + outcome.message) : outcome;
}
};
/** Checks top-down if input conforms to an EBNF or BNF grammar.
*
* Starting with the start symbol, this method recursively visits the grammar.
* A node will only be visited if the next input symbol
* is in the set of first terminals of the node.
*
* | node | parse action |
* |------|--------------|
* | {@link page.EBNF.NT} | delegate to the corresponding rule.
* | {@link page.EBNF.Rule} | send a `'rule'` message to the observer if any; visit the descendant; send a `'reduce'` message. It is an error if the observer returns a string.
* | {@link page.EBNF.Alt} | visit the first possible alternative and return the result.
* | {@link page.EBNF.Lst} | visit the body and then delimiter and body as often as possible and allowed; return an error if there are not enough iterations, or return the last result or the first string returned by the descendant.
* | {@link page.EBNF.Rep} | visit the descendant as often as possible and allowed; return an error if there are not enough iterations, or return the last result or the first string returned by the descendant.
* | {@link page.EBNF.Seq} | visit the descendants; return an error if a descendant cannot be visited, or return `null` or the first string returned by a descendant.
* | {@link page.BNF.T} | send a `'shift'` message to the observer if any and advance in input. It is an error if the observer returns a string.
*
* The method employs recursion, rather than a rule stack, and cannot perform deep error recovery.
*
* The method is greedy, i.e., it will match input to a rule as long as possible;
* therefore, some types of ambiguous grammars are still suitable for parsing.
*
* ##### Create parsers and scanner
* var parser = new page.LL1(page.BNF.from(' ... grammar described in BNF ...'));
* var parser = new page.LL1(page.EBNF.from(' ... grammar described in EBNF ...'));
* var scanner = parser.tokenizer(/ skip pattern /);
*
* ##### Parse string
* parser.parser(scanner('... input ...'));
*
* ##### Parse a stream — pull tuples
* function stream () { return scanner('... start/more input/null ...'); }
* parser.parser(stream);
*
* ##### Trace
* parser.parser(stream, page.trace());
*
* ##### Collect token values
* var result = parser.parser(stream, parser.reflect());
* print(result); // array with token values
*
* ##### Create observer for traced building
* var environment = ... something useful for the builder ...;
* ruleName: // matched by reflection
* function (_values, _environment, _tuple, _sender) {
* return ... anything
* throw string for error message
* throw Error with message to abort
* }
* ...
* };
* var observer = page.trace(parser.reflect(environment, builder));
*
* ##### Build
* var result = parser.parser(stream, observer);
* print(result); // array with builder value(s)
*
* @param {page.Tuple[]|Tokenizer} _input either the complete input
* or a {@link Tokenizer} which will be called *without arguments*
* and each time must return the next portion of input, `null`, or an empty array.
* @param {Observer} [_observer] to observe recognition.
*
* @returns {object} the top-level value (which could be `null`) returned by the observer, if any, when the end of input is reached.
*
* @throws {Error} from the observer to abort parsing.
*
* @see page.trace
*/
page.LL1.prototype.parser = function (_input, _observer) {
var self = this;
self.assert('__WHERE__', _input instanceof Array || typeof _input == 'function');
!_observer || self.assert('__WHERE__', typeof _observer == 'function');
if (_observer) self.observer = _observer;
// begin parsing
self.errors = 0;
var grammar = self.ebnf ? self.ebnf : self.bnf; // grammar to use
var current = null; // current input tuple, if any
var currentRule = undefined; // index of current rule for observer
var tuples = []; // available tuples
var index = 0; // index of next tuple to become current
// set current to a new tuple, but not past $eof
function next () {
if (index >= tuples.length) { // no tuples left
if (typeof _input == 'function') // call back for more
tuples = _input();
else if (_input) { // consume argument
tuples = _input;
_input = false;
} else // consumed; set EOF
tuples = null;
if (!tuples || tuples.length == 0) // EOF
tuples = [ new page.Tuple(0, grammar.lit(), '') ];
index = 0;
}
// use 'index' tuple
if ((current = tuples[index]) == null)
current = tuples[index] = new page.Tuple(0, grammar.lit(), '');
// advance index (to later tokenize more) but don't run past $eof tuple
if (current.t != grammar.lit()) ++ index;
}
// if rule or reduce action: set currentRule
// inform observer, if any
// if error action: print result or info if any; return null
// rethrow Error; return result or null
function observe (_action, _info) {
var result = _action == 'error' ? _info : null;
if (_action == 'rule' || _action == 'reduce') currentRule = _info.index;
if (self.observer)
try {
result = self.observer(currentRule, current, _action, _info, self);
} catch (e) {
if (typeof e == 'string')
self.error(e);
else {
self.assert('__WHERE__', e instanceof Error)
throw 'lineNumber' in e ? new Error('('+e.lineNumber+') ' + e.message) : e;
}
}
if (_action == 'error') {
if (result) self.error(result);
return null;
}
return result;
}
// produce a message listing expected symbols
// context 'this' node being visited
// node to check for first and (if exists) follow
// follow of context to abandon visit
// run the message past the observer (which may throw a string).
// else discard input until current input fits one of
// return 1 for follow if any
// return 0 for node.follow if any
// return -1 for node.first
// or if $eof doesn't fit, throw irrecoverable error
function expecting (context, node, follow) {
var msg = current + ' is not allowed in "'+ context + '"\nexpecting:';
var expect = {};
page.merge(expect, node.first);
if (node.empty && node.follow) page.merge(node.follow);
for (var e in expect) msg += ' ' + expect[e];
observe('error', msg);
while (current.t != grammar.lit()) { // don't go past $eof
if (node.follow && current.t.ord in node.follow)
return 0; // bypass the node
if (follow && current.t.ord in follow)
return 1; // terminate visit
next(); // discard current input
if (current.t.ord in node.first) // new input fits?
return -1; // retry the node!
}
throw new Error(current + ' irrecoverable error'); // crash
}
// visitor, implements closure for each class
// invariant is NOT to visit unless current.t.ord in first
// returns result from observing a rule reduce action
function visit (node) {
self.assert('__WHERE__', current.t.ord in node.first);
if (node instanceof page.EBNF.Alt) { // visit first possible choice
if (! node.nodes.some(function (choice) {
if (current.t.ord in choice.first) {
visit(choice);
return true;
}
return false;
}))
self.assert('__WHERE__');
}
else if (node instanceof page.EBNF.Lst) { // alternately visit body and delimiter
loop: for (;;) {
if (current.t.ord in node.node.first) // try term
visit(node.node);
else if (node.node.empty && current.t.ord in node.delim.first) { // empty term, can do delim
} else // botched term
switch (expecting(node, node.node, node.follow)) {
case -1: continue loop; // ready for term
case 0:
if (current.t.ord in node.delim.first) break; // ready for delim
case 1:
break loop; // done with list
}
if (current.t.ord in node.delim.first) // try delim
visit(node.delim);
else if (node.delim.empty && current.t.ord in node.node.first) { // empty delim, can do term
} else break loop; // can't continue
}
}
else if (node instanceof page.EBNF.NT) { // visit rule
visit(node.rule);
}
else if (node instanceof page.BNF.NT) { // visit first possible rule
if (! node.rules.some(function (rule) {
if (current.t.ord in rule.first) {
visit(rule);
return true;
}
return false;
}))
self.assert('__WHERE__');
}
else if (node instanceof page.EBNF.Rep) { // visit body one or more times
do
visit(node.node);
while (!node.max && current.t.ord in node.first);
}
else if (node instanceof page.EBNF.Rule) { // 'rule', visit right-hand side, 'reduce', return observer value
// rule message
observe('rule', node);
// visit right-hand side
visit(node.symbol);
// reduce message
return observe('reduce', node);
}
else if (node instanceof page.BNF.Rule) { // 'rule', visit right-hand side, 'reduce', return observer value
// rule message
observe('rule', node);
// visit right-hand side
loop: for (var s = 0; s < node.symbols.length; ++ s) {
var symbol = node.symbols[s]
if (current.t.ord in symbol.first)
visit(symbol);
else if (symbol.empty && current.t.ord in symbol.follow) {
} else
switch (expecting(node, symbol, node.follow)) {
case 1:
break loop;
case -1: // retry
-- s;
case 0: // move on
continue loop;
}
}
// reduce message
return observe('reduce', node);
}
else if (node instanceof page.EBNF.Seq) { // visit each possible descendant
loop: for (var n = 0; n < node.nodes.length; ++ n) {
var symbol = node.nodes[n]
if (current.t.ord in symbol.first)
visit(symbol);
else if (symbol.empty && current.t.ord in symbol.follow) {
} else
switch (expecting(node, symbol, node.follow)) {
case 1:
break loop;
case -1: // retry
-- n;
case 0: // move on
continue loop;
}
}
}
else if (node instanceof page.BNF.T) { // 'shift', advance in input
// shift message
observe('shift', node);
// advance in input
next();
}
else self.assert('__WHERE__');
}
try { // call/cc: throw [ result ] to return result from the parser
// move to first tuple
next();
// top-level visit
if (current.t.ord in grammar.rules[0].first
|| expecting(grammar.rules[0], grammar.rules[0]) == -1)
throw [ visit(grammar.rules[0]) ];
throw new Error(current + ' irrecoverable error');
} catch (outcome) { // return outcome[0] from parser or rethrow (e.g., assert)
// end parsing
delete self.values;
if (outcome instanceof Array) return outcome[0];
self.assert('__WHERE__', outcome instanceof Error);
throw 'lineNumber' in outcome ? new Error('('+outcome.lineNumber+') ' + outcome.message) : outcome;
}
};
/** Creates a function which can observe {@link page.LL1#parse} or {@link page.LL1#parser}.
*
* The observer function collects a list of all token values and gives each rule a chance to structure the list of values
* collected for the rule. It maintains a stack of value lists as follows:
*
* | action | effect |
* |--------|--------|
* | `'rule'` | pushes the stack with an empty value list and returns `null`.
* | `'shift'` | ignores a current literal or pushes the current token's associated string value onto the top-level value list and returns `null`.
* | `'reduce'` | pops the top-level value list, interacts with `_obj` if possible, or concatenates the list to the next level value list and returns that.
* | `'error'` | returns the error message.
*
* For `'reduce'`, if a property with the rule's non-terminal's name can be found through `_obj`,
* it should be a {@link Builder} function which is then called with `_env` and the popped top-level value list
* which contains the values collected for the rule in order.
* If the result is an array it is concatenated to the next level value list;
* otherwise the result is pushed onto the next level value list.
* This new list is returned by the observer.
*
* The name of the non-terminal is searched in the properties of `_obj` or by inheritance, i.e.,
* if one property name is `{}` it's associated value is searched recursively.
*
* @param {object} [_env] environment, passed through to the functions found through the properties in `_obj`.
* @param {object} [_obj] collection of {@link Builder} functions, i.e., semantic actions.
*
* @returns {Observer} a function to observe a parser.
*
* @see page.trace
*/
page.LL1.prototype.reflect = function (_env, _obj) {
var self = this;
// closure
return function (_at, _tuple, _action, _info, _sender) {
self.assert('__WHERE__', typeof _at == 'number');
self.assert('__WHERE__', _tuple instanceof page.Tuple);
self.assert('__WHERE__', typeof _action == 'string');
!_info || self.assert('__WHERE__', typeof _info == 'string'
|| _info instanceof page.BNF.Rule || _info instanceof page.BNF.T);
self.assert('__WHERE__', _sender === self);
if (!('values' in self))
self.values = [[]]; // stack of value lists with empty sentinel
switch (_action) {
case 'rule': // push a new value list
self.values.push([]);
return null;
case 'shift': // terminal
self.assert('__WHERE__', self.values.length > 0);
if (_tuple.t instanceof page.BNF.Token)
// push top-level value list with associated string
self.values[self.values.length-1].push(_tuple.value);
return null;
case 'reduce': // rule
self.assert('__WHERE__', self.values.length > 1);
self.assert('__WHERE__', _info instanceof page.BNF.Rule);
// pop top-level value list
var top = self.values.pop();
// reflect
for (var o = _obj; o; o = o['{}'])
if (_info.nt.name in o) {
top = o[_info.nt.name](top, _env, _tuple, _sender);
break;
}
// attach result to the end of the new top-level list
self.values.push(self.values.pop().concat(top instanceof Array ? top : [ top ]));
// return new value list
return top;
case 'error': // report a message
self.assert('__WHERE__', typeof _info == 'string');
return _info;
}
self.assert('__WHERE__');
};
};