jscc-parser
Version:
A LALR(1) Parser Generator for JavaScript written in JavaScript.
706 lines (637 loc) • 30.6 kB
JavaScript
/*
* Universal module definition for tabgen.
*/
(function(root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(['require', './global', './first', './util', './log/log', './classes/State', './classes/Item',
'./classes/TableEntry', './classes/Symbol', './enums/SPECIAL', './enums/ASSOC', './enums/SYM',
'./enums/MODE_GEN', './enums/EXEC', './debug'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require);
} else {
root.jscctabgen = factory(function(mod) {
return root["jscc" + mod.split("/").pop()];
});
}
}(this,
/**
* @param {reqParameter} require
* @param {...*} others
* @returns {jscc.tabgen}
*/
function(require, others) {
//>>excludeStart("closure", pragmas.closure);
var jscc = {};
var has = /** @type {hasObject} */ (require("./localHas"));
//>>excludeEnd("closure");
var log;
var global = require("./global");
var first = require("./first");
var util = require("./util");
/**
* @suppress {uselessCode}
*/
(function() {
if (has("node")) {
log = require("./log/logNode");
} else {
log = require("./log/log");
}
})();
var State = /** @type {function(new:jscc.classes.State, StateOptions=)} */ (require("./classes/State"));
var Item = /** @type {function(new:jscc.classes.Item, ItemOptions=)} */ (require("./classes/Item"));
var TableEntry = /** @type {function(new:jscc.classes.TableEntry, number, number)} */ (require(
"./classes/TableEntry"));
var Symbol = /** @type {function(new:jscc.classes.Symbol, SymbolOptions=)} */ (require("./classes/Symbol"));
var SPECIAL = require("./enums/SPECIAL");
var ASSOC = require("./enums/ASSOC");
var SYM = require("./enums/SYM");
var EXEC = require("./enums/EXEC");
var MODE_GEN = require("./enums/MODE_GEN");
var debugFunctions = require("./debug");
/**
* Contains table-generation functions.
* @module {jscc.tabgen} jscc/tabgen
* @requires module:jscc/global
* @requires module:jscc/first
* @requires module:jscc/util
* @requires module:jscc/log/log
* @constructor
*/
jscc.tabgen = function() {
};
/**
* Creates a new {@link jscc.classes.State} object, adds it to the global states array,
* and returns the new {@link jscc.classes.State} object.
* @returns {!jscc.classes.State} The newly-created, default State object.
*/
jscc.tabgen.prototype.create_state = function() {
var state = new State({
kernel: [],
epsilon: [],
actionrow: [],
gotorow: [],
done: false,
closed: false,
def_act: 0
});
global.states.push(state);
return state;
};
/**
* Creates and returns a new Item instance using the specified parameter
* as the {@link jscc.classes.Item#prod} value.
* @param {number} p - The prod value of the new Item instance.
* @returns {!jscc.classes.Item} The new Item instance.
*/
jscc.tabgen.prototype.create_item = function(p) {
return new Item({
prod: p,
dot_offset: 0,
lookahead: []
});
};
/**
* If row already contains a {@link jscc.classes.TableEntry} with the given symbol,
* returns row. Otherwise, adds a new TableEntry with the
* provided symbol and action to the row, then returns the row.
* @param {Array<!jscc.classes.TableEntry>} row - The row to check for the
* given symbol.
* @param {number} sym - The symbol for which to check, or if not found,
* the symbol that becomes part of the new TableEntry.
* @param {number} act - The action that becomes part of the new
* TableEntry, should one be created.
* @returns {!Array<!jscc.classes.TableEntry>} The row parameter with the possible
* addition of a new TableEntry if no TableEntry with the given symbol
* was already in the row.
*/
jscc.tabgen.prototype.add_table_entry = function(row, sym, act) {
for (var i = 0; i < row.length; i++) {
if (row[i].symbol == sym) {
return row;
}
}
row.push(new TableEntry(sym, act));
return row;
};
/**
* If row already contains a {@link jscc.classes.TableEntry} with the given symbol,
* updates the {@link jscc.classes.TableEntry#action}
* property of that TableEntry to match the given action. If the
* symbol is not present, no changes are made to the row. Either way,
* the row is returned.
* @param {Array<!jscc.classes.TableEntry>} row - The row to check for the given
* symbol.
* @param {number} sym - The symbol for which to check.
* @param {number} act - The action to which to update a TableEntry
* containing the given symbol.
* @returns {!Array<!jscc.classes.TableEntry>} The row parameter, possibly with one
* modified TableEntry.
*/
jscc.tabgen.prototype.update_table_entry = function(row, sym, act) {
var i;
for (i = 0; i < row.length; i++) {
if (row[i].symbol == sym) {
row[i].action = act;
return row;
}
}
return row;
};
/**
* If row contains a {@link jscc.classes.TableEntry} with the given symbol, removes that
* symbol's TableEntry and returns the row. Otherwise, simply
* returns the row.
* @param {Array<!jscc.classes.TableEntry>} row - The row to check for the given
* symbol.
* @param {number} sym - The symbol for which to check and, if found,
* delete.
* @returns {!Array<!jscc.classes.TableEntry>} The row parameter, possibly with one
* fewer TableEntry.
*/
jscc.tabgen.prototype.remove_table_entry = function(row, sym) {
for (var i = 0; i < row.length; i++) {
if (row[i].symbol == sym) {
row.splice(i, 1);
return row;
}
}
return row;
};
/**
* If row contains a TableEntry with the given symbol, returns that
* TableEntry's {@link jscc.classes.TableEntry#action} property.
* Otherwise, returns void(0).
* @param {Array<!jscc.classes.TableEntry>} row - The row to check for the given
* symbol.
* @param {number} sym - The symbol for which to check.
* @returns {(number|void)}
*/
jscc.tabgen.prototype.get_table_entry = function(row, sym) {
for (var i = 0; i < row.length; i++) {
if (row[i].symbol == sym) {
return row[i].action;
}
}
return void(0);
};
/**
* Returns the index of the first {@link jscc.global.states}
* array entry whose {@link jscc.global.State#done} property
* is false, or -1 if no such entry exists.
* @returns {number} The index of the first undone state, or -1 if
* no undone states exist.
*/
jscc.tabgen.prototype.get_undone_state = function() {
for (var i = 0; i < global.states.length; i++) {
if (global.states[i].done == false) {
return i;
}
}
return -1;
};
/**
* Compares two {@link jscc.classes.Item} objects for
* sorting purposes by comparing their
* {@link jscc.classes.Item#prod} values.
* @param {!jscc.classes.Item} a - The first Item
* @param {!jscc.classes.Item} b - The second Item
* @returns {number} Less than zero if a.prod < b.prod;
* greater than zero if a.prod > b.prod; zero if
* a.prod equals b.prod.
*/
jscc.tabgen.prototype.sort_partition = function(a, b) {
return a.prod - b.prod;
};
/**
* Returns the index within the {@link jscc.global.symbols}
* array of the first symbol with the given label, SYM, and
* SPECIAL values. If no such symbol is found, returns -1.
* @param {string} label - The symbol label for which to search.
* @param {jscc.enums.SYM} kind - Whether the symbol is terminating or
* nonterminating.
* @param {jscc.enums.SPECIAL=} special - The type of special symbol, if any.
* Defaults to NONE.
* @returns {number} - The index within the symbols array of the
* first matching symbol, or -1 if there are no matching symbols.
*/
jscc.tabgen.prototype.find_symbol = function(label, kind, special) {
if (!special) {
special = SPECIAL.NONE;
}
for (var i = 0; i < global.symbols.length; i++) {
if (global.symbols[i].label.toString() == label.toString()
&& global.symbols[i].kind == kind
&& global.symbols[i].special == special) {
return i;
}
}
return -1;
};
/**
* Creates a new symbol (if necessary) and appends it to the
* global symbol array. If the symbol does not already exist,
* the instance of that symbol is returned only.
* @param {string} label - The label of the symbol. In case
* of kind == SYM.NONTERM, the label is the name of the
* right-hand side, else it is the regular expression for the
* terminal symbol.
* @param {jscc.enums.SYM} kind - Type of the symbol. This can be
* SYM.NONTERM or SYM.TERM.
* @param {jscc.enums.SPECIAL} special - Specialized symbols.
* @returns {number} The id property of the particular Symbol
* object.
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.create_symbol = function(label, kind, special) {
var exists;
if ((exists = this.find_symbol(label, kind, special)) > -1) {
return global.symbols[exists].id;
}
var sym = new Symbol({
label: label,
kind: kind,
prods: [],
nullable: false,
id: global.symbols.length,
code: "",
associativity: ASSOC.NONE,
level: 0,
special: special,
defined: false,
first: []
});
if (kind == SYM.TERM) {
sym.first.push(sym.id);
}
global.symbols.push(sym);
return sym.id;
};
/**
* Checks if two item sets contain the same items. The
* items may only differ in their lookahead.
* @param {Array<!jscc.classes.Item>} set1 - Set to be compared with
* set2.
* @param {Array<!jscc.classes.Item>} set2 - Set to be compared with
* set1.
* @returns {boolean} True if equal, else false.
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.item_set_equal = function(set1, set2) {
var i, j, cnt = 0;
if (set1.length != set2.length) {
return false;
}
for (i = 0; i < set1.length; i++) {
for (j = 0; j < set2.length; j++) {
if (set1[i].prod == set2[j].prod &&
set1[i].dot_offset == set2[j].dot_offset) {
cnt++;
break;
}
}
}
return cnt == set1.length;
};
/**
*
* @param {Array<!jscc.classes.Item>} seed
* @param {Array<!jscc.classes.Item>} closure
* @returns {number}
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.close_items = function(seed, closure) {
var i, j, k;
var cnt = 0, tmp_cnt = 0;
var item;
for (i = 0; i < seed.length; i++) {
if (seed[i].dot_offset < global.productions[seed[i].prod].rhs.length) {
if (global.symbols[global.productions[seed[i].prod].rhs[seed[i].dot_offset]].kind ==
SYM.NONTERM) {
for (j = 0;
j < global.symbols[global.productions[seed[i].prod].rhs[seed[i].dot_offset]].prods.length;
j++) {
for (k = 0; k < closure.length; k++) {
if (closure[k].prod ==
global.symbols[global.productions[seed[i].prod].rhs[seed[i].dot_offset]].prods[j]) {
break;
}
}
if (k == closure.length) {
item =
this.create_item(
global.symbols[global.productions[seed[i].prod].rhs[seed[i].dot_offset]].prods[j]);
closure.push(item);
cnt++;
}
tmp_cnt = closure[k].lookahead.length;
if (first.rhs_first(closure[k], global.productions[seed[i].prod], seed[i].dot_offset + 1)) {
closure[k].lookahead = util.union(closure[k].lookahead, seed[i].lookahead);
}
cnt += closure[k].lookahead.length - tmp_cnt;
}
}
}
}
return cnt;
};
/**
* @summary Implements the LALR(1) closure algorithm.
* @description A short overview:
* 1. Closing a closure_set of Item objects from a given
* kernel seed (this includes the kernel seed itself!)
* 2. Moving all epsilon items to the current state's epsilon
* set.
* 3. Moving all symbols with the same symbol right to the
* dot to a partition set.
* 4. Check if there is already a state with the same items
* as there are in the partition. If so, union the
* lookaheads, else, create a new state and set the
* partition as kernel seed.
* 5. If the (probably new) state was not closed yet, perform
* some table creation: If there is a terminal to the
* right of the dot, do a shift on the action table, else
* do a goto on the goto table. Reductions are performed
* later, when all states are closed.
* @param {number} s - Id of the state that should be closed.
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.lalr1_closure = function(s) {
var closure = [], nclosure, partition;
var partition_sym;
var i, j, cnt = 0, old_cnt = 0, tmp_cnt, ns;
do {
old_cnt = cnt;
cnt = this.close_items(((old_cnt == 0) ? global.states[s].kernel : closure), closure);
} while (cnt != old_cnt);
for (i = 0; i < global.states[s].kernel.length; i++) {
if (global.states[s].kernel[i].dot_offset <
global.productions[global.states[s].kernel[i].prod].rhs.length) {
closure.unshift(new Item({
prod: global.states[s].kernel[i].prod,
dot_offset: global.states[s].kernel[i].dot_offset,
lookahead: []
}));
for (j = 0; j < global.states[s].kernel[i].lookahead.length; j++) {
closure[0].lookahead[j] = global.states[s].kernel[i].lookahead[j];
}
}
}
for (i = 0; i < closure.length; i++) {
if (global.productions[closure[i].prod].rhs.length == 0) {
for (j = 0; j < global.states[s].epsilon.length; j++) {
if (global.states[s].epsilon[j].prod == closure[i].prod
&& global.states[s].epsilon[j].dot_offset == closure[i].dot_offset) {
break;
}
}
if (j == global.states[s].epsilon.length) {
global.states[s].epsilon.push(closure[i]);
}
closure.splice(i, 1);
}
}
while (closure.length > 0) {
partition = [];
nclosure = [];
partition_sym = -1;
for (i = 0; i < closure.length; i++) {
if (partition.length == 0) {
partition_sym = global.productions[closure[i].prod].rhs[closure[i].dot_offset];
}
if (closure[i].dot_offset < global.productions[closure[i].prod].rhs.length) {
if (global.productions[closure[i].prod].rhs[closure[i].dot_offset] == partition_sym) {
closure[i].dot_offset++;
partition.push(closure[i]);
} else {
nclosure.push(closure[i]);
}
}
}
if (partition.length > 0) {
// beachcoder Feb 23, 2009:
// Uhh here was a very exciting bug that only came up on
// special grammar constellations: If we don't sort the
// partition set by production here, it may happen that
// states get wrong lookahead, and unexpected conflicts
// or failing grammars come up.
partition.sort(this.sort_partition);
// Now one can check for equality
for (i = 0; i < global.states.length; i++) {
if (this.item_set_equal(global.states[i].kernel, partition)) {
break;
}
}
if (i == global.states.length) {
ns = this.create_state();
ns.kernel = partition;
}
tmp_cnt = 0;
cnt = 0;
for (j = 0; j < partition.length; j++) {
tmp_cnt += global.states[i].kernel[j].lookahead.length;
global.states[i].kernel[j].lookahead =
util.union(global.states[i].kernel[j].lookahead, partition[j].lookahead);
cnt += global.states[i].kernel[j].lookahead.length;
}
if (tmp_cnt != cnt) {
global.states[i].done = false;
}
if (!(global.states[s].closed)) {
for (j = 0; j < partition.length; j++) {
if (partition[j].dot_offset - 1 < global.productions[partition[j].prod].rhs.length) {
if (global.symbols[global.productions[partition[j].prod].rhs[partition[j].dot_offset -
1]].kind ==
SYM.TERM) {
global.states[s].actionrow = this.add_table_entry(global.states[s].actionrow,
global.productions[partition[j].prod].rhs[partition[j].dot_offset -
1],
i);
global.shifts++;
} else {
global.states[s].gotorow = this.add_table_entry(global.states[s].gotorow,
global.productions[partition[j].prod].rhs[partition[j].dot_offset -
1],
i);
global.gotos++;
}
}
}
}
}
closure = nclosure;
}
global.states[s].closed = true;
};
/**
* Inserts reduce-cells into the action table. A reduction
* does always occur for items with the dot to the far right
* of the production and to items with no production (epsilon
* items).
*
* The reductions are done on the corresponding lookahead
* symbols. If a shift-reduce conflict appears, the function
* will always behave in favor of the shift.
*
* Reduce-reduce conflicts are reported immediately, and need
* to be solved.
* @param {number} s - The index of the state where the
* reductions take effect.
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.do_reductions = function(s) {
var n, i, j, ex, act, output_warning, item_set;
var reds = [];
var max = 0, count;
for (n = 0; n < 2; n++) {
if (!n) {
item_set = global.states[s].kernel;
} else {
item_set = global.states[s].epsilon;
}
for (i = 0; i < item_set.length; i++) {
if (item_set[i].dot_offset == global.productions[item_set[i].prod].rhs.length) {
for (j = 0; j < item_set[i].lookahead.length; j++) {
output_warning = true;
ex = this.get_table_entry(global.states[s].actionrow,
item_set[i].lookahead[j]);
if (ex == void(0)) {
act = -1 * item_set[i].prod;
global.states[s].actionrow = this.add_table_entry(global.states[s].actionrow,
item_set[i].lookahead[j], act);
global.reduces++;
} else {
act = ex;
var warning = "";
if (ex > 0) {
// Shift-reduce conflict
// Is there any level specified?
if (global.symbols[item_set[i].lookahead[j]].level > 0
|| global.productions[item_set[i].prod].level > 0) {
// Is the level the same?
if (global.symbols[item_set[i].lookahead[j]].level ==
global.productions[item_set[i].prod].level) {
// In case of left-associativity, reduce
if (global.symbols[item_set[i].lookahead[j]].associativity == ASSOC.LEFT) {
// Reduce
act = -1 * item_set[i].prod;
} else if (global.symbols[item_set[i].lookahead[j]].associativity ==
ASSOC.NOASSOC) {
// else, if nonassociativity is set,
// remove table entry
this.remove_table_entry(global.states[s].actionrow,
item_set[i].lookahead[j]);
log.warn("Removing nonassociative symbol '" +
global.symbols[item_set[i].lookahead[j]].label +
"' in state " + s);
output_warning = false;
}
} else {
// If symbol precedence is lower production's
// precedence, reduce
if (global.symbols[item_set[i].lookahead[j]].level <
global.productions[item_set[i].prod].level) {
// Reduce
act = -1 * item_set[i].prod;
}
}
}
warning = "Shift";
} else {
// Reduce-reduce conflict
act = ((act * -1 < item_set[i].prod) ?
act : -1 * item_set[i].prod);
warning = "Reduce";
}
warning += "-reduce conflict on symbol '" +
global.symbols[item_set[i].lookahead[j]].label +
"' in state " + s;
warning += "\n Conflict resolved by " +
((act <= 0) ? "reducing with production" :
"shifting to state") + " " +
((act <= 0) ? act * -1 : act);
if (output_warning) {
log.warn(warning);
}
if (act != ex) {
this.update_table_entry(global.states[s].actionrow,
item_set[i].lookahead[j], act);
}
}
}
}
}
}
// Find most common reduction
global.states[s].def_act = -1; // Define no default action
// Are there any reductions? Then select the best of them.
for (i = 0; i < reds.length; i++) {
for (j = 0, count = 0; j < reds.length; j++) {
if (reds[j] == reds[i]) {
count++;
}
}
if (max < count) {
max = count;
global.states[s].def_act = reds[i];
}
}
// Remove all default reduce action reductions, if they exist.
if (global.states[s].def_act >= 0) {
do {
count = global.states[s].actionrow.length;
for (i = 0; i < global.states[s].actionrow.length; i++) {
if (global.states[s].actionrow[i][1] == global.states[s].def_act * -1) {
global.states[s].actionrow.splice(i, 1);
}
}
} while (count != global.states[s].actionrow.length);
}
};
/**
* Entry function to perform table generation. If all states
* of the parsing state machine are constructed, all reduce
* operations are inserted in the particular positions of the
* action table.
*
* If there is a Shift-reduce conflict, the shift takes the
* higher precedence. Reduce-reduce conflicts are resolved by
* choosing the first defined production.
* @param {boolean} debug - Toggle debug trace output. This
* should only be switched on when JS/CC is executed in a web
* environment, because HTML-code will be printed.
* @author Jan Max Meyer
*/
jscc.tabgen.prototype.lalr1_parse_table = function(debug) {
var i, item, s;
// Create EOF symbol
item = this.create_item(0);
s = this.create_symbol("$", SYM.TERM, SPECIAL.EOF);
item.lookahead.push(s);
// Create first state
s = this.create_state();
s.kernel.push(item);
while ((i = this.get_undone_state()) >= 0) {
global.states[i].done = true;
this.lalr1_closure(i);
}
for (i = 0; i < global.states.length; i++) {
this.do_reductions(i);
}
if (debug) {
for (i = 0; i < global.states.length; i++) {
debugFunctions.print_item_set((global.exec_mode == EXEC.CONSOLE) ?
MODE_GEN.TEXT :
MODE_GEN.HTML,
"states[" + i + "].kernel", global.states[i].kernel);
debugFunctions.print_item_set((global.exec_mode == EXEC.CONSOLE) ?
MODE_GEN.TEXT :
MODE_GEN.HTML,
"states[" + i + "].epsilon", global.states[i].epsilon);
}
log.debug(global.states.length + " States created.");
}
};
return new jscc.tabgen();
}));