"use strict"; /** @fileOverview Implements `class page.EBNF extends page.BNF`. * @author Axel T. Schreiner <ats@cs.rit.edu> * @version 1.5.0 */ /** Creates a new grammar representation. * @class Represents a grammar as a tree; supports choices and iterations. * @extends page.BNF * * @property {page.BNF} bnf grammar represented in BNF, terminals are shared here. * This object is created here and completed in {@link page.EBNF#init}. */ page.EBNF = function () { page.BNF.call(this); // create bnf object here this.bnf = new page.BNF(); // share terminals here (does include $error and $eof) this.bnf.lits = this.lits; this.bnf.litsByValue = this.litsByValue; this.bnf.tokens = this.tokens; this.bnf.tokensByName = this.tokensByName; }; page.subclass(page.EBNF, 'page.EBNF', page.BNF); /** Factory method: Represent a grammar described in EBNF. * * This is bootstrapped using {@link page.EBNF.ctor} * to create an LL(1) compiler for EBNF which uses {@link page.EBNF.builder}. * @static * * @param {string} _grammar description in EBNF; * comments extend from `#` to the end of a line. * * @returns {page.EBNF} a new grammar representation; check `.errors`. */ page.EBNF.from = function (_grammar) { page.assert('__WHERE__', typeof _grammar == 'string'); // create a compiler which recognizes EBNF var ebnfCompiler = new page.LL1(page.EBNF.ctor()); page.assert('__WHERE__', ebnfCompiler.ambiguous == 0 && ebnfCompiler.greedy == 0 && ebnfCompiler.errors == 0); // build the new grammar var ebnf = new page.EBNF(); try { ebnfCompiler.parser(ebnfCompiler.tokenizer(/\s+|#.*/)(_grammar), ebnfCompiler.reflect(ebnf, page.EBNF.builder)); } catch (e) { page.assert('__WHERE__', e instanceof Error); ebnfCompiler.error(e); } ebnf.errors += ebnfCompiler.errors; return ebnf; }; /** Nodes in an EBNF grammar. * @typedef eNode * @type page.BNF.Lit|page.BNF.Token|page.EBNF.Alt|page.EBNF.Lst|page.EBNF.NT|page.EBNF.Rep|page.EBNF.Seq */ /** Asserts that a node has a type suitable for EBNF. * @private * * @param {string} _marker to identify position across a re-throw. * @param {eNode} _node to test. * * @throws {Error} if type is not suitable. * @see page.assert */ page.EBNF.prototype.eNode = function (_marker, _node) { this.assert(_marker, _node instanceof page.EBNF.Alt || _node instanceof page.EBNF.Lst || _node instanceof page.EBNF.NT || _node instanceof page.EBNF.Rep || _node instanceof page.EBNF.Seq || _node instanceof page.BNF.Lit || _node instanceof page.BNF.Token); }; /** The EBNF grammar manually represented with {@link page.EBNF}. * @static * * @returns {page.EBNF} the EBNF grammar for EBNF, hand-crafted. */ page.EBNF.ctor = function () { var g = new page.EBNF(); g.init(g.nt('grammar'), [ g.rule(g.nt('grammar'), g.seq([ g.many(g.nt('token')), g.some(g.nt('rule')) ])), g.rule(g.nt('token'), g.seq([ g.lit('%token'), g.token('Name', '[a-zA-Z][-a-zA-Z0-9_]*'), // alphanum with _ and - g.token('Literal', "'(?:[^'\n\\\\]|\\\\['n\\\\])+'"), // single quoted g.lit(';') ])), g.rule(g.nt('rule'), g.seq([ g.nt('nt'), g.lit(':'), g.nt('alt'), g.lit(';') ])), g.rule(g.nt('nt'), g.token('Name')), g.rule(g.nt('alt'), g.someList(g.nt('seq'), g.lit('|'))), g.rule(g.nt('seq'), g.some(g.nt('item'))), g.rule(g.nt('item'), g.seq([ g.nt('term'), g.opt(g.alt([ g.nt('opt'), g.nt('some'), g.nt('many') ])) ])), g.rule(g.nt('opt'), g.lit('?')), g.rule(g.nt('some'), g.seq([ g.lit('+'), g.opt(g.seq([ g.lit('/'), g.nt('term') ])) ])), g.rule(g.nt('many'), g.seq([ g.lit('*'), g.opt(g.seq([ g.lit('/'), g.nt('term') ])) ])), g.rule(g.nt('term'), g.seq([ g.alt([ g.nt('token_or_nt'), g.nt('lit'), g.seq([ g.lit('('), g.nt('alt'), g.lit(')') ]) ]) ])), g.rule(g.nt('token_or_nt'), g.token('Name')), g.rule(g.nt('lit'), g.token('Literal')) ]); return g; }; { /** The EBNF grammar for EBNF. * @static */ page.EBNF.grammar = " %token Name '[a-zA-Z][-a-zA-Z0-9_]*'; \n" + " %token Literal '\\'(?:[^\\'\\n\\\\\\\\]|\\\\\\\\[\\'n\\\\\\\\])+\\''; \n" + " grammar: token* rule+; \n" + " token: '%token' Name Literal ';'; \n" + " rule: nt ':' alt ';'; \n" + " nt: Name; \n" + " alt: seq +/ '|'; \n" + " seq: item+; \n" + " item: term ( opt | some | many )?; \n" + " opt: '?'; \n" + " some: '+' ( '/' term )?; \n" + " many: '*' ( '/' term )?; \n" + " term: token_or_nt | lit | '(' alt ')'; \n" + " token_or_nt: Name; \n" + " lit: Literal; \n"; } /** An object with action functions corresponding to {@link page.EBNF}`.grammar` * for {@link page.LL1#reflect} which require a new {@link page.EBNF} as * environment to represent a grammar in EBNF notation using this environment. * @static */ page.EBNF.builder = { // grammar: tokens rules // token: '%token' Name Literal ';' // nt: Name // token_or_nt: Name '{}': page.BNF.builder, // inherited rule: // nt ':' alt ';' function (_values, _ebnf, _tuple) { if (!_values[0].rule) return _ebnf.rule(_values[0], _values[1]); _ebnf.error('('+_tuple.lineno+') '+_values[0]+' can only have one rule'); return []; }, alt: // seq +/ '|' function (_values, _ebnf) { return _ebnf.alt(_values); }, seq: // item+ function (_values, _ebnf) { return _ebnf.seq(_values); }, item: // term ( opt | some | many )? function (_values, _ebnf) { if (_values.length == 1) return _values[0]; return _values[1](_values[0]); }, opt: // '?' function (_values, _ebnf) { return function (term) { return _ebnf.opt(term); } }, some: // '+' ( '/' term )? function (_values, _ebnf) { return function (term) { return _values.length ? _ebnf.someList(term, _values[0]) : _ebnf.some(term); }; }, many: // '*' ( '/' term )? function (_values, _ebnf) { return function (term) { return _values.length ? _ebnf.manyList(term, _values[0]) : _ebnf.many(term); }; } }; /** Displays EBNF grammar, appends {@link page.EBNF#bnf} and a dump. * @override * @variation EBNF * @returns {string} */ page.EBNF.prototype.dump = function () { var result = this.rules.map(function (rule) { return rule.dump(); }).join('\n'); if (this.bnf) result += '\n\n' + this.bnf.dump(); return result; }; /** Must be called once to complete EBNF grammar initialization. * * Completes rule 0: with a new start symbol `$accept` * to accept the grammar's original start symbol and then `$eof`. * * Sets {@link page.BNF.Rule}`.index` as index into the rules array. * * Uses `.bnf` to represent the grammar in BNF and imports `empty` and the * `first` and `follow` sets. The algorithm is careful to create new non-terminals * for concealed BNF rules to make the EBNF non-terminals visible in the BNF grammar. * @override * @variation EBNF * * @param {page.EBNF.NT} [_start] start symbol of grammar, * default is the non-terminal of rule 1. * @param {page.BNF.Rule[]} [_rules] (additional) grammar rules. * * @see page.BNF#initTree */ page.EBNF.prototype.init = function (_start, _rules) { var self = this; // none or valid start symbol !_start || self.assert('__WHERE__', _start instanceof page.EBNF.NT); // common code self.initTree(_rules); // use once only: rule 0 not yet filled in self.assert('__WHERE__', !self.rules[0].symbol); // rule zero: $accept: start $eof self.rules[0].symbol = self.seq([ _start ? _start : self.rules[1].nt, self.lit() ]); // index the rules self.rules.forEach(function (rule, n) { rule.index = Number(n); }); // translate all but rule zero; don't translate terminals self.rules.forEach(function (rule, n) { if (n > 0) { var nt = rule.nt.toBNF(self); if (rule.symbol instanceof page.EBNF.NT) self.bnf.rules.push( self.bnf.rule( nt, [ rule.symbol.toBNF(self) ])); else if (rule.symbol instanceof page.BNF.T) self.bnf.rules.push( self.bnf.rule( nt, [ rule.symbol ])); else rule.symbol.toBNF(self, nt); } }); self.bnf.init(undefined, []); // connect rule zero to BNF rule zero, i.e., $accept and seq to BNF's $accept self.rules[0].nt.nt = self.bnf.nt(); // $accept.nt = $accept self.rules[0].symbol.nt = self.bnf.nt(); // seq.nt = $accept self.rules[0].symbol.nodes[0].nt = self.bnf.rules[0].symbols[0]; // start.nt = start.nt // import empty, first, and follow self.nts.forEach(function (nt) { nt.init(); }); self.rules.forEach(function (rule) { rule.init(); }); // import error count, if any self.errors += self.bnf.errors; self.bnf.errors = 0; }; /** Factory method to create a new representation of a choice for EBNF. * * @param {eNode[]} _nodes alternatives, not empty. * * @returns {eNode} a new representation of a choice for EBNF; * flattens if there is a single descendant. */ page.EBNF.prototype.alt = function (_nodes) { var self = this; self.assert('__WHERE__', _nodes instanceof Array && _nodes.length > 0); _nodes.forEach(function (node) { self.eNode('__WHERE__', node); }); return _nodes.length == 1 ? _nodes[0] : new page.EBNF.Alt(_nodes); }; /** Factory method to create a new representation of a delimited iteration for EBNF; * represents 1 or more occurrences. * * @param {eNode} _node body. * @param {eNode} _delim delimiter. * * @returns {page.EBNF.Lst} a new representation of a delimited iteration for EBNF. */ page.EBNF.prototype.someList = function (_node, _delim) { this.eNode('__WHERE__', _node); this.eNode('__WHERE__', _delim); return new page.EBNF.Lst(_node, _delim, 1); }; /** Factory method to create a new representation of a delimited iteration for EBNF; * represents 0 or more occurrences. * * @param {eNode} _node body. * @param {eNode} _delim delimiter. * * @returns {page.EBNF.Lst} a new representation of a delimited iteration for EBNF. */ page.EBNF.prototype.manyList = function (_node, _delim) { this.eNode('__WHERE__', _node); this.eNode('__WHERE__', _delim); return new page.EBNF.Lst(_node, _delim, 0); }; /** Factory method to create a unique non-terminal representation, * maintains {@link page.EBNF#nts} and {@link page.EBNF#ntsByName}, * checks against {@link page.EBNF#tokensByName}. * @override * @variation EBNF * * @param {string} [_name] non-terminal's name, cannot start with `$`. * If omitted represents the `$accept` non-terminal. * * @returns {page.EBNF.NT} a unique non-terminal representation. */ page.EBNF.prototype.nt = function (_name) { if (arguments.length == 0) _name = '$accept'; else { this.assert('__WHERE__', typeof _name == 'string' && _name.length > 0 && _name[0] != '$'); this.assert('__WHERE__', !(_name in this.tokensByName)); // token } if (_name in this.ntsByName) return this.ntsByName[_name]; var result = new page.EBNF.NT(_name, this.nts.length); this.nts.push(result); return this.ntsByName[_name] = result; }; /** Factory method to create a new representation of an iteration for EBNF; * represents 0 to 1 occurence. * * @param {eNode} _node body. * * @returns {page.EBNF.Rep} a new representation of an iteration for EBNF. */ page.EBNF.prototype.opt = function (_node) { this.eNode('__WHERE__', _node); return new page.EBNF.Rep(_node, 0, 1); }; /** Factory method to create a new representation of an iteration for EBNF; * represents 1 or more occurrences. * * @param {eNode} _node body. * * @returns {page.EBNF.Rep} a new representation of an iteration for EBNF. */ page.EBNF.prototype.some = function (_node) { this.eNode('__WHERE__', _node); return new page.EBNF.Rep(_node, 1, undefined); }; /** Factory method to create a new representation of an iteration for EBNF; * represents 0 or more occurrences. * * @param {eNode} _node body. * * @returns {page.EBNF.Rep} a new representation of an iteration for EBNF. */ page.EBNF.prototype.many = function (_node) { this.eNode('__WHERE__', _node); return new page.EBNF.Rep(_node, 0, undefined); }; /** Factory method to create a rule representation for EBNF. * Sets it as rule's non-terminal's {@link page.EBNF.NT#rule} * @override * @variation EBNF * * @param {page.EBNF.NT} _nt left-hand side, non-terminal, may not yet have a rule. * @param {eNode} [_symbol] single symbol for right-hand side. * if omitted, must be rule 0 for `$accept`. * * @returns {page.EBNF.Rule} a new rule representation. * * @see page.EBNF.init */ page.EBNF.prototype.rule = function (_nt, _symbol) { if (arguments.length == 1) { this.assert('__WHERE__', this.rules.length == 0 && _nt == this.nt()); _symbol = null; // filled in during init } else { this.assert('__WHERE__', _nt instanceof page.EBNF.NT && !_nt.rule); this.eNode('__WHERE__', _symbol); } var result = new page.EBNF.Rule(_nt, _symbol); // set new rule as rule's nt's rule result.nt.rule = result; return result; }; /** Factory method to create a new representation of a sequence for EBNF. * * @param {eNode[]} _nodes sequence, not empty. * * @returns {eNode} a new representation of a sequence for EBNF; * flattens if there is a single descendant. */ page.EBNF.prototype.seq = function (_nodes) { var self = this; self.assert('__WHERE__', _nodes instanceof Array && _nodes.length > 0); _nodes.forEach(function (node) { self.eNode('__WHERE__', node); }); return _nodes.length == 1 ? _nodes[0] : new page.EBNF.Seq(_nodes); };