UNPKG

syntax-cli-prog

Version:

Syntactic analysis toolkit, language agnostic parsers generator.

746 lines (615 loc) 20.4 kB
/** * 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'); }