From ac2d6bfa8df18ccc49d665beedffd920ff427342 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 26 Jul 2017 21:25:46 +0200 Subject: [PATCH] Various additions --- gulpfile.js | 9 +-- package.json | 8 ++- pegjs-options.js | 6 ++ src/expression.pegjs | 146 +++++++++++++++++++++++++++++++++++++------ src/index.js | 29 +++++++-- src/stringify.js | 108 +++++++++++++++++++++++++++----- test.js | 20 +++++- 7 files changed, 276 insertions(+), 50 deletions(-) create mode 100644 pegjs-options.js diff --git a/gulpfile.js b/gulpfile.js index a2bdc67..a893133 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -19,12 +19,9 @@ gulp.task('pegjs', function() { return gulp.src(sources.pegjs) .pipe(presetPegjs({ basePath: __dirname, - pegjs: { - format: "commonjs", - dependencies: { - "reorderOperatorExpressions": "./reorder-operator-expressions" - } - } + pegjs: Object.assign({ + format: "commonjs" + }, require("./pegjs-options")) })) .pipe(gulp.dest("lib/")); }); diff --git a/package.json b/package.json index d7a215d..234be3b 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,17 @@ ], "author": "Sven Slootweg", "license": "WTFPL", - "dependencies": {}, + "dependencies": { + "assure-array": "^1.0.0", + "left-pad": "^1.1.3" + }, "devDependencies": { "@joepie91/gulp-preset-es2015": "^1.0.1", "@joepie91/gulp-preset-pegjs": "^1.0.0", "babel-preset-es2015": "^6.6.0", "better-peg-tracer": "^1.0.0", "gulp": "^3.9.1", - "pegjs": "^0.10.0" + "pegjs": "^0.10.0", + "repeat-string": "^1.6.1" } } diff --git a/pegjs-options.js b/pegjs-options.js new file mode 100644 index 0000000..5007d7f --- /dev/null +++ b/pegjs-options.js @@ -0,0 +1,6 @@ +module.exports = { + dependencies: { + reorderOperatorExpressions: "./reorder-operator-expressions" + }, + cache: true +} diff --git a/src/expression.pegjs b/src/expression.pegjs index adcd580..9a4f32c 100644 --- a/src/expression.pegjs +++ b/src/expression.pegjs @@ -8,7 +8,7 @@ operators: ["numericalNegate"] }, { associativity: "none", - operators: ["hasAttribute"] + operators: ["?"] }, { associativity: "right", operators: ["++"] @@ -57,10 +57,29 @@ return node; } } + + function maybe(array, index) { + if (array != null) { + return array[index]; + } else { + return undefined; + } + } + + function joinBlockText(segments) { + return segments.map((segment) => { + return segment[1] + segment[2].join(""); + }).join(""); + } + + function log(value) { + console.log(value); + return value; + } } start - = _ expression:reorderedExpression _ { return expression; } + = _ expressions:(reorderedExpression _)* { return expressions.map(expression => expression[0]); } // Character classes whitespace @@ -76,9 +95,6 @@ stringLiteralCharacter = !('"' / "\\") char:. { return char; } / "\\" escapableCharacter -numberLiteralCharacter - = [0-9.] - escapableCharacter = '"' / "n" @@ -89,16 +105,45 @@ commentCharacter = [^\n\r] // Literals +// FIXME: String interpolation stringLiteral - = '"' chars:stringLiteralCharacter+ '"' { return {type: "stringLiteral", value: chars.join("")} } + = '"' chars:stringLiteralCharacter+ '"' { return {type: "stringLiteral", value: chars.join(""), multiline: false} } + +multilineString + = "''" segments:(! "''" . [^']*)* "''" { return {type: "stringLiteral", value: joinBlockText(segments), multiline: true} } numberLiteral - = chars:numberLiteralCharacter+ { return {type: "numberLiteral", value: chars.join("")} } + = [0-9]+ { return {type: "numberLiteral", value: text()} } + +path + = regularPath + / homePath + / storePath + +regularPath + = [a-zA-Z0-9\.\_\-\+]* ("/" [a-zA-Z0-9\.\_\-\+]+)+ "/"? { return {type: "path", pathType: "regular", path: text()}; } + +homePath + = "~" ("/" [a-zA-Z0-9\.\_\-\+]+)+ "/"? { return {type: "path", pathType: "home", path: text()}; } + +storePath + = "<" path:([a-zA-Z0-9\.\_\-\+]+ ("/" [a-zA-Z0-9\.\_\-\+]+)*) ">" { return {type: "path", pathType: "store", storePath: path}; } // Utilities _ = (whitespace / comment)* {} +__ + = whitespace+ {} + +reservedWord // FIXME: Missing reserved words? + = "if" + / "then" + / "else" + / "inherit" + / "with" + / "assert" // Is this really a reserved word? + // Language constructs reorderedExpression = expression:expression { return maybeReorderOperatorExpressions(expression); } @@ -106,26 +151,37 @@ reorderedExpression expression = numericallyNegatedOperatorExpression / booleanNegatedOperatorExpression + / hasAttributeOperatorExpression / binaryOperatorExpression / functionCallOperatorExpression + / divisionOperatorExpression / nonOperatorExpression nonOperatorExpression = group / letBlock / stringLiteral + / multilineString / numberLiteral / functionDefinition / recursiveSet / set + / list + / blockComment + / withStatement + / conditional + / path / identifier comment = "#" chars:commentCharacter* _ { return {type: "comment", text: chars.join("")} } +blockComment + = "/*" segments:(! "*/" . [^*]*)* "*/" { return {type: "blockComment", text: joinBlockText(segments)} } + functionCallOperatorExpression = left:nonOperatorExpression - space + __ right:expression { return {type: "operatorExpression", operator: "call", left: left, right: right} } hasAttributeOperatorExpression @@ -135,9 +191,14 @@ hasAttributeOperatorExpression binaryOperatorExpression = left:nonOperatorExpression - _ operator:("++" / "+" / "-" / "*" / "/" / "//" / "<" / "<=" / ">" / ">=" / "&&" / "||" / "==" / "!=" / "->") + _ operator:("++" / "//" / "+" / "-" / "*" / "<" / "<=" / ">" / ">=" / "&&" / "||" / "==" / "!=" / "->") _ right:expression { return {type: "operatorExpression", operator: operator, left: left, right: right} } +divisionOperatorExpression + = left:nonOperatorExpression + _ "/" + _ right:expression { return {type: "operatorExpression", operator: "/", left: left, right: right} } + numericallyNegatedOperatorExpression = "-" _ right:expression { return {type: "operatorExpression", operator: "numericalNegate", right: right} } @@ -147,14 +208,22 @@ booleanNegatedOperatorExpression _ right:expression { return {type: "operatorExpression", operator: "booleanNegate", right: right} } identifier - = literal:stringLiteral { return {type: "identifier", identifier: literal.value} } - / chars:[a-z.]i+ { return {type: "identifier", identifier: text()} } + = identifier:(! (reservedWord [^a-z0-9._-]) identifierValue) { return {type: "identifier", identifier: identifier[1]} } + +identifierValue + = literal:stringLiteral { return literal.value; } + / chars:[a-z_]i [a-z0-9._-]i+ { return text(); } group = "(" _ expression:reorderedExpression _ ")" { return {type: "group", expression: expression} } +list + = "[" + _ firstExpression:nonOperatorExpression? nextExpressions:(__ nonOperatorExpression)* + _ "]" { return {type: "list", items: [firstExpression].concat(nextExpressions.map(expr => expr[1]))} } + functionDefinition = argument:functionDefinitionArgument _ ":" @@ -165,16 +234,16 @@ functionDefinitionArgument / setPattern assignment - = _ identifier:identifier - _ "=" - _ expression:reorderedExpression - _ ";" { return {type: "assignment", identifier: identifier, expression: expression} } + = identifier:identifier + _ "=" + _ expression:reorderedExpression + _ ";" { return {type: "assignment", identifier: identifier, expression: expression} } assignmentList - = items:assignment* { return items; } + = _ items:((assignment / inheritStatement) _)* { return items.map(item => item[0]); } bindingList - = items:assignment* { return {type: "bindings", assignments: items} } + = items:(assignment _)* { return {type: "bindings", assignments: items.map(item => item[0])} } set = "{" @@ -189,19 +258,56 @@ letBlock = "let" _ bindingList:bindingList _ "in" - _ expression:reorderedExpression { return {type: "letBlock", bindings: bindingList.assignments, expression: expression} } + _ expression:reorderedExpression { return {type: "letBlock", bindings: bindingList.assignments, expression: expression}; } + +conditional + = conditionalWithAlternative + / conditionalWithoutAlternative + +// The following rule exists to force the alternative to parse, if it exists. +conditionalWithAlternative + = original:conditionalWithoutAlternative + _ alternative:elseClause { return Object.assign({alternative: alternative}, original); } + +conditionalWithoutAlternative + = "if" + _ condition:expression + _ "then" + _ body:expression { return {type: "conditional", condition: condition, body: body}; } + +elseClause + = "else" + _ alternative:expression { return alternative; } + +withStatement + = "with" + _ identifier:identifier + _ ";" + _ expression + +inheritStatement // FIXME: Distinguish between identifiers and attribute paths? + = "inherit" + _ namespace:("(" identifier ")")? + _ identifiers:(identifier _)+ + _ ";" { return {type: "inheritStatement", identifiers: identifiers.map(item => item[0]), namespace: maybe(namespace, 1)} } setPattern = "{" _ args:setPatternVariableList - _ "}" { return {type: "setPattern", variables: args} } + _ "}" rest:storedRestParameter? { return {type: "setPattern", variables: args, rest}; } setPatternVariableList = firstItem:setPatternVariable otherItems:(_ "," _ setPatternVariable)* { return concatRepeat(firstItem, otherItems, 3); } setPatternVariable = restParameter - / identifier + / setPatternIdentifier + +setPatternIdentifier + = identifier:identifier defaultValue:(_ "?" _ expression)? { return {type: "setPatternIdentifier", identifier, defaultValue: maybe(defaultValue, 3)}; } restParameter = "..." { return {type: "restParameter"} } + +storedRestParameter + = "@" identifier:identifier { return identifier; } diff --git a/src/index.js b/src/index.js index c6853a5..2ed5563 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,19 @@ 'use strict'; +const path = require("path"); + +function fixDependencyPaths(dependencies) { + /* When generating a parser on runtime, the parser source will be evaluated + * from within the PEG.js source code, which means that relative dependency + * paths will not work. This function patches all relative paths, by + * resolving them to absolute paths relative from this file. */ + + return Object.keys(dependencies).reduce((obj, key) => { + obj[key] = path.join(__dirname, dependencies[key]); + return obj; + }, {}); +} + module.exports = function(body, options = {}) { let expressionParser, parserOptions; @@ -10,13 +24,16 @@ module.exports = function(body, options = {}) { let grammar = fs.readFileSync(path.join(__dirname, "../src/expression.pegjs"), {encoding: "utf8"}); - expressionParser = pegjs.generate(grammar, { + let pegOptions = require("../pegjs-options.js"); + + if (pegOptions.dependencies != null) { + pegOptions.dependencies = fixDependencyPaths(pegOptions.dependencies); + } + + expressionParser = pegjs.generate(grammar, Object.assign({ trace: true, - format: "commonjs", - dependencies: { - "reorderOperatorExpressions": path.join(__dirname, "./reorder-operator-expressions") - } - }); + format: "commonjs" + }, pegOptions)); parserOptions = { tracer: options.tracer diff --git a/src/stringify.js b/src/stringify.js index 1be9f9a..f5c041c 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -1,21 +1,59 @@ 'use strict'; -function stringifyNode(node) { +const repeatString = require("repeat-string"); +const assureArray = require("assure-array"); + +function debugPrintNode(node) { + return `${node.type} | ${node.value || node.operator || node.identifier}`; +} + +function prettyPrint(items, indentation) { + if (indentation == null) { + throw new Error(`Missing indentation parameter (node: ${debugPrintNode(node)})`); + } + + return items.map((item) => { + if (typeof item === "string") { + return item.split("\n").map((line) => { + return repeatString("\t", indentation) + line; + }).join("\n"); + } else { + return prettyPrint(item, indentation + 1); + } + }).join("\n"); +} + + +function stringifyNode(node, indentation) { + if (indentation == null) { + throw new Error(`Missing indentation parameter (node: ${debugPrintNode(node)})`); + } + switch (node.type) { case "operatorExpression": - return stringifyOperator(node); + return stringifyOperator(node, indentation); case "numberLiteral": - return stringifyNumberLiteral(node); + return stringifyNumberLiteral(node, indentation); case "stringLiteral": - return stringifyStringLiteral(node); + return stringifyStringLiteral(node, indentation); case "identifier": - return stringifyIdentifier(node); + return stringifyIdentifier(node, indentation); + case "set": + return stringifySet(node, indentation); + case "letBlock": + return stringifyLetBlock(node, indentation); + case "list": + return stringifyList(node, indentation); + case "functionDefinition": + return stringifyFunctionDefinition(node, indentation); + case "group": + return stringifyGroup(node, indentation); default: throw new Error(`Unexpected node type '${node.type}'`); } } -function stringifyOperator(node) { +function stringifyOperator(node, indentation) { let result; switch (node.operator) { @@ -33,34 +71,76 @@ function stringifyOperator(node) { case "<=": case ">": case ">=": - result = `${stringifyNode(node.left)} ${node.operator} ${stringifyNode(node.right)}`; + result = `${stringifyNode(node.left, indentation)} ${node.operator} ${stringifyNode(node.right, indentation)}`; break; case "call": - result = `${stringifyNode(node.left)} ${stringifyNode(node.right)}`; + result = `${stringifyNode(node.left, indentation)} ${stringifyNode(node.right, indentation)}`; break; case "numericalNegate": - result = `-${stringifyNode(node.right)}`; + result = `-${stringifyNode(node.right, indentation)}`; break; case "booleanNegate": - result = `!${stringifyNode(node.right)}`; + result = `!${stringifyNode(node.right, indentation)}`; break; } return `(${result})`; } -function stringifyStringLiteral(node) { +function stringifyStringLiteral(node, indentation) { return `"${node.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } -function stringifyNumberLiteral(node) { +function stringifyNumberLiteral(node, indentation) { return `${node.value}`; } -function stringifyIdentifier(node) { +function stringifyIdentifier(node, indentation) { return `${node.identifier}`; } +function stringifyBinding(node, indentation) { + return `${stringifyNode(node.identifier, indentation)} = ${stringifyNode(node.expression, indentation)};` +} + +function stringifyGroup(node, indentation) { + return `(${stringifyNode(node.expression, indentation)})` +} + +function stringifySet(node, indentation) { + return prettyPrint([ + "{", + node.assignments.map((binding) => { + return stringifyBinding(binding, indentation); + }), + "}" + ], indentation); +} + +function stringifyLetBlock(node, indentation) { + return prettyPrint([ + "let", + node.bindings.map((binding) => { + return stringifyBinding(binding, indentation); + }), + "in", + [stringifyNode(node.expression, indentation)] + ], indentation); +} + +function stringifyList(node, indentation) { + return `[ ${node.items.map(item => stringifyNode(item, indentation)).join(" ")} ]`; +} + +function stringifyFunctionDefinition(node, indentation) { + return prettyPrint([ + `${stringifyNode(node.argument, indentation)}:`, + [stringifyNode(node.body, indentation)] + ], indentation); +} + module.exports = function stringifyTree(tree) { - return stringifyNode(tree); + return assureArray(tree).map((subTree) => { + return stringifyNode(subTree, 0); + }).join("\n"); }; diff --git a/test.js b/test.js index be5fce7..876eb30 100644 --- a/test.js +++ b/test.js @@ -3,6 +3,8 @@ const util = require("util"); const fs = require("fs"); const createBetterPegTracer = require("better-peg-tracer"); +const pegjsBacktrace = require("pegjs-backtrace"); +const pegjsPermutationTracer = require("pegjs-permutation-tracer"); const parse = require("./"); const stringify = require("./lib/stringify"); @@ -11,10 +13,23 @@ function fullInspect(obj) { return util.inspect(obj, {colors: true, depth: null, customInspect: false}) } +let trace, file; + +if (process.argv[2] === "--trace") { + trace = true; + file = process.argv[3]; +} else { + trace = false; + file = process.argv[2]; +} + +let contents = fs.readFileSync(file).toString(); +let tracer = pegjsPermutationTracer(contents); + try { - let contents = fs.readFileSync(process.argv[2]).toString(); let tree = parse(contents, { - tracer: createBetterPegTracer(contents) + tracer: trace ? createBetterPegTracer(contents) : undefined + // tracer: trace ? tracer : undefined }); console.log(fullInspect(tree)); @@ -23,4 +38,5 @@ try { console.log(stringify(tree)); } catch (err) { console.log(fullInspect(err)) + tracer.printPermutations(194); }