syntax-cli-prog
Version:
Syntactic analysis toolkit, language agnostic parsers generator.
746 lines (615 loc) • 20.4 kB
JavaScript
/**
* The MIT License (MIT)
* Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
*/
'use strict';
/*global ROOT:true*/
// To require local modules from root.
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
global.ROOT = __dirname + '/../';
var colors = require('colors');
var fs = require('fs');
var path = require('path');
var options = require('nomnom').script('syntax').options({
version: {
abbr: 'v',
help: 'Print current version',
flag: true
},
mode: {
abbr: 'm',
transform: normalizeMode
},
grammar: {
abbr: 'g',
help: 'File containing LL or LR grammar',
metavar: 'FILE'
},
lex: {
abbr: 'l',
help: 'File containing lexical grammar',
required: false,
metavar: 'FILE'
},
table: {
abbr: 't',
help: 'Generate and output parsing table',
flag: true
},
collection: {
abbr: 'c',
help: 'Generate and output canonical collection of LR items',
flag: true
},
sets: {
abbr: 's',
help: 'Generate and output parsing sets (all/first/follow/predict)'
},
parse: {
abbr: 'p',
help: 'Parse a string and checks for acceptance',
type: 'string'
},
file: {
abbr: 'f',
help: 'File to be parsed',
type: 'string',
metavar: 'FILE'
},
output: {
abbr: 'o',
help: 'Output file for a generated parser module',
type: 'string',
metavar: 'FILE'
},
'custom-tokenizer': {
abbr: 'k',
help: 'Path to a file with custom tokenizer class',
type: 'string',
metavar: 'FILE'
},
'tokenizer-only': {
help: 'Whether to generate only standalone tokenizer output file',
flag: true
},
tokenize: {
help: 'Show list of tokens',
flag: true
},
'ignore-whitespaces': {
abbr: 'w',
help: 'Adds a Lex rule to ignore whitespaces',
flag: true
},
'resolve-conflicts': {
abbr: 'r',
help: 'Whether to auto-resolve conflicts with default action',
flag: true
},
'generate-inline-parser': {
help: 'Whether to generate a parser module for parsing a passed string',
flag: true
},
loc: {
help: 'Capture token locations (offsets, line and column numbers)',
flag: true
},
'case-insensitive': {
help: 'Sets case-insensitive mode to lexical grammar',
abbr: 'i',
flag: true
},
debug: {
help: 'Debug mode (outputs steps and timing)',
abbr: 'd',
flag: true
},
validate: {
help: 'Validate a grammar, showing conflicts, number of states, etc.',
flag: true
},
namespace: {
help: 'Append a wrapping namespace to generated code',
type: 'string'
}
}).parse();
/**
* Setup debug mode.
*/
global.SYNTAX_DEBUG = options.debug;
// NOTE: all Syntax tool requires go after debug `global.SYNTAX_DEBUG`
// is set, since debug module uses it at load time.
var GRAMMAR_MODE = require(ROOT + 'grammar/grammar-mode').MODES;
var Grammar = require(ROOT + 'grammar/grammar').default;
var debug = require(ROOT + 'debug').default;
if (global.SYNTAX_DEBUG) {
console.info(colors.bold('\nDEBUG mode is: ON\n'));
}
/**
* Default generator options.
*/
var generatorOptions = {
customTokenizer: options['custom-tokenizer'],
resolveConflicts: options['resolve-conflicts'],
namespace: options['namespace']
};
/**
* Returns a parsing table for a grammar.
*/
function getLRParsingTable(grammar) {
var LRParsingTable = require(ROOT + 'lr/lr-parsing-table').default;
return new LRParsingTable({
grammar: grammar,
canonicalCollection: getCanonicalCollection(grammar),
resolveConflicts: options['resolve-conflicts']
});
}
/**
* Returns a canonical collection for a grammar.
*/
function getCanonicalCollection(grammar) {
var CanonicalCollection = require(ROOT + 'lr/canonical-collection').default;
return new CanonicalCollection({ grammar: grammar });
}
/**
* Validates a grammar.
*/
function validateLRGrammar(grammar) {
var LRParsingTable = require(ROOT + 'lr/lr-parsing-table').default;
var EntryType = LRParsingTable.EntryType;
var table = getLRParsingTable(grammar);
var conflictsData = table.getConflictsData();
var hasConflicts = false;
var srConflicts = new Map();
var rrConflicts = new Map();
// Conflicts in all states.
for (var state in conflictsData) {
var stateConflicts = conflictsData[state];
// Conflitcs within a state.
for (var symbol in stateConflicts) {
var symbolConflict = stateConflicts[symbol];
if (symbolConflict.resolved === false) {
hasConflicts = true;
var conflict = symbolConflict.conflict;
switch (LRParsingTable.getEntryType(conflict)) {
// Shift-reduce conflict.
case EntryType.SR_CONFLICT:
{
var srParts = table.splitSRParts(conflict);
var reducePart = srParts[0];
var production = grammar.getProduction(reducePart.slice(1));
var data = srConflicts.get(production) || [];
data.push(symbol);
srConflicts.set(production, data);
break;
}
// Reduce-reduce conflict.
case EntryType.RR_CONFLICT:
{
var rrParts = conflict.split('/');
var reduce1 = rrParts[0];
var reduce2 = rrParts[1];
rrConflicts.set(conflict, {
production1: grammar.getProduction(reduce1.slice(1)),
production2: grammar.getProduction(reduce2.slice(1))
});
break;
}
default:
throw new Error('Unknown conflict type: ' + symbolConflict.conflict);
}
}
}
}
// Had conflicts, but all were resolved.
if (!hasConflicts) {
console.info(colors.green('\n\u2713 Grammar doesn\'t have any conflicts!\n'));
return;
}
console.info(colors.red('\nGrammar has the following unresolved conflicts:\n'));
// Shift-reduce.
if (srConflicts.size > 0) {
console.info(colors.bold('"Shift-reduce" conflicts:\n'));
var i = 1;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = srConflicts.entries()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var productionData = _step.value;
var _production = productionData[0];
var _data = productionData[1];
var pad = i >= 10 ? '' : ' ';
_data = _data.map(function (symbol) {
return colors.bold(symbol);
});
console.info(' ' + pad + i++ + '. ' + ('Production: ' + colors.bold(_production.toFullString()) + ', ') + ('conflicts with symbols ' + _data.join(', ') + '.'));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
console.info('');
}
// Reduce-reduce.
if (rrConflicts.size > 0) {
console.info(colors.bold('"Reduce-reduce" conflicts:\n'));
var _i = 1;
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = rrConflicts.values()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var productions = _step2.value;
var production1 = productions.production1;
var production2 = productions.production2;
var _pad = _i >= 10 ? '' : ' ';
console.info(' ' + _pad + _i++ + '. ' + ('Production: ' + colors.bold(production1.toFullString()) + ', ') + ('conflicts with production ' + colors.bold(production2.toFullString()) + '.'));
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
console.info('');
}
// Hint message how the conflicts can be fixed.
console.info(colors.bold('Possible solutions:') + '\n\n' + ' 1. Conflicts possibly can be resolved by using ' + (colors.bold('"operators"') + ' section,\n where you can specify ') + (colors.bold('precedence') + ' and ' + colors.bold('associativity') + '.\n\n') + ' 2. By using different parsing mode' + (options.mode !== GRAMMAR_MODE.LALR1 ? ', e.g. ' + colors.bold('LALR1') + ' instead of ' + colors.bold(options.mode) : '') + '.\n\n' + ' 3. Restructuring grammar.\n\n' + ('See docs and details in: ' + colors.bold('http://bit.ly/2l0zslL') + '.\n'));
}
/**
* Set of parsers.
*/
var parsers = {
LR0: function LR0(options) {
return this._genericLR(GRAMMAR_MODE.LR0, options);
},
SLR1: function SLR1(options) {
return this._genericLR(GRAMMAR_MODE.SLR1, options);
},
CLR1: function CLR1(options) {
return this._genericLR(GRAMMAR_MODE.CLR1, options);
},
LALR1: function LALR1(options) {
// Default algorithm for LALR(1) is "LALR(1) by SLR(1)".
return this.LALR1_BY_SLR1(options);
},
LALR1_BY_SLR1: function LALR1_BY_SLR1() {
return this._genericLR(GRAMMAR_MODE.LALR1_BY_SLR1, options);
},
LALR1_BY_CLR1: function LALR1_BY_CLR1() {
return this._genericLR(GRAMMAR_MODE.LALR1_BY_CLR1, options);
},
_genericLR: function _genericLR(mode, options) {
var grammar = getGrammar(options.grammar, mode);
console.info('\nParsing mode: ' + grammar.getMode() + '.');
// Canonical collection or LR items.
if (options.collection) {
getCanonicalCollection(grammar).print();
}
// Validates a grammar.
if (options.validate) {
validateLRGrammar(grammar);
}
// LR parsing table.
if (options.table) {
getLRParsingTable(grammar).print();
}
// Parse a string.
if (provided('parse')) {
parse(options.parse, grammar);
}
// Parse a file.
if (provided('file')) {
parse(fs.readFileSync(options.file, 'utf-8'), grammar);
}
// Output information about tokens.
if (options['tokenize']) {
tokenizeFromOptions(options, grammar.getLexGrammar());
}
// Generate parser module.
if (options.output) {
var outputFile = options.output;
var language = path.extname(outputFile).slice(1);
// Generator is language agnostic.
var GENERATORS = {
// Default.
js: require(ROOT + 'lr/lr-parser-generator-default.js').default,
// Plugins.
example: require(ROOT + 'plugins/example/lr/lr-parser-generator-example.js').default,
py: require(ROOT + 'plugins/python/lr/lr-parser-generator-py.js').default,
php: require(ROOT + 'plugins/php/lr/lr-parser-generator-php.js').default,
rb: require(ROOT + 'plugins/ruby/lr/lr-parser-generator-ruby.js').default,
cs: require(ROOT + 'plugins/csharp/lr/lr-parser-generator-csharp.js').default,
rs: require(ROOT + 'plugins/rust/lr/lr-parser-generator-rust.js').default
};
var LRParserGenerator = GENERATORS[language] || GENERATORS.js;
new LRParserGenerator({
grammar: grammar,
outputFile: outputFile,
options: generatorOptions
}).generate();
showGeneratedSuccessMessage(options.output);
}
},
LL1: function LL1(options) {
var grammar = getGrammar(options.grammar, GRAMMAR_MODE.LL1);
console.info('\nParsing mode: ' + grammar.getMode() + '.');
// LL parsing table.
if (options.table) {
var LLParsingTable = require(ROOT + 'll/ll-parsing-table').default;
new LLParsingTable({
grammar: grammar
}).print();
}
// Parse a string.
if (provided('parse')) {
parse(options.parse, grammar);
}
// Parse a file.
if (provided('file')) {
parse(fs.readFileSync(options.file, 'utf-8'), grammar);
}
// Output information about tokens.
if (options['tokenize']) {
tokenizeFromOptions(options, grammar.getLexGrammar());
}
// Generate parser module.
if (options.output) {
var outputFile = options.output;
var language = path.extname(outputFile).slice(1);
// Generator is language agnostic.
var GENERATORS = {
// Default.
js: require(ROOT + 'll/ll-parser-generator-default.js').default,
// Plugins.
example: require(ROOT + 'plugins/example/ll/ll-parser-generator-example.js').default,
py: require(ROOT + 'plugins/python/ll/ll-parser-generator-py.js').default,
php: require(ROOT + 'plugins/php/ll/ll-parser-generator-php.js').default,
rb: require(ROOT + 'plugins/ruby/ll/ll-parser-generator-ruby.js').default
};
var LLParserGenerator = GENERATORS[language];
new LLParserGenerator({
grammar: grammar,
outputFile: outputFile,
options: generatorOptions
}).generate();
showGeneratedSuccessMessage(options.output);
}
}
};
function showGeneratedSuccessMessage(filePath) {
console.info('' + colors.green('\n\u2713 Successfully generated:'), filePath, '\n');
}
function parse(string, grammar) {
console.info('\n' + colors.bold('Parsing:') + '\n\n' + string + '\n');
try {
var parsed = grammar.getMode().isLR() ? lrParse(string, grammar) : llParse(string, grammar);
if (parsed.status === 'accept') {
console.info(colors.green('\u2713 Accepted') + '\n');
}
if (parsed.hasOwnProperty('value')) {
console.info(colors.bold('Parsed value:'), '\n\n' + formatParsedOutput(parsed.value), '\n');
}
} catch (e) {
console.info(colors.red(e.stack) + '\n');
process.exit(1);
}
}
function lrParse(string, grammar) {
var LRParser = require(ROOT + 'lr/lr-parser').default;
if (options['generate-inline-parser']) {
return LRParser.fromParserGenerator({ grammar: grammar }).parse(string);
}
return new LRParser({
grammar: grammar,
resolveConflicts: options['resolve-conflicts']
}).parse(string);
}
function llParse(string, grammar) {
var LLParser = require(ROOT + 'll/ll-parser').default;
if (options['generate-inline-parser']) {
return LLParser.fromParserGenerator({ grammar: grammar }).parse(string);
}
return new LLParser({ grammar: grammar }).parse(string);
}
function formatParsedOutput(output) {
// Object constructor is used from another realm, so no direct
// constructor check, neither `instanceof` would work. Check
// `name` property.
if (Array.isArray(output) || output && output.constructor && output.constructor.name === 'Object') {
return JSON.stringify(output, null, 2);
}
return output;
}
function getGrammar(grammarFile, mode) {
if (!grammarFile) {
return null;
}
var grammarData = Grammar.dataFromGrammarFile(grammarFile, 'bnf');
// If explicit lexical grammar file was passed, use it.
var lexGrammarData = getLexGrammarData(options);
if (!grammarData.lex) {
grammarData.lex = lexGrammarData;
} else if (lexGrammarData) {
var _grammarData$lex$rule;
(_grammarData$lex$rule = grammarData.lex.rules).push.apply(_grammarData$lex$rule, _toConsumableArray(lexGrammarData.rules));
}
var grammarOptions = {
/**
* Parsing mode.
*/
mode: mode,
/**
* Whether to capture locations.
*/
captureLocations: options.loc
};
return Grammar.fromData(grammarData, grammarOptions);
}
function getLexGrammarData(options) {
var data = null;
// If explicit lexical grammar file was passed, use it.
if (options.lex) {
data = Grammar.dataFromGrammarFile(options.lex, 'lex');
}
if (options['ignore-whitespaces'] && !data) {
data = {
rules: [['\\s+', /* skip whitespace */'']]
};
}
if (options['case-insensitive']) {
if (!data) {
data = {};
}
if (!data.options) {
data.options = {};
}
data.options['case-insensitive'] = true;
}
return data;
}
function normalizeMode(mode) {
return mode.toUpperCase();
}
function getModesList() {
return Object.keys(GRAMMAR_MODE).join(', ');
}
function extractMode(options) {
var mode = options.mode;
// If no explicit mode is passed, try
// infer it from the grammar file extension.
if (!mode && options.grammar) {
mode = path.extname(options.grammar).slice(1);
}
if (!mode) {
error('\nError: "mode" option is required for parsing\n');
return null;
}
mode = normalizeMode(mode);
if (!GRAMMAR_MODE.hasOwnProperty(mode)) {
error('\nError: "' + mode + '" is not a valid parsing mode. ' + ('Valid modes are: ' + getModesList() + '.\n'));
return null;
}
if (!parsers.hasOwnProperty(mode)) {
var availableModes = Object.keys(parsers).filter(function (mode) {
return !mode.startsWith('_');
}).join(', ');
error('\nError: "' + mode + '" is not implemented yet. ' + ('Available parsers are: ' + availableModes + '.\n'));
return null;
}
return options.mode = mode;
}
function handleSets() {
var SetsGenerator = require(ROOT + 'sets-generator').default;
var sets = options.sets;
var sg = new SetsGenerator({
grammar: getGrammar(options.grammar, options.mode)
});
if (sets.indexOf('first') !== -1 || sets === 'all') {
sg.printSet(sg.getFirstSets());
}
if (sets.indexOf('follow') !== -1 || sets === 'all') {
sg.printSet(sg.getFollowSets());
}
if (sets.indexOf('predict') !== -1 || sets === 'all') {
sg.printSet(sg.getPredictSets());
}
}
function error(message) {
console.error(colors.red(message));
console.info('Run --help for details.\n');
process.exit(1);
}
function isTokenizerOnly(options) {
return options['tokenizer-only'] || options.lex && !options.grammar;
}
function handleStandaloneTokenizer() {
var LexGrammar = require(ROOT + 'grammar/lex-grammar').default;
var lexGrammarData = getLexGrammarData(options);
var lexGrammar = void 0;
if (lexGrammarData) {
lexGrammar = new LexGrammar(lexGrammarData);
} else {
// Try infer from --grammar.
lexGrammar = getGrammar(options.grammar, options.mode).getLexGrammar();
}
if (!provided('tokenize') && !provided('output')) {
error('\nError: for tokenization pass either --tokenize or --output.\n');
}
if (options['tokenize']) {
tokenizeFromOptions(options, lexGrammar);
}
}
function provided(option) {
return options.hasOwnProperty(option);
}
function tokenizeFromOptions(options, lexGrammar) {
if (!provided('parse') && !provided('file')) {
error('\nError: tokenization requires -p or -f parameter\n');
return;
}
// Tokenize a string.
if (provided('parse')) {
tokenize(options.parse, lexGrammar);
}
// Tokinize a file.
if (provided('file')) {
tokenize(fs.readFileSync(options.file, 'utf-8'), lexGrammar);
}
}
function tokenize(string, lexGrammar) {
// Inline tokenization supported only for JS.
var Tokenizer = require(ROOT + 'tokenizer').default;
var tokens = new Tokenizer({ string: string, lexGrammar: lexGrammar }).getTokens();
// Don't show last EOF token.
tokens.pop();
console.info(colors.bold('\nList of tokens:'), '\n\n', formatParsedOutput(tokens), '\n');
}
function main() {
if (options.version) {
var pkg = require('../package.json');
console.info('v' + pkg.version);
if (!options.grammar && !options.lex) {
return;
}
}
if (!options.grammar && !options.lex) {
error('\nError: expected at least --grammar or --lex parameters.\n');
return;
}
// Generating a standalone tokenizer, either from direct --lex
// parameter, or from the `lex` part of the --grammar parameter.
if (isTokenizerOnly(options)) {
return handleStandaloneTokenizer();
}
// Sets.
if (options.sets) {
handleSets();
}
parsers[extractMode(options)](options);
}
module.exports = main;
if (require.main === module) {
debug.time('Total time');
main();
debug.timeEnd('Total time');
}