"use strict";
var util = require("util");
var Logger = require("./logger"),
Parser = require("./Parser"),
StreamEmitter = require("./StreamEmitter");
util.inherits(DebuggerVariableParser, Parser);
StreamEmitter.mixin(DebuggerVariableParser);
module.exports = DebuggerVariableParser;
// tokens.
DebuggerVariableParser.ASSIGNMENT = "assignment";
DebuggerVariableParser.FAT_COMMA = "fat-comma";
DebuggerVariableParser.DEREFERENCE = "dereference";
DebuggerVariableParser.OPEN_BRACKET = "open-bracket";
DebuggerVariableParser.CLOSE_BRACKET = "close-bracket";
DebuggerVariableParser.IDENTIFIER = "identifier";
DebuggerVariableParser.STRING = "string";
DebuggerVariableParser.NUMBER = "number";
DebuggerVariableParser.SUB_REF = "sub-ref";
DebuggerVariableParser.ARRAY_REF = "array-ref";
DebuggerVariableParser.HASH_REF = "hash-ref";
DebuggerVariableParser.CODE_REF = "code-ref";
DebuggerVariableParser.EMPTY_NON_SCALAR = "empty-non-scalar";
DebuggerVariableParser.WHITESPACE = Parser.WHITESPACE;
DebuggerVariableParser.INDENT = "indent";
DebuggerVariableParser.PROMPT = Parser.PROMPT;
// virtual tokens.
DebuggerVariableParser.ARRAY = "array";
DebuggerVariableParser.ARRAY_ITEM = "array-item";
DebuggerVariableParser.ARRAY_INDEX = "array-index";
DebuggerVariableParser.ARRAY_RESULT = "array-result";
DebuggerVariableParser.HASH = "hash";
DebuggerVariableParser.HASH_PAIR = "hash-pair";
DebuggerVariableParser.HASH_RESULT = "hash-result";
DebuggerVariableParser.END = "end";
// Follow sets.
DebuggerVariableParser.REF = [ DebuggerVariableParser.ARRAY_REF, DebuggerVariableParser.HASH_REF, DebuggerVariableParser.CODE_REF ];
DebuggerVariableParser.START_NON_SCALAR = [ DebuggerVariableParser.OPEN_BRACKET ];
DebuggerVariableParser.END_NON_SCALAR = [ DebuggerVariableParser.CLOSE_BRACKET ];
DebuggerVariableParser.EXPRESSION = [ DebuggerVariableParser.STRING, DebuggerVariableParser.NUMBER ].concat(DebuggerVariableParser.REF)
.concat(DebuggerVariableParser.START_NON_SCALAR);
DebuggerVariableParser.ARRAY_POS = [ DebuggerVariableParser.NUMBER ];
DebuggerVariableParser.KEY = [ DebuggerVariableParser.STRING, DebuggerVariableParser.NUMBER ];
DebuggerVariableParser.VALUE = [ DebuggerVariableParser.STRING, DebuggerVariableParser.NUMBER ].concat(DebuggerVariableParser.REF);
DebuggerVariableParser.NON_SCALAR_ITEM = DebuggerVariableParser.KEY_VALUE_PAIR = DebuggerVariableParser.ARRAY_ENTRY =
[ DebuggerVariableParser.INDENT, DebuggerVariableParser.EMPTY_NON_SCALAR ];
DebuggerVariableParser.INDENT_LENGTH = 3;
/**
* Parses the listing of variables from the Perl debugger.
* <p>
* This should not be used directly
*
* @param {ParserConfiguration} [config]
* @constructor
* @private
*/
function DebuggerVariableParser(config) {
Parser.call(this);
this._variables = null;
this._logger = Logger.initLogger(config);
this.reset();
}
DebuggerVariableParser.prototype.reset = function() {
Parser.prototype.reset.call(this);
this._variables = [];
};
DebuggerVariableParser.prototype._tokenise = function() {
switch (this._tokenType) {
case DebuggerVariableParser.IDENTIFIER:
return this._continueIdentifier();
case DebuggerVariableParser.STRING:
return this._continueString();
case DebuggerVariableParser.NUMBER:
return this._continueNumber();
case DebuggerVariableParser.WHITESPACE:
return this._continueWhitespace();
case DebuggerVariableParser.HASH_REF:
case DebuggerVariableParser.ARRAY_REF:
case DebuggerVariableParser.CODE_REF:
case DebuggerVariableParser.SUB_REF:
return this._continueReference();
case DebuggerVariableParser.EMPTY_NON_SCALAR:
return this._continueEmptyNonScalar();
case DebuggerVariableParser.PROMPT:
return this._continuePrompt();
default:
this._beginToken();
}
};
DebuggerVariableParser.prototype._beginToken = function() {
switch (this._next[0]) {
case "$":
case "@":
case "%":
return this._newToken(DebuggerVariableParser.IDENTIFIER);
case "=":
if (!this._matchAssignment()) {
this._matchFatComma();
}
return;
case "-":
return this._matchDereference();
case "'":
return this._newToken(DebuggerVariableParser.STRING);
case "(":
return this._matchOpenBracket();
case ")":
return this._matchCloseBracket();
case "&":
return this._newToken(DebuggerVariableParser.SUB_REF);
case " ":
return this._newToken(DebuggerVariableParser.WHITESPACE);
case "\n":
// don't care.
return this._consumeChar();
case "e":
return this._newToken(DebuggerVariableParser.EMPTY_NON_SCALAR);
case "A":
return this._newToken(DebuggerVariableParser.ARRAY_REF);
case "H":
return this._newToken(DebuggerVariableParser.HASH_REF);
case "C":
return this._newToken(DebuggerVariableParser.CODE_REF);
case "D":
return this._newToken(DebuggerVariableParser.PROMPT);
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
return this._newToken(DebuggerVariableParser.NUMBER);
default:
// we don't know what we're dealing with.
throw new Error("Don't know what token is being recognised '" + this._next[0] + "'");
}
};
DebuggerVariableParser.prototype._continueIdentifier = function() {
if (this._next[0] === " ") {
return this._matchIdentifier();
}
this._continueToken();
};
DebuggerVariableParser.prototype._matchIdentifier = function() {
var symbol = this._tokenValue.substring(0, 1);
switch (symbol) {
case "$":
symbol = "scalar";
break;
case "@":
symbol = "array";
break;
case "%":
symbol = "hash";
break;
}
this._endToken({
vartype: symbol
});
this._consumeChar();
};
DebuggerVariableParser.prototype._continueString = function() {
if (this._next[0] === "'" && this._lastChar !== "\\") {
this._continueToken();
this._endToken();
return;
}
this._continueToken();
};
DebuggerVariableParser.prototype._continueNumber = function() {
if (/\d|\./.test(this._next[0])) {
this._continueToken();
return;
}
this._endToken();
// we may have started another token
this._tokenise();
};
DebuggerVariableParser.prototype._continueWhitespace = function() {
if (this._next[0] !== " ") {
var valueLength = this._tokenValue.length;
if (valueLength >= DebuggerVariableParser.INDENT_LENGTH) {
// normalise the indent
valueLength -= valueLength % DebuggerVariableParser.INDENT_LENGTH;
this._tokenType = DebuggerVariableParser.INDENT;
this._endToken({
length: valueLength
});
/*
* Don't consume as we may have started another token.
*/
}
else {
/*
* We're not getting anymore whitespace, so toss what we've got.
*/
this._resetToken();
}
this._tokenise();
}
else {
this._continueToken();
}
};
DebuggerVariableParser.prototype._continueReference = function() {
if (this._next[0] !== "\n") {
return this._continueToken();
}
return this._endToken();
};
DebuggerVariableParser.prototype._continueEmptyNonScalar = function() {
if (this._next[0] === "\n") {
this._endToken();
return this._consumeChar();
}
return this._continueToken();
};
DebuggerVariableParser.prototype._matchAssignment = function() {
if (this._next[1] && this._next[1] !== ">") {
this._newToken(DebuggerVariableParser.ASSIGNMENT);
this._endToken();
return true;
}
return false;
};
/**
* @ignore
*
* @see https://en.wikipedia.org/wiki/Fat_comma
* @private
*/
DebuggerVariableParser.prototype._matchFatComma = function() {
if (this._next[1] && this._next[1] === ">") {
this._newToken(DebuggerVariableParser.FAT_COMMA);
this._continueToken();
this._endToken();
}
};
DebuggerVariableParser.prototype._matchDereference = function() {
if (this._next[1] && this._next[1] === ">") {
this._newToken(DebuggerVariableParser.DEREFERENCE);
this._continueToken();
this._endToken();
return true;
}
return false;
};
DebuggerVariableParser.prototype._matchOpenBracket = function() {
this._newToken(DebuggerVariableParser.OPEN_BRACKET);
this._endToken();
};
DebuggerVariableParser.prototype._matchCloseBracket = function() {
this._newToken(DebuggerVariableParser.CLOSE_BRACKET);
this._endToken();
};
DebuggerVariableParser.prototype._parse = function(token) {
this._logger("Follow rules: " + stringify(this._parserAllowedFollow));
this._logger("Parsing: " + stringify(token));
if (!this._startRule(token)) {
if (!this._followRules(token)) {
throw new Error("Don't know what to do with '" + token.type + "'");
}
}
};
DebuggerVariableParser.prototype._startRule = function(token) {
if (!this._parserAllowedFollow) {
if (token.type === DebuggerVariableParser.IDENTIFIER) {
return this._pushIdentifier(token);
}
if (token.type === DebuggerVariableParser.PROMPT) {
// we've finished parsing variables
this._event("variables", this._variables);
this._event("prompt");
return true;
}
throw new Error("Illegal START token: " + token.type);
}
else {
if (token.type === DebuggerVariableParser.IDENTIFIER || token.type === DebuggerVariableParser.PROMPT) {
/*
* Unfortunately for anonymous/referenced non scalars we may not know we've finished the
* data structure until we hit another start token.
*/
this._reduceTokens();
this._parse(token);
return true;
}
}
return false;
};
DebuggerVariableParser.prototype._followRules = function(token) {
for (var i = 0; i < this._parserAllowedFollow.length; i++) {
var type = this._parserAllowedFollow[i];
if (type === token.type) {
switch (type) {
case DebuggerVariableParser.ASSIGNMENT:
return this._pushAssignment(token);
case DebuggerVariableParser.FAT_COMMA:
return this._pushFatComma(token);
case DebuggerVariableParser.DEREFERENCE:
return this._pushDereference(token);
case DebuggerVariableParser.STRING:
case DebuggerVariableParser.NUMBER:
return this._pushScalar(token);
case DebuggerVariableParser.OPEN_BRACKET:
return this._pushNonScalar(token);
case DebuggerVariableParser.CLOSE_BRACKET:
this._reduceNonRef();
return true;
case DebuggerVariableParser.HASH_REF:
return this._pushHashRef(token);
case DebuggerVariableParser.ARRAY_REF:
return this._pushArrayRef(token);
case DebuggerVariableParser.CODE_REF:
return this._pushCodeRef(token);
case DebuggerVariableParser.SUB_REF:
return this._pushSubRef(token);
case DebuggerVariableParser.INDENT:
return this._pushIndent(token);
case DebuggerVariableParser.EMPTY_NON_SCALAR:
return this._pushEmptyNonScalar(token);
}
}
}
return false;
};
DebuggerVariableParser.prototype._pushIdentifier = function(token) {
this._pushToken(token);
this._parserAllowedFollow = [ DebuggerVariableParser.ASSIGNMENT ];
return true;
};
DebuggerVariableParser.prototype._pushAssignment = function(token) {
this._pushToken(token);
this._parserAllowedFollow = DebuggerVariableParser.EXPRESSION;
return true;
};
DebuggerVariableParser.prototype._pushFatComma = function(token) {
this._pushToken(token);
this._parserAllowedFollow = DebuggerVariableParser.VALUE;
return true;
};
DebuggerVariableParser.prototype._pushDereference = function(token) {
this._pushToken(token);
this._parserAllowedFollow = [ DebuggerVariableParser.SUB_REF ];
return true;
};
DebuggerVariableParser.prototype._pushScalar = function(token) {
var top = this._topToken();
switch (top.type) {
case DebuggerVariableParser.ASSIGNMENT:
this._pushToken(token);
return true;
case DebuggerVariableParser.ARRAY_ITEM:
return this._pushArrayOperator(token);
case DebuggerVariableParser.ARRAY_INDEX:
return this._pushValue(token);
case DebuggerVariableParser.FAT_COMMA:
return this._pushValue(token);
case DebuggerVariableParser.HASH_PAIR:
return this._pushKey(token);
}
return false;
};
DebuggerVariableParser.prototype._pushArrayRef = function(token) {
this._pushToken(token);
this._pushRef(token);
this._parserAllowedFollow = DebuggerVariableParser.ARRAY_ENTRY.concat(DebuggerVariableParser.PROMPT);
return true;
};
DebuggerVariableParser.prototype._pushHashRef = function(token) {
this._pushToken(token);
this._pushRef(token);
this._parserAllowedFollow = DebuggerVariableParser.KEY_VALUE_PAIR.concat(DebuggerVariableParser.PROMPT);
return true;
};
DebuggerVariableParser.prototype._pushCodeRef = function(token) {
this._pushToken(token);
this._pushRef(token);
this._parserAllowedFollow = DebuggerVariableParser.NON_SCALAR_ITEM;
return true;
};
DebuggerVariableParser.prototype._pushSubRef = function(token) {
this._pushToken(token);
this._parserAllowedFollow = DebuggerVariableParser.NON_SCALAR_ITEM.concat(DebuggerVariableParser.END_NON_SCALAR);
return true;
};
DebuggerVariableParser.prototype._pushRef = function(token) {
this._parserAux.symbolStack.push(token);
};
DebuggerVariableParser.prototype._pushNonScalar = function(token) {
// what data structure are we starting?
var id = this._findLatestIdentifier();
switch (id.vartype) {
case "array":
return this._pushArray(token);
case "hash":
return this._pushHash(token);
}
return false;
};
DebuggerVariableParser.prototype._pushIndent = function(token) {
var currentIndent = this._parserAux.indent,
i;
if (currentIndent) {
// are we increasing or decreasing the indentation length?
if (token.length < currentIndent.length) {
// we've finished an array or hash
for (i = token.length; i < currentIndent.length; i += DebuggerVariableParser.INDENT_LENGTH) {
this._pushToken({
type: DebuggerVariableParser.END
});
this._parserAux.symbolStack.pop();
}
}
}
this._parserAux.indent = token;
// what type of non scalar, non ended variable are we in?
var tok = this._parserAux.symbolStack.top();
switch (tok.type) {
case DebuggerVariableParser.ARRAY:
case DebuggerVariableParser.ARRAY_REF:
return this._pushArrayItem(token);
case DebuggerVariableParser.HASH:
case DebuggerVariableParser.HASH_REF:
return this._pushKeyValuePair(token);
case DebuggerVariableParser.CODE_REF:
this._parserAllowedFollow = [ DebuggerVariableParser.DEREFERENCE ];
return true;
}
return false;
};
DebuggerVariableParser.prototype._pushEmptyNonScalar = function() {
// what type of non scalar variable are we in?
for (var i = this._parserStack.length - 1; i >=0; i--) {
var tok = this._parserStack[i];
switch (tok.type) {
case DebuggerVariableParser.ARRAY:
case DebuggerVariableParser.HASH:
this._parserAllowedFollow = DebuggerVariableParser.END_NON_SCALAR;
return true;
case DebuggerVariableParser.ARRAY_REF:
case DebuggerVariableParser.HASH_REF:
this._parserAllowedFollow = [ DebuggerVariableParser.INDENT ];
return true;
}
}
return false;
};
DebuggerVariableParser.prototype._pushArray = function(token) {
var virtToken = {
type: DebuggerVariableParser.ARRAY,
value: token
};
this._pushToken(virtToken);
this._parserAux.symbolStack.push(virtToken);
this._parserAllowedFollow = DebuggerVariableParser.ARRAY_ENTRY;
return true;
};
/**
* @ignore
*
* This fakes an operator for when the array is reduced.
* @private
*/
DebuggerVariableParser.prototype._pushArrayOperator = function(token) {
this._pushToken({
type: DebuggerVariableParser.ARRAY_INDEX,
value: token
});
this._parserAllowedFollow = DebuggerVariableParser.VALUE;
return true;
};
DebuggerVariableParser.prototype._pushArrayItem = function(token) {
this._pushToken({
type: DebuggerVariableParser.ARRAY_ITEM,
value: token
});
this._parserAllowedFollow = DebuggerVariableParser.ARRAY_POS.concat(DebuggerVariableParser.EMPTY_NON_SCALAR);
return true;
};
DebuggerVariableParser.prototype._pushHash = function(token) {
var virtToken = {
type: DebuggerVariableParser.HASH,
value: token
};
this._pushToken(virtToken);
this._parserAux.symbolStack.push(virtToken);
this._parserAllowedFollow = DebuggerVariableParser.KEY_VALUE_PAIR;
return true;
};
DebuggerVariableParser.prototype._pushKeyValuePair = function(token) {
this._pushToken({
type: DebuggerVariableParser.HASH_PAIR,
value: token
});
this._parserAllowedFollow = DebuggerVariableParser.KEY.concat(DebuggerVariableParser.EMPTY_NON_SCALAR);
return true;
};
DebuggerVariableParser.prototype._pushKey = function(token) {
this._pushToken(token);
this._parserAllowedFollow = [ DebuggerVariableParser.FAT_COMMA ];
return true;
};
DebuggerVariableParser.prototype._pushValue = function(token) {
this._pushToken(token);
this._parserAllowedFollow = DebuggerVariableParser.NON_SCALAR_ITEM.concat(DebuggerVariableParser.END_NON_SCALAR);
return true;
};
DebuggerVariableParser.prototype._reduceNonRef = function() {
this._reduceTokens();
this._parserAux.symbolStack.pop();
};
DebuggerVariableParser.prototype._reduceTokens = function() {
this._logger("Reducing tokens" + stringify(this._parserStack.map(function(token) {
return token.type;
})));
this._variables.push(this._reduce());
if (this._parserStack.length > 0) {
// we didn't reduce all the tokens, so we're stuffed.
throw Error("Reduction failed to reduce all tokens");
}
this._parserAllowedFollow = null;
this._parserAux.indent = null;
};
DebuggerVariableParser.prototype._reduce = function() {
var lhs, op, rhs,
items = [];
var token, next;
do {
token = this._shiftToken();
if (token) {
switch (token.type) {
case DebuggerVariableParser.END:
// we're done with this data structure.
return items;
case DebuggerVariableParser.IDENTIFIER:
lhs = token;
break;
case DebuggerVariableParser.ASSIGNMENT:
case DebuggerVariableParser.FAT_COMMA:
op = token.type;
break;
case DebuggerVariableParser.DEREFERENCE:
// do nothing
break;
case DebuggerVariableParser.STRING:
case DebuggerVariableParser.NUMBER:
if (!lhs) {
lhs = token;
}
else {
rhs = token;
}
break;
case DebuggerVariableParser.ARRAY:
case DebuggerVariableParser.ARRAY_REF:
case DebuggerVariableParser.HASH:
case DebuggerVariableParser.HASH_REF:
case DebuggerVariableParser.CODE_REF:
rhs = token;
next = this._peekAtToken();
switch (next.type) {
case DebuggerVariableParser.ARRAY_ITEM:
case DebuggerVariableParser.HASH_PAIR:
case DebuggerVariableParser.DEREFERENCE:
rhs.value = this._reduce();
break;
default:
rhs.value = [];
}
break;
case DebuggerVariableParser.ARRAY_INDEX:
op = token.type;
break;
case DebuggerVariableParser.ARRAY_ITEM:
next = this._peekAtToken();
if (DebuggerVariableParser.ARRAY_INDEX === next.type) {
// dummy assignment
lhs = token;
}
/*
* Else we have an empty array.
* This empty array can "end" either when there are no more tokens, in which case the default
* return value of this function is an empty array. Or there will be an "end" token which will return
* an empty array for us.
*/
break;
case DebuggerVariableParser.HASH_PAIR:
next = this._peekAtToken();
if (DebuggerVariableParser.KEY.indexOf(next.type) !== -1) {
items.push(this._reduce());
}
/*
* Else we have no pairs (and an empty hash).
* See ARRAY_ITEM as to why we don't return here.
*/
break;
case DebuggerVariableParser.SUB_REF:
items.push(token);
}
if (lhs && rhs) {
try {
switch (op) {
case DebuggerVariableParser.ASSIGNMENT:
return {
name: lhs.value,
type: rhs.type,
value: rhs.value
};
case DebuggerVariableParser.FAT_COMMA:
return {
key: lhs,
value: rhs
};
case DebuggerVariableParser.ARRAY_INDEX:
case DebuggerVariableParser.DEREFERENCE:
items.push(rhs);
break;
}
}
finally {
lhs = null;
rhs = null;
op = null;
}
}
}
}
while(token);
// we've got no more tokens
if (lhs || op || rhs) {
// we haven't reduced successfully
throw Error("Can't reduce expression due to no more tokens");
}
// return what we've got.
return items;
};
DebuggerVariableParser.prototype._findLatestIdentifier = function() {
for (var i = this._parserStack.length - 1; i >= 0; i--) {
var token = this._parserStack[i];
if (token.type === DebuggerVariableParser.IDENTIFIER) {
return token;
}
}
throw Error("No IDENTIFIER in parser stack");
};
function stringify(token) {
return JSON.stringify(token, null, 2);
}