'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      * The MIT License (MIT)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      * Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      */

var _bnfParserGen = require('../generated/bnf-parser.gen.js');

var _bnfParserGen2 = _interopRequireDefault(_bnfParserGen);

var _grammarMode = require('./grammar-mode');

var _grammarMode2 = _interopRequireDefault(_grammarMode);

var _grammarSymbol = require('./grammar-symbol');

var _grammarSymbol2 = _interopRequireDefault(_grammarSymbol);

var _lexGrammar = require('./lex-grammar');

var _lexGrammar2 = _interopRequireDefault(_lexGrammar);

var _lexRule = require('./lex-rule');

var _lexRule2 = _interopRequireDefault(_lexRule);

var _lexParserGen = require('../generated/lex-parser.gen.js');

var _lexParserGen2 = _interopRequireDefault(_lexParserGen);

var _production = require('./production');

var _production2 = _interopRequireDefault(_production);

var _colors = require('colors');

var _colors2 = _interopRequireDefault(_colors);

var _fs = require('fs');

var _fs2 = _interopRequireDefault(_fs);

var _vm = require('vm');

var _vm2 = _interopRequireDefault(_vm);

var _debug = require('../debug');

var _debug2 = _interopRequireDefault(_debug);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

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); } }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

/**
 * Class encapsulates operations with a grammar.
 */
var Grammar = function () {
  /**
   * A grammar may be passed as an object with `lex` and `bnf` properties.
   * The `lex` part is a set of rules for the lexer, and `bnf` is actual
   * context-free grammar. If `start` property is passed, it's used as the
   * start symbol, otherwise it's inferred from the first production.
   *
   * const grammar = {
   *
   *   // Lexical grammar
   *   // format: <regexp rule>: token
   *   // The token can either be a raw string value, like "foo",
   *   // or a variable (written in ALL_CAPITALIZED notation), which
   *   // can be used further in the `bnf` grammar.
   *
   *   "lex": {
   *     "macros": {
   *       "digit": "[0-9]",
   *     },
   *
   *     "rules": [
   *       ["a", "return 'a';"],
   *       ["\\(", "return '(';"],
   *       ["\\)", "return ')';"],
   *       ["\\+", "return '+';"],
   *       ["{digit}+(\\.{digit}+)?\\b", "return 'NUMBER';"],
   *
   *       // A rule with start conditions. Such rules are matched only
   *       // when a scanner enters these states.
   *       [["string", "code"], '[^"]',  "return 'STRING';"],
   *     ],
   *
   *     "startConditions": { https://gist.github.com/DmitrySoshnikov/f5e2583b37e8f758c789cea9dcdf238a
   *       "string": 1, // inclusive condition %s
   *       "code": 0,   // exclusive consition %x
   *     },
   *   },
   *
   *   // Arbitrary code to be included in the generated parser.
   *
   *   "moduleInclude": "const AST = require('./ast');"
   *
   *   "tokens": "a ( ) + NUMBER",
   *
   *   "start": "S",
   *
   *   // BNF grammar
   *
   *   bnf: {
   *     "S": [ "F",
   *            "( S + F )" ],
   *     "F": [ "a",
   *            "NUMBER" ],
   *   }
   * };
   *
   * Note: the "bnf" can also be passed as a string:
   *
   *   bnf: `
   *     S -> F
   *        | ( S + F )
   *
   *     F -> a
   *        | NUMBER
   *   `
   *
   * Note: if no `lex` is provided, the lexical grammar is inferred
   * from the list of all terminals in the `bnf` grammar.
   */
  function Grammar(_ref) {
    var lex = _ref.lex,
        tokens = _ref.tokens,
        bnf = _ref.bnf,
        operators = _ref.operators,
        start = _ref.start,
        mode = _ref.mode,
        _ref$moduleInclude = _ref.moduleInclude,
        moduleInclude = _ref$moduleInclude === undefined ? '' : _ref$moduleInclude,
        _ref$captureLocations = _ref.captureLocations,
        captureLocations = _ref$captureLocations === undefined ? false : _ref$captureLocations;

    _classCallCheck(this, Grammar);

    this._mode = new _grammarMode2.default(mode);
    this._startSymbol = start;

    this._captureLocations = captureLocations;

    // Operators and precedence.
    this._operators = this._processOperators(operators);

    // Actual BNF grammar.
    this._originalBnf = bnf;
    this._bnf = this._processBnf(this._originalBnf);

    // Injecting user code, including handlers for `yyparse.onParseBegin`,
    // and `yyparse.onParseEnd`.
    this._moduleInclude = moduleInclude;

    this._nonTerminals = this.getNonTerminals();
    this._terminals = this.getTerminals();

    // Process tokens list, or extract from BNF.
    this._tokens = this._processTokens(tokens);

    // Lexical grammar.
    this._lexGrammar = this._createLexGrammar(lex);

    // Caching maps.
    this._productionsForSymbol = {};
    this._productionsWithSymbol = {};
  }

  /**
   * Loads a grammar object from a grammar file,
   * for the specific options.
   */


  _createClass(Grammar, [{
    key: 'getLexGrammar',


    /**
     * Returns associated lexical grammar.
     */
    value: function getLexGrammar() {
      return this._lexGrammar;
    }

    /**
     * Returns Start symbol of this grammar (it's initialized
     * during normalization process).
     */

  }, {
    key: 'getStartSymbol',
    value: function getStartSymbol() {
      return this._startSymbol;
    }

    /**
     * Returns grammar mode.
     */

  }, {
    key: 'getMode',
    value: function getMode() {
      return this._mode;
    }

    /**
     * Returns module include code.
     */

  }, {
    key: 'getModuleInclude',
    value: function getModuleInclude() {
      return this._moduleInclude;
    }

    /**
     * Whther should capture locations.
     */

  }, {
    key: 'shouldCaptureLocations',
    value: function shouldCaptureLocations() {
      return this._captureLocations;
    }

    /**
     * Returns precedence and associativity of operators.
     */

  }, {
    key: 'getOperators',
    value: function getOperators() {
      return this._operators;
    }

    /**
     * Returns list of terminals in this grammar.
     */

  }, {
    key: 'getTerminals',
    value: function getTerminals() {
      var _this = this;

      if (!this._terminals) {
        this._terminals = [];

        this._terminalsMap = {};

        this._bnf.forEach(function (production) {
          production.getRHS().forEach(function (symbol) {
            if (symbol.isTerminal() && !_this._terminalsMap.hasOwnProperty(symbol.getSymbol())) {
              _this._terminalsMap[symbol.getSymbol()] = true;
              _this._terminals.push(symbol);
            }
          });
        });
      }

      return this._terminals;
    }

    /**
     * Returns terminal symbols.
     */

  }, {
    key: 'getTerminalSymbols',
    value: function getTerminalSymbols() {
      if (!this._terminalSymbols) {
        this._terminalSymbols = this.getTerminals().map(function (symbol) {
          return symbol.getSymbol();
        });
      }
      return this._terminalSymbols;
    }

    /**
     * Returns list of non-terminals in this grammar.
     */

  }, {
    key: 'getNonTerminals',
    value: function getNonTerminals() {
      var _this2 = this;

      if (!this._nonTerminals) {
        this._nonTerminals = [];

        this._nonTerminalsMap = {};

        this._bnf.forEach(function (production) {
          if (production.isAugmented()) {
            return;
          }
          var nonTerminal = production.getLHS();
          if (!_this2._nonTerminalsMap.hasOwnProperty(nonTerminal.getSymbol())) {
            _this2._nonTerminalsMap[nonTerminal.getSymbol()] = true;
            _this2._nonTerminals.push(nonTerminal);
          }
        });
      }

      return this._nonTerminals;
    }

    /**
     * Returns list of non-terminal symbols.
     */

  }, {
    key: 'getNonTerminalSymbols',
    value: function getNonTerminalSymbols() {
      if (!this._nonTerminalSymbols) {
        this._nonTerminalSymbols = this.getNonTerminals().map(function (symbol) {
          return symbol.getSymbol();
        });
      }
      return this._nonTerminalSymbols;
    }

    /**
     * Returns tokens. Infer tokens from the grammar if
     * they were not passed explicitly.
     */

  }, {
    key: 'getTokens',
    value: function getTokens() {
      var _this3 = this;

      if (!this._tokens) {
        this._tokens = [];

        this._tokensMap = {};

        this._bnf.forEach(function (production) {
          if (production.isAugmented() || production.isEpsilon()) {
            return;
          }
          production.getRHS().forEach(function (symbol) {
            var rawSymbol = symbol.getSymbol();
            if (!symbol.isTerminal() && !_this3._nonTerminalsMap.hasOwnProperty(rawSymbol) && !_this3._tokensMap.hasOwnProperty(rawSymbol)) {
              _this3._tokensMap[rawSymbol] = true;
              _this3._tokens.push(symbol);
            }
          });
        });
      }

      return this._tokens;
    }

    /**
     * Returns token symbols.
     */

  }, {
    key: 'getTokenSymbols',
    value: function getTokenSymbols() {
      if (!this._tokenSymbols) {
        this._tokenSymbols = this.getTokens().map(function (symbol) {
          return symbol.getSymbol();
        });
      }
      return this._tokenSymbols;
    }

    /**
     * Returns grammar productions.
     */

  }, {
    key: 'getProductions',
    value: function getProductions() {
      return this._bnf;
    }

    /**
     * Returns productions for a specific non-terminal.
     */

  }, {
    key: 'getProductionsForSymbol',
    value: function getProductionsForSymbol(symbol) {
      if (!this._productionsForSymbol.hasOwnProperty(symbol)) {
        this._productionsForSymbol[symbol] = this._bnf.filter(function (production) {
          return production.getLHS().isSymbol(symbol);
        });
      }
      return this._productionsForSymbol[symbol];
    }

    /**
     * Returns productions where a non-terminal is used (appears on RHS).
     */

  }, {
    key: 'getProductionsWithSymbol',
    value: function getProductionsWithSymbol(symbol) {
      if (!this._productionsWithSymbol.hasOwnProperty(symbol)) {
        this._productionsWithSymbol[symbol] = this._bnf.filter(function (production) {
          return production.getRHSSymbolsMap().hasOwnProperty(symbol);
        });
      }
      return this._productionsWithSymbol[symbol];
    }

    /**
     * Gets a production by number.
     */

  }, {
    key: 'getProduction',
    value: function getProduction(number) {
      // LL grammars do not have augmented 0-production.
      return this._bnf[this._mode.isLL() ? number - 1 : number];
    }

    /**
     * Returns an augmented production (used in LR parsers),
     * which is built during normalization process. The augmented
     * production is always the first one.
     */

  }, {
    key: 'getAugmentedProduction',
    value: function getAugmentedProduction() {
      if (!this._mode.isLR()) {
        throw new TypeError('Augmented production is built only for LR grammars');
      }
      return this._bnf[0];
    }

    /**
     * Tokens are either raw text values like "foo", or
     * one of the variables from the lexical grammar.
     */

  }, {
    key: 'isTokenSymbol',
    value: function isTokenSymbol(symbol) {
      if (symbol instanceof _grammarSymbol2.default) {
        symbol = symbol.getSymbol();
      }

      return this._terminalsMap.hasOwnProperty(symbol) || this._tokensMap.hasOwnProperty(symbol);
    }

    /**
     * Whether a symbol is a non-terminal in this grammar.
     */

  }, {
    key: 'isNonTerminal',
    value: function isNonTerminal(symbol) {
      if (symbol instanceof _grammarSymbol2.default) {
        symbol = symbol.getSymbol();
      }

      return this._nonTerminalsMap.hasOwnProperty(symbol);
    }

    /**
     * Pretty prints the grammar.
     */

  }, {
    key: 'print',
    value: function print() {
      var _this4 = this;

      console.info('\nGrammar:\n');

      var pad = '    ';
      var productions = this.getProductions();
      var numberPad = productions.length.toString().length;

      productions.forEach(function (production) {
        var productionOutput = '' + pad + _this4._padLeft(production.getNumber(), numberPad) + '. ' + production.toString();

        console.info(productionOutput);

        if (_this4._mode.isLR() && production.isAugmented()) {
          var splitter = Array(productionOutput.length - 2).join('-');
          console.info('' + pad + splitter);
        }
      });
    }
  }, {
    key: '_padLeft',
    value: function _padLeft(value, times) {
      value = value.toString();
      var spaces = Array(times - value.length + 1).join(' ');
      return spaces + value;
    }
  }, {
    key: '_processOperators',
    value: function _processOperators(operators) {
      var processedOperators = {};

      if (operators) {
        operators.forEach(function (opData, i) {
          opData.slice(1).forEach(function (op) {
            processedOperators[op] = {
              precedence: i + 1,
              assoc: opData[0]
            };
          });
        });
      }

      return processedOperators;
    }

    /**
     * Generates data arrays for lex rules inferred from terminals.
     */

  }, {
    key: '_generateLexRulesDataForTerminals',
    value: function _generateLexRulesDataForTerminals() {
      return this.getTerminals().map(function (terminal) {
        return [_lexRule2.default.matcherFromTerminal(terminal.getSymbol()), // matcher
        'return ' + terminal.quotedTerminal() + ';'];
      } // token handler
      );
    }

    /**
     * Creates lex grammar instance.
     */

  }, {
    key: '_createLexGrammar',
    value: function _createLexGrammar(lex) {
      var _lex$rules;

      if (!lex) {
        lex = {
          rules: []
        };
      }

      // Infer automatic lex-rules from raw terminals
      // (symbols in quotes) in BNF productions RHS.
      (_lex$rules = lex.rules).push.apply(_lex$rules, _toConsumableArray(this._generateLexRulesDataForTerminals()));

      return new _lexGrammar2.default(lex);
    }

    /**
     * Processes tokens.
     */

  }, {
    key: '_processTokens',
    value: function _processTokens(tokens) {
      var _this5 = this;

      if (typeof tokens === 'string') {
        tokens = tokens.split(/\s+/);
      }

      this._tokensMap = {};

      return Array.isArray(tokens) ? tokens.map(function (token) {
        _this5._tokensMap[token] = true;
        return _grammarSymbol2.default.get(token);
      }) : this.getTokens();
    }
  }, {
    key: '_processBnf',
    value: function _processBnf(originalBnf) {
      var
      /* grammar */_this6 = this;

      var processedBnf = [];
      var nonTerminals = Object.keys(originalBnf);

      var isDefaultLR = this._mode.isLR() && !this._mode.isLALR1Extended();

      // LR grammar uses augmented 0-production.
      var number = isDefaultLR ? 0 : 1;

      if (!this._startSymbol) {
        this._startSymbol = nonTerminals[0];
      }

      // Augmented rule, $accept -> S. The LALR(1) extended
      // grammar already have this rule.
      if (isDefaultLR) {
        var augmentedProduction = new _production2.default(
        /* LHS */'$accept',
        /* RHS */this._startSymbol,
        /* number */number++,
        /* semanticAction */null,
        /* isShort */false,
        /* grammar */this);
        processedBnf[0] = augmentedProduction;
      }

      nonTerminals.forEach(function (LHS) {
        originalBnf[LHS].forEach(function (RHS, k) {
          var semanticAction = null;
          var precedence = null;

          if (Array.isArray(RHS)) {
            var precedenceTag = null;

            if (typeof RHS[1] === 'string') {
              semanticAction = RHS[1];
              if (RHS[2] !== null && typeof RHS[2] === 'object') {
                precedenceTag = RHS[2].prec;
              }
            } else if (RHS[1] !== null && typeof RHS[1] === 'object') {
              precedenceTag = RHS[1].prec;
            }

            RHS = RHS[0];

            if (precedenceTag && _this6._operators) {
              precedence = _this6._operators[precedenceTag].precedence;
            }
          }

          processedBnf.push(new _production2.default(LHS, RHS,
          /* number */number++, semanticAction,
          /* isShort */k > 0, _this6, precedence));
        });
      });

      return processedBnf;
    }
  }], [{
    key: 'fromGrammarFile',
    value: function fromGrammarFile(grammarFile) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      var grammarType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'bnf';

      var grammarData = Grammar.dataFromGrammarFile(grammarFile, grammarType);
      return Grammar.fromData(grammarData, options);
    }

    /**
     * Reads grammar file data. Supports reading `bnf`,
     * and `lex` grammars based on mode.
     */

  }, {
    key: 'dataFromGrammarFile',
    value: function dataFromGrammarFile(grammarFile) {
      var grammarType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'bnf';

      return Grammar.dataFromString(_fs2.default.readFileSync(grammarFile, 'utf-8'), grammarType);
    }

    /**
     * Creates Grammar instance from grammar data for
     * a particular parsing options.
     */

  }, {
    key: 'fromData',
    value: function fromData(grammarData) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      return new Grammar(Object.assign({}, grammarData, options));
    }

    /**
     * Creates Grammar instance from grammar string.
     */

  }, {
    key: 'fromString',
    value: function fromString(string) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      var grammarType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'bnf';

      var grammarData = Grammar.dataFromString(string, grammarType);
      return Grammar.fromData(grammarData, options);
    }

    /**
     * Generates data from grammar string.
     */

  }, {
    key: 'dataFromString',
    value: function dataFromString(grammarString) {
      var grammarType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'bnf';

      var grammarData = null;

      _debug2.default.time('Grammar loaded in');

      try {
        // Pure JSON representation.
        grammarData = JSON.parse(grammarString);
        _debug2.default.log('Grammar (' + grammarType + ') is in JSON format');
      } catch (e) {
        // JS code.
        try {
          grammarData = _vm2.default.runInNewContext('\n          (function() { return (' + grammarString + ');})()\n        ');
          _debug2.default.log('Grammar (' + grammarType + ') is in JS format');
        } catch (jsEx) {
          var jsError = jsEx.stack;

          // A grammar as a string, for BNF, and lex.
          switch (grammarType) {
            case 'bnf':
              grammarData = Grammar.parseFromType(_bnfParserGen2.default, grammarType, grammarString, jsError);

              // BNF grammar may define lexical grammar inline.
              if (typeof grammarData.lex === 'string') {
                grammarData.lex = Grammar.parseFromType(_lexParserGen2.default, 'lex', grammarData.lex, jsError);
              }
              break;

            case 'lex':
              grammarData = Grammar.parseFromType(_lexParserGen2.default, grammarType, grammarString, jsError);
              break;

            default:
              throw new Error('Unknown grammar type: ' + grammarType);
          }
        }
      }

      _debug2.default.timeEnd('Grammar loaded in');
      return grammarData;
    }

    /**
     * Parses grammar (lex or bnf).
     */

  }, {
    key: 'parseFromType',
    value: function parseFromType(ParserType, grammarType, grammarString, jsError) {
      var grammarData = null;
      try {
        grammarData = ParserType.parse(grammarString);
        _debug2.default.log('Grammar (' + grammarType + ') is in Yacc/Bison format');
      } catch (ex) {
        console.error(_colors2.default.red('\nParsing grammar in JS-format failed:\n\n') + jsError + '\n');
        console.error(_colors2.default.red('\nParsing grammar in Yacc/Bison format failed:\n\n'));
        throw ex;
      }
      return grammarData;
    }
  }]);

  return Grammar;
}();

exports.default = Grammar;