diff --git a/gulpfile.js b/gulpfile.js index 8a27f08..a2bdc67 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,7 +18,13 @@ gulp.task('babel', function() { gulp.task('pegjs', function() { return gulp.src(sources.pegjs) .pipe(presetPegjs({ - basePath: __dirname + basePath: __dirname, + pegjs: { + format: "commonjs", + dependencies: { + "reorderOperatorExpressions": "./reorder-operator-expressions" + } + } })) .pipe(gulp.dest("lib/")); }); @@ -28,4 +34,4 @@ gulp.task("watch", function () { gulp.watch(sources.pegjs, ["pegjs"]); }); -gulp.task("default", ["babel", "pegjs", "watch"]); \ No newline at end of file +gulp.task("default", ["babel", "pegjs", "watch"]); diff --git a/package.json b/package.json index 72648d0..d7a215d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@joepie91/gulp-preset-es2015": "^1.0.1", "@joepie91/gulp-preset-pegjs": "^1.0.0", "babel-preset-es2015": "^6.6.0", - "gulp": "^3.9.1" + "better-peg-tracer": "^1.0.0", + "gulp": "^3.9.1", + "pegjs": "^0.10.0" } } diff --git a/src/expression.pegjs b/src/expression.pegjs index b12108f..adcd580 100644 --- a/src/expression.pegjs +++ b/src/expression.pegjs @@ -1,42 +1,76 @@ { + let operators = [{ + associativity: "left", + operators: ["call"] + }, { + associativity: "none", + unary: true, + operators: ["numericalNegate"] + }, { + associativity: "none", + operators: ["hasAttribute"] + }, { + associativity: "right", + operators: ["++"] + }, { + associativity: "left", + operators: ["*", "/"] + }, { + associativity: "left", + operators: ["+", "-"] + }, { + associativity: "none", + unary: true, + operators: ["booleanNegate"] + }, { + associativity: "right", + operators: ["//"] + }, { + associativity: "left", + operators: ["<", "<=", ">", ">="] + }, { + // NOTE: Special case, can only occur once in sequence! + associativity: "none", + operators: ["==", "!="] + }, { + associativity: "left", + operators: ["&&"] + }, { + associativity: "left", + operators: ["||"] + }, { + // NOTE: Special case, can only occur once in sequence! + associativity: "none", + operators: ["->"] + }]; + function concatRepeat(first, rest, restIndex) { return [first].concat(rest.map(function(item) { return item[restIndex]; })); } - function unnestFunctionCalls(first, nested, last) { - var callStack = nested.concat([last]); - - function createFunctionCall(i) { - var func; - - if (i === 0) { - func = first; - } else { - func = createFunctionCall(i - 1); - } - - return { - type: "functionCall", - argument: callStack[i], - function: func - }; + function maybeReorderOperatorExpressions(node) { + if (node.type === "operatorExpression") { + return reorderOperatorExpressions(node, operators); + } else { + return node; } - - return createFunctionCall(callStack.length - 1); } } start - = _ expression:expression* _ { return expression; } + = _ expression:reorderedExpression _ { return expression; } // Character classes whitespace = "\t" / "\n" / "\r" - / " " + / space + +space + = " " stringLiteralCharacter = !('"' / "\\") char:. { return char; } @@ -66,14 +100,17 @@ _ = (whitespace / comment)* {} // Language constructs -expression - = additive - / operand +reorderedExpression + = expression:expression { return maybeReorderOperatorExpressions(expression); } -comment - = "#" chars:commentCharacter* _ { return {type: "comment", text: chars.join("")} } +expression + = numericallyNegatedOperatorExpression + / booleanNegatedOperatorExpression + / binaryOperatorExpression + / functionCallOperatorExpression + / nonOperatorExpression -operand +nonOperatorExpression = group / letBlock / stringLiteral @@ -81,26 +118,33 @@ operand / functionDefinition / recursiveSet / set - / possiblyNestedFunctionCall / identifier -additive - = left:subtractive - _ "+" - _ right:additive { return {type: "operation", operator: "+", left: left, right: right} } - / subtractive +comment + = "#" chars:commentCharacter* _ { return {type: "comment", text: chars.join("")} } + +functionCallOperatorExpression + = left:nonOperatorExpression + space + right:expression { return {type: "operatorExpression", operator: "call", left: left, right: right} } + +hasAttributeOperatorExpression + = left:nonOperatorExpression + _ "?" + _ right:identifier { return {type: "operatorExpression", operator: "?", left: left, right: right} } // FIXME: attribute path -subtractive - = left:multiplicative - _ "-" - _ right:subtractive { return {type: "operation", operator: "-", left: left, right: right} } - / multiplicative +binaryOperatorExpression + = left:nonOperatorExpression + _ operator:("++" / "+" / "-" / "*" / "/" / "//" / "<" / "<=" / ">" / ">=" / "&&" / "||" / "==" / "!=" / "->") + _ right:expression { return {type: "operatorExpression", operator: operator, left: left, right: right} } -multiplicative - = left:operand - _ "*" - _ right:multiplicative { return {type: "operation", operator: "*", left: left, right: right} } - / operand +numericallyNegatedOperatorExpression + = "-" + _ right:expression { return {type: "operatorExpression", operator: "numericalNegate", right: right} } + +booleanNegatedOperatorExpression + = "!" + _ right:expression { return {type: "operatorExpression", operator: "booleanNegate", right: right} } identifier = literal:stringLiteral { return {type: "identifier", identifier: literal.value} } @@ -108,38 +152,22 @@ identifier group = "(" - _ expression:expression + _ expression:reorderedExpression _ ")" { return {type: "group", expression: expression} } functionDefinition = argument:functionDefinitionArgument _ ":" - _ body:expression { return {type: "functionDefinition", argument: argument, body: body} } + _ body:reorderedExpression { return {type: "functionDefinition", argument: argument, body: body} } functionDefinitionArgument = identifier / setPattern -//functionCallWithParens -// = functionName:expression "(" -// _ arg:expression -// _ ")" {return {type: "functionCall", name: functionName, argument: arg} } -// -//functionCallWithoutParens -// = functionName:expression " " -// _ arg:expression {return {type: "functionCall", name: functionName, argument: arg} } -// -//functionCall -// = functionCallWithParens -// / functionCallWithoutParens - -possiblyNestedFunctionCall - = first:expression nested:expression* last:expression { return unnestFunctionCalls(first, nested, last); } - assignment = _ identifier:identifier _ "=" - _ expression:expression + _ expression:reorderedExpression _ ";" { return {type: "assignment", identifier: identifier, expression: expression} } assignmentList @@ -161,7 +189,7 @@ letBlock = "let" _ bindingList:bindingList _ "in" - _ expression:expression { return {type: "letBlock", bindings: bindingList.assignments, expression: expression} } + _ expression:reorderedExpression { return {type: "letBlock", bindings: bindingList.assignments, expression: expression} } setPattern = "{" diff --git a/src/index.js b/src/index.js index a5b11bd..c6853a5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,31 @@ 'use strict'; -const expressionParser = require("./expression"); +module.exports = function(body, options = {}) { + let expressionParser, parserOptions; -module.exports = function(body) { - return expressionParser.parse(body); -} \ No newline at end of file + if (options.tracer != null) { + const pegjs = require("pegjs"); + const fs = require("fs"); + const path = require("path"); + + let grammar = fs.readFileSync(path.join(__dirname, "../src/expression.pegjs"), {encoding: "utf8"}); + + expressionParser = pegjs.generate(grammar, { + trace: true, + format: "commonjs", + dependencies: { + "reorderOperatorExpressions": path.join(__dirname, "./reorder-operator-expressions") + } + }); + + parserOptions = { + tracer: options.tracer + }; + } else { + expressionParser = require("./expression"); + + parserOptions = {}; + } + + return expressionParser.parse(body, parserOptions); +} diff --git a/src/reorder-operator-expressions/add-precedences.js b/src/reorder-operator-expressions/add-precedences.js new file mode 100644 index 0000000..689d35f --- /dev/null +++ b/src/reorder-operator-expressions/add-precedences.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = function addPrecedences(operators) { + return operators.map((item, i) => { + return Object.assign({ + precedence: i + }, item); + }); +}; diff --git a/src/reorder-operator-expressions/find-operator-sequences.js b/src/reorder-operator-expressions/find-operator-sequences.js new file mode 100644 index 0000000..9e99986 --- /dev/null +++ b/src/reorder-operator-expressions/find-operator-sequences.js @@ -0,0 +1,82 @@ +'use strict'; + +const util = require("util"); + +module.exports = function findOperatorSequences(tree, operatorMap) { + let current = tree; + let currentIndex = 0; + let currentSequence; + + let operators = []; + let operands = []; + let operatorSequences = []; + let operatorSequenceMap = {}; + + function storeCurrentOperatorSequence() { + operatorSequences.push(currentSequence); + + if (operatorSequenceMap[currentSequence.precedence] == null) { + operatorSequenceMap[currentSequence.precedence] = []; + } + + operatorSequenceMap[currentSequence.precedence].push(currentSequence); + } + + while (current != null) { + operators.push(current.operator); + + if (current.left != null) { + operands.push(current.left); + } else { + /* Unary prefix operator */ + operands.push(null); + } + + if (operatorMap[current.operator] == null) { + throw new Error(`Encountered unknown '${current.operator}' operator`); + } + + let associativity = operatorMap[current.operator].associativity; + let precedence = operatorMap[current.operator].precedence; + + if (currentSequence != null && precedence === currentSequence.precedence) { + /* Next operator in the same sequence. */ + currentSequence.length += 1; + } else { + if (currentSequence != null) { + /* We were previously working on a different operator. */ + storeCurrentOperatorSequence(); + } + + currentSequence = { + precedence: precedence, + associativity: associativity, + length: 1, + firstIndex: currentIndex + } + } + + if (current.right == null) { + throw new Error("An error occurred; encountered a `null` right operand, but this should never happen"); + } else { + if (current.right.type === "operatorExpression") { + current = current.right; + currentIndex += 1; + } else { + /* Store the very last operand. */ + operands.push(current.right); + break; + } + } + } + + /* Store the very last sequence we were still working on. */ + storeCurrentOperatorSequence(); + + return { + operators, + operands, + operatorSequences, + operatorSequenceMap + } +}; diff --git a/src/reorder-operator-expressions/generate-operator-map.js b/src/reorder-operator-expressions/generate-operator-map.js new file mode 100644 index 0000000..a0fc4c9 --- /dev/null +++ b/src/reorder-operator-expressions/generate-operator-map.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function generateOperatorMap(operators) { + return operators.reduce((map, item, i) => { + item.operators.forEach((operator) => { + map[operator] = item; + }); + + return map; + }, {}); +}; diff --git a/src/reorder-operator-expressions/index.js b/src/reorder-operator-expressions/index.js new file mode 100644 index 0000000..65e3514 --- /dev/null +++ b/src/reorder-operator-expressions/index.js @@ -0,0 +1,106 @@ +'use strict'; + +const util = require("util"); + +const generateOperatorMap = require("./generate-operator-map"); +const findOperatorSequences = require("./find-operator-sequences"); +const addPrecedences = require("./add-precedences"); + +module.exports = function reorderOperatorExpressions(tree, operators) { + let operatorsWithPrecedence = addPrecedences(operators); + let operatorMap = generateOperatorMap(operatorsWithPrecedence); + + let sequenceResults = findOperatorSequences(tree, operatorMap); + let modifiedOperands = sequenceResults.operands.slice(); + let relocatedOperands = new Array(modifiedOperands.length); + + function lookupOperandIndex(i) { + if (relocatedOperands[i] != null) { + // FIXME: This shouldn't be recursive; instead, relocation pointers should be updated after a cascading relocation. + return lookupOperandIndex(relocatedOperands[i]); + } else { + return i; + } + } + + function getOperand(i) { + let index = lookupOperandIndex(i); + let operand = modifiedOperands[lookupOperandIndex(i)]; + + //console.log(`OPERAND AT INDEX ${index}: ${util.inspect(operand)}`) + return modifiedOperands[lookupOperandIndex(i)]; + } + + operatorsWithPrecedence.forEach((operator) => { + let currentPrecedence = operator.precedence; + let sequences = sequenceResults.operatorSequenceMap[currentPrecedence]; + + if (sequences != null) { + sequences.forEach((sequence, sequenceIndex) => { + let untilIndex = sequence.firstIndex + sequence.length; + let associativity = sequence.associativity; + + let root, current; + + function addOperator(index) { + let newNode = { + type: "operatorExpression", + operator: sequenceResults.operators[index], + right: (associativity !== "right") ? getOperand(index + 1) : undefined, + left: (associativity !== "left" && !operator.unary) ? getOperand(index) : undefined + } + + if (root == null) { + root = newNode; + } else { + if (associativity === "left") { + current.left = newNode; + } else if (associativity === "right") { + current.right = newNode; + } + } + + current = newNode; + } + + if (associativity === "none") { + /* Cannot occur more than once... */ + if (sequence.length > 1) { + // FIXME: This does not yet detect multiple occurrences in different sequences... + throw new Error(`Operators in series '${operators[sequence.precedence].join(", ")}' may only occur once in a series of operator expressions, but occurred ${sequence.length} times`); + } else { + addOperator(sequence.firstIndex); + } + } else { + if (associativity === "left") { + for (let i = untilIndex - 1; i >= sequence.firstIndex; i--) { + addOperator(i); + } + + current.left = getOperand(sequence.firstIndex); + } else if (associativity === "right") { + for (let i = sequence.firstIndex; i < untilIndex; i++) { + addOperator(i); + } + + current.right = getOperand(untilIndex); + } + } + + let newLocation = lookupOperandIndex(sequence.firstIndex); + modifiedOperands[newLocation] = root; + + for (let i = sequence.firstIndex; i < untilIndex; i++) { + modifiedOperands[i + 1] = null; + relocatedOperands[i + 1] = newLocation; + } + + // console.log(`MODIFIED OPERANDS AFTER PRECEDENCE LEVEL ${currentPrecedence} (pos ${sequence.firstIndex} - ${sequence.firstIndex + sequence.length}):`, modifiedOperands.map((modOp) => { + // return (modOp == null) ? "null" : `<${modOp.type} ${modOp.identifier || modOp.value || modOp.operator}>`; + // }).join(", ")) + }); + } + }); + + return modifiedOperands[0]; +}; diff --git a/src/reorder-operator-expressions/test.js b/src/reorder-operator-expressions/test.js new file mode 100644 index 0000000..a7f9b36 --- /dev/null +++ b/src/reorder-operator-expressions/test.js @@ -0,0 +1,102 @@ +'use strict'; + +const util = require("util"); + +const reorderOperatorExpressions = require("./index"); + +let operators = [{ + associativity: "right", + operators: ["++"] +}, { + associativity: "left", + operators: ["*", "/"] +}, { + associativity: "left", + operators: ["+", "-"] +}, { + associativity: "right", + operators: ["//"] +}, { + associativity: "left", + operators: ["<", "<=", ">", ">="] +}, { + // NOTE: Special case, can only occur once in sequence! + associativity: "none", + operators: ["==", "!="] +}, { + associativity: "left", + operators: ["&&"] +}, { + associativity: "left", + operators: ["||"] +}, { + // NOTE: Special case, can only occur once in sequence! + associativity: "none", + operators: ["->"] +}]; + +// let testData = { +// type: "operatorExpression", +// operator: "+", +// left: { type: "variable", name: "a" }, +// right: { +// type: "operatorExpression", +// operator: "+", +// left: { type: "variable", name: "b" }, +// right: { +// type: "operatorExpression", +// operator: "-", +// left: { type: "variable", name: "c" }, +// right: { +// type: "operatorExpression", +// operator: "+", +// left: { type: "variable", name: "d" }, +// right: { +// type: "operatorExpression", +// operator: "-", +// left: { type: "variable", name: "e" }, +// right: { +// type: "operatorExpression", +// operator: "+", +// left: { type: "variable", name: "f" }, +// right: { type: "variable", name: "g" } +// } +// } +// } +// } +// } +// } + +let testData = { + type: "operatorExpression", + operator: "+", + left: { type: "variable", name: "a" }, + right: { + type: "operatorExpression", + operator: "*", + left: { type: "variable", name: "b" }, + right: { + type: "operatorExpression", + operator: "&&", + left: { type: "variable", name: "c" }, + right: { + type: "operatorExpression", + operator: "*", + left: { type: "variable", name: "d" }, + right: { + type: "operatorExpression", + operator: "-", + left: { type: "variable", name: "e" }, + right: { + type: "operatorExpression", + operator: "+", + left: { type: "variable", name: "f" }, + right: { type: "variable", name: "g" } + } + } + } + } + } +} + +console.log(util.inspect(reorderOperatorExpressions(testData, operators), {colors: true, depth: null})); diff --git a/src/stringify.js b/src/stringify.js new file mode 100644 index 0000000..1be9f9a --- /dev/null +++ b/src/stringify.js @@ -0,0 +1,66 @@ +'use strict'; + +function stringifyNode(node) { + switch (node.type) { + case "operatorExpression": + return stringifyOperator(node); + case "numberLiteral": + return stringifyNumberLiteral(node); + case "stringLiteral": + return stringifyStringLiteral(node); + case "identifier": + return stringifyIdentifier(node); + default: + throw new Error(`Unexpected node type '${node.type}'`); + } +} + +function stringifyOperator(node) { + let result; + + switch (node.operator) { + case "+": + case "-": + case "/": + case "*": + case "++": + case "//": + case "&&": + case "||": + case "==": + case "!=": + case "<": + case "<=": + case ">": + case ">=": + result = `${stringifyNode(node.left)} ${node.operator} ${stringifyNode(node.right)}`; + break; + case "call": + result = `${stringifyNode(node.left)} ${stringifyNode(node.right)}`; + break; + case "numericalNegate": + result = `-${stringifyNode(node.right)}`; + break; + case "booleanNegate": + result = `!${stringifyNode(node.right)}`; + break; + } + + return `(${result})`; +} + +function stringifyStringLiteral(node) { + return `"${node.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +function stringifyNumberLiteral(node) { + return `${node.value}`; +} + +function stringifyIdentifier(node) { + return `${node.identifier}`; +} + +module.exports = function stringifyTree(tree) { + return stringifyNode(tree); +}; diff --git a/test.js b/test.js index 898d003..be5fce7 100644 --- a/test.js +++ b/test.js @@ -2,7 +2,10 @@ const util = require("util"); const fs = require("fs"); +const createBetterPegTracer = require("better-peg-tracer"); + const parse = require("./"); +const stringify = require("./lib/stringify"); function fullInspect(obj) { return util.inspect(obj, {colors: true, depth: null, customInspect: false}) @@ -10,7 +13,14 @@ function fullInspect(obj) { try { let contents = fs.readFileSync(process.argv[2]).toString(); - console.log(fullInspect(parse(contents))); + let tree = parse(contents, { + tracer: createBetterPegTracer(contents) + }); + + console.log(fullInspect(tree)); + + console.log("----"); + console.log(stringify(tree)); } catch (err) { console.log(fullInspect(err)) -} \ No newline at end of file +} diff --git a/test/let-block.nix b/test/let-block.nix index f74ce65..0a4cd7c 100644 --- a/test/let-block.nix +++ b/test/let-block.nix @@ -1,7 +1,7 @@ -let +let h = "Hello"; w = "World"; in { - helloWorld = h + X + X; -} \ No newline at end of file + helloWorld = h + X + Y * 4 + (foo 16) * 33; +} diff --git a/test/nested-function-call-and-computation.nix b/test/nested-function-call-and-computation.nix new file mode 100644 index 0000000..25eadf3 --- /dev/null +++ b/test/nested-function-call-and-computation.nix @@ -0,0 +1 @@ +3 + builtins.isInt 4 10 && !12 + - 6 - 5 diff --git a/test/set-list.nix b/test/set-list.nix new file mode 100644 index 0000000..c843b53 --- /dev/null +++ b/test/set-list.nix @@ -0,0 +1 @@ +{ f = x: x; a=1; b=2; list = [ a f b ]; }