Browse Source

Implement recursive operator expression support

master
Sven Slootweg 4 years ago
parent
commit
d26ae50244
  1. 10
      gulpfile.js
  2. 4
      package.json
  3. 154
      src/expression.pegjs
  4. 32
      src/index.js
  5. 9
      src/reorder-operator-expressions/add-precedences.js
  6. 82
      src/reorder-operator-expressions/find-operator-sequences.js
  7. 11
      src/reorder-operator-expressions/generate-operator-map.js
  8. 106
      src/reorder-operator-expressions/index.js
  9. 102
      src/reorder-operator-expressions/test.js
  10. 66
      src/stringify.js
  11. 14
      test.js
  12. 6
      test/let-block.nix
  13. 1
      test/nested-function-call-and-computation.nix
  14. 1
      test/set-list.nix

10
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"]);
gulp.task("default", ["babel", "pegjs", "watch"]);

4
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"
}
}

154
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
= "{"

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

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

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

11
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;
}, {});
};

106
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];
};

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

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

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

6
test/let-block.nix

@ -1,7 +1,7 @@
let
let
h = "Hello";
w = "World";
in
{
helloWorld = h + X + X;
}
helloWorld = h + X + Y * 4 + (foo 16) * 33;
}

1
test/nested-function-call-and-computation.nix

@ -0,0 +1 @@
3 + builtins.isInt 4 10 && !12 + - 6 - 5

1
test/set-list.nix

@ -0,0 +1 @@
{ f = x: x; a=1; b=2; list = [ a f b ]; }
Loading…
Cancel
Save