From a1afc0b4cd79dc0b32202b5779f3c7b9c4ff297b Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 16 Nov 2015 20:32:09 -0800 Subject: [PATCH 01/35] Stub for JSON to Bolt decoder. --- bin/firebase-bolt | 132 +++++++++++++++++++++++++++++-------------- samples/decoded.bolt | 6 ++ samples/decoded.json | 13 +++++ src/bolt.ts | 3 + src/rules-decoder.ts | 22 ++++++++ src/test/cli-test.ts | 6 ++ 6 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 samples/decoded.bolt create mode 100644 samples/decoded.json create mode 100644 src/rules-decoder.ts diff --git a/bin/firebase-bolt b/bin/firebase-bolt index bc4a856..8e4d918 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -26,6 +26,8 @@ var pkg = require('../package.json'); var VERSION_STRING = "Firebase Bolt v" + pkg.version; +var commandName = 'bolt'; + var opts = { boolean: ['version', 'help'], string: ['output'], @@ -42,6 +44,20 @@ var opts = { } }; +var commands = { + migrate: function(args) { + if (args._.length != 2) { + log("Missing JSON file name to migrate to bolt file."); + process.exit(1); + } + var inputFileName = getInputFileName(args._[1], 'json'); + var outputFileName = getOutputFileName(args.output, inputFileName, 'bolt'); + readInputFile(inputFileName, 'JSON', function(jsonData) { + writeOutputFile(outputFileName, bolt.decodeJSON(jsonData)); + }); + } +}; + function main() { var args = parseArgs(process.argv.slice(2), opts); @@ -60,73 +76,105 @@ function main() { usage(0); } + // command file + if (args._.length >= 1 && commands[args._[0]] !== undefined) { + commandName += '-' + args._[0]; + commands[args._[0]](args); + return; + } + if (args._.length > 1) { log("Can only compile a single file."); usage(1); } - // Read Bolt file from stdin - if (args._.length === 0) { - if (process.stdin.isTTY) { - log("Type in a Bolt file terminated by CTRL-D."); - } - readFile(process.stdin, function(data) { - if (args.output !== undefined) { - writeTranslation(util.ensureExtension(args.output, 'json'), data); - } else { - console.log(translateRules(data)); - } - }); - return; + var inputFileName = getInputFileName(args._[0], 'bolt'); + var outputFileName = getOutputFileName(args.output, inputFileName, 'json'); + readInputFile(inputFileName, 'Bolt', function(boltData) { + writeOutputFile(outputFileName, translateRules(boltData)); + }); +} + +function getInputFileName(name, extension) { + // Read file from stdin + if (name === undefined) { + return undefined; } + return util.ensureExtension(name, extension); +} - // Read Bolt file and write json file. - var inFile = util.ensureExtension(args._[0], bolt.FILE_EXTENSION); - var outFile; - if (args.output) { - outFile = util.ensureExtension(args.output, 'json'); +function getOutputFileName(name, inputFileName, extension) { + if (name === undefined) { + if (inputFileName === undefined) { + return undefined; + } + name = util.replaceExtension(inputFileName, extension); } else { - outFile = util.replaceExtension(inFile, 'json'); + name = util.ensureExtension(name, extension); } - if (inFile === outFile) { - log("Cannot overwrite input file: " + inFile); - log("(Did you mean '" + util.replaceExtension(inFile, 'bolt') + "'?)"); + if (name === inputFileName) { + log("Cannot overwrite input file: " + inputFileName); + log("(Did you mean '" + + util.replaceExtension(name, extension == 'json' ? 'bolt' : 'json') + + "'?)"); process.exit(1); } - fs.readFile(inFile, 'utf8', function(err, data) { + return name; +} + +function readInputFile(name, kind, callback) { + if (name === undefined) { + if (process.stdin.isTTY) { + log("Type in a " + kind + " file terminated by CTRL-D."); + } + readStream(process.stdin, callback); + return; + } + + fs.readFile(name, 'utf8', function(err, data) { if (err) { - log("Could not read file: " + inFile); + log("Could not read file: " + name); process.exit(1); } - writeTranslation(outFile, data); + callback(data); }); } -function writeTranslation(outFile, data) { - log("Generating " + outFile + "..."); - fs.writeFile(outFile, translateRules(data) + '\n', 'utf8', function(err2) { - if (err2) { - log("Could not write file: " + outFile); - process.exit(1); - } - }); +function writeOutputFile(name, data) { + if (name === undefined) { + console.log(data + '\n'); + } else { + log("Generating " + name + "..."); + fs.writeFile(name, data + '\n', 'utf8', function(err) { + if (err) { + log("Could not write file: " + outputFile); + process.exit(1); + } + }); + } } function usage(code) { var cmdName = process.argv[1].split('/').slice(-1); - console.error("Translate Firebase Bolt file into JSON rules format.\n"); + console.error("Translate Firebase Bolt file into JSON rules format"); + console.error(" (or use migrate sub-command to generate a Bolt file equivalent\n" + + " of a Firebase JSON rule set).\n"); + + console.error(" Usage: " + cmdName + " [options] [file[.bolt]]"); + console.error(" " + cmdName + " [options] migrate [file[.json]]\n"); - console.error(" Usage: " + cmdName + " [options] [file]\n"); + console.error(" Examples: " + cmdName + " rules.bolt --output rules.json"); + console.error(" " + cmdName + " < rules.bolt > rules.json"); + console.error(" " + cmdName + " rules"); + console.error(" (rules.bolt => rules.json)"); + console.error(" " + cmdName + " migrate rules.json"); + console.error(" (rules.json => rules.bolt)\n"); - console.error(" Examples: " + cmdName + " myapp.bolt --output rules.json"); - console.error(" " + cmdName + " < myapp.bolt > rules.json"); - console.error(" " + cmdName + " myapp"); - console.error(" (myapp.bolt => myapp.json)\n"); console.error(" Options:\n"); console.error(util.formatColumns(4, [ ["-h --help", "Display this helpful message."], - ["-o --output file", "Output to file.json."], + ["-o --output file", "Output to file."], ["-v --version", "Display Firebase Bolt version."], [] ]).join('\n')); @@ -136,7 +184,7 @@ function usage(code) { main(); -function readFile(f, callback) { +function readStream(f, callback) { var input = ""; f.setEncoding('utf8'); @@ -172,7 +220,7 @@ function translateRules(input) { } function log(message, line, column) { - var parts = ['bolt']; + var parts = [commandName]; if (line) { util.extendArray(parts, [line, column]); } diff --git a/samples/decoded.bolt b/samples/decoded.bolt new file mode 100644 index 0000000..65aa3a9 --- /dev/null +++ b/samples/decoded.bolt @@ -0,0 +1,6 @@ +path /users/$uid { + read() = auth != null; + write() = auth != null && auth.uid == $uid; +} + +path /users/$uid/name is String; diff --git a/samples/decoded.json b/samples/decoded.json new file mode 100644 index 0000000..decde7c --- /dev/null +++ b/samples/decoded.json @@ -0,0 +1,13 @@ +{ + "rules": { + "users": { + "$uid": { + ".read": "auth != null", + ".write": "auth != null && auth.uid == $uid", + "name": { + ".validate": "newData.isString()" + } + } + } + } +} diff --git a/src/bolt.ts b/src/bolt.ts index d308c59..57470f7 100644 --- a/src/bolt.ts +++ b/src/bolt.ts @@ -17,16 +17,19 @@ var parser = require('./rules-parser'); import generator = require('./rules-generator'); +import decoder = require('./rules-decoder'); import simulator = require('./simulator'); import astReal = require('./ast'); import util = require('./util'); export var FILE_EXTENSION = 'bolt'; + export var parse = util.maybePromise(parser.parse); export var generate = util.maybePromise(generateSync); export var Generator = generator.Generator; export var ast = astReal; export var decodeExpression = ast.decodeExpression; +export var decodeJSON = decoder.decodeJSON; export var rulesSuite = simulator.rulesSuite; // Usage: diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts new file mode 100644 index 0000000..3ab6481 --- /dev/null +++ b/src/rules-decoder.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +// import util = require('./util'); +// import ast = require('./ast'); + +export function decodeJSON(json) { + return "// Bolt file auto-generated from JSON file."; +} diff --git a/src/test/cli-test.ts b/src/test/cli-test.ts index b3cabd7..3d48888 100644 --- a/src/test/cli-test.ts +++ b/src/test/cli-test.ts @@ -58,6 +58,12 @@ suite("firebase-bolt CLI", function() { expect: {out: /^$/, err: /bolt: Could not read file: nosuchfile.bolt/} }, { data: "two files", expect: {out: /^$/, err: /bolt: Can only compile a single file/} }, + { data: "migrate", + expect: {out: /^$/, err: /bolt-migrate: Missing JSON file name/} }, + + // Migrate from json file + { data: "migrate samples/decoded --output " + TMP_DIR + "decoded", + expect: {out: /^$/, err: /^bolt-migrate: Generating tmp\/decoded.bolt\.\.\.\n$/} }, ]; helper.dataDrivenTest(tests, function(data, expect) { From 8291d6304f1d8b92ab4d6f9ae76d9810564f5573 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 19 Nov 2015 14:50:03 -0800 Subject: [PATCH 02/35] Build basic Bolt expressions from JSON. --- bin/firebase-bolt | 3 +- src/rules-decoder.ts | 66 ++++++++++++++++++++++++++++++++++++++-- src/test/decoder-test.ts | 45 +++++++++++++++++++++++++++ src/test/util-test.ts | 20 ++++++++++-- src/util.ts | 8 +++++ tslint.json | 2 +- 6 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 src/test/decoder-test.ts diff --git a/bin/firebase-bolt b/bin/firebase-bolt index 8e4d918..6c786e6 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -53,7 +53,8 @@ var commands = { var inputFileName = getInputFileName(args._[1], 'json'); var outputFileName = getOutputFileName(args.output, inputFileName, 'bolt'); readInputFile(inputFileName, 'JSON', function(jsonData) { - writeOutputFile(outputFileName, bolt.decodeJSON(jsonData)); + jsonData = util.stripComments(jsonData); + writeOutputFile(outputFileName, bolt.decodeJSON(JSON.parse(jsonData))); }); } }; diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 3ab6481..2e3c4d5 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -15,8 +15,68 @@ */ /// // import util = require('./util'); -// import ast = require('./ast'); +import ast = require('./ast'); +var parser = require('./rules-parser'); -export function decodeJSON(json) { - return "// Bolt file auto-generated from JSON file."; +export var PREAMBLE = "// Bolt file auto-generated from JSON file.\n"; + +export function decodeJSON(json: Object): string { + var formatter = new Formatter; + formatter.decodeParts('/', json['rules']); + return formatter.toString(); +} + +class Formatter { + exps: { [path: string]: { [method: string]: string } }; + indent: number; + + constructor() { + this.exps = {}; + } + + decodeParts(path: string, json: Object) { + for (var key in json) { + if (key[0] === '.') { + this.emit(path, key.slice(1), json[key]); + } else { + this.decodeParts(childPath(path, key), json[key]); + } + } + } + + emit(path: string, method: string, expString: string) { + if (this.exps[path] === undefined) { + this.exps[path] = {}; + } + this.exps[path][method] = decodeJSONExpression(expString); + } + + toString(): string { + let lines = []; + Object.keys(this.exps).sort().forEach((path) => { + let methods = this.exps[path]; + lines.push("path " + path + " {"); + for (let method in methods) { + lines.push(" " + method + "() = " + methods[method] + ";"); + } + lines.push("}"); + }); + return PREAMBLE + lines.join('\n') + '\n'; + } +} + +function decodeJSONExpression(expString: string): string { + return ast.decodeExpression(parse(expString)); +} + +function parse(s: string): ast.Exp { + var result = parser.parse("f() = " + s + ";"); + return result.functions.f.body; +} + +function childPath(path: string, child: string): string { + if (path.slice(-1) === '/') { + return path + child; + } + return path + '/' + child; } diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts new file mode 100644 index 0000000..ef38fd8 --- /dev/null +++ b/src/test/decoder-test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +/// +/// + +import decoder = require('../rules-decoder'); +import helper = require('./test-helper'); + +import chai = require('chai'); +chai.config.truncateThreshold = 1000; +var assert = chai.assert; + + +suite("JSON Rules Decoder", function() { + suite("Basic Samples", function() { + var tests = [ + { data: { rules: {".read": "true", ".write": "true"} }, + expect: decoder.PREAMBLE + "path / {\n read() = true;\n write() = true;\n}\n", + }, + + { data: { rules: { "a": { ".read": "true", ".write": "true"}} }, + expect: decoder.PREAMBLE + "path /a {\n read() = true;\n write() = true;\n}\n", + }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + var result = decoder.decodeJSON(data); + assert.deepEqual(result, expect); + }); + }); +}); diff --git a/src/test/util-test.ts b/src/test/util-test.ts index 31604eb..ba97634 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -20,7 +20,7 @@ import helper = require('./test-helper'); var util = require('../util'); suite("Util", function() { - suite("pruneEmptyChildren", function() { + suite("pruneEmptyChildren", () => { function T() { this.x = 'dummy'; } @@ -36,9 +36,25 @@ suite("Util", function() { [ {a: {a: {a: {}, b: 1}}}, {a: {a: {b: 1}}} ], ]; - helper.dataDrivenTest(tests, function(data, expect) { + helper.dataDrivenTest(tests, (data, expect) => { util.pruneEmptyChildren(data); assert.deepEqual(data, expect); }); }); + + suite("stripComments", () => { + var tests = [ + [ "abc", "abc" ], + [ "a /* comment */ c", "a c" ], + [ "a /* comment */ c /* comment */ d", "a c d" ], + [ "a /* comment\n */ c\n /* comment\n */ d", "a c\n d" ], + [ "a // comment", "a " ], + [ "a // comment \nb", "a \nb" ], + ]; + + helper.dataDrivenTest(tests, (data, expect) => { + let result = util.stripComments(data); + assert.equal(result, expect); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 30d4b62..105f859 100644 --- a/src/util.ts +++ b/src/util.ts @@ -257,3 +257,11 @@ function fillString(s: string, n: number): string { } return s; } + +// Remove all single and multi-line command from string. +// Note use of *? - "lazy" match (not greedy). +export function stripComments(s: string) : string { + return s + .replace(/\/\*[^]*?\*\//g, '') + .replace(/\/\/.*/g, ''); +} diff --git a/tslint.json b/tslint.json index 5ea7358..5145b1a 100644 --- a/tslint.json +++ b/tslint.json @@ -4,7 +4,7 @@ "comment-format": [true, "check-space"], "curly": true, "eofline": true, - "forin": true, + "forin": false, "indent": [true, "spaces"], "label-position": true, "label-undefined": true, From 302ddd873b88420cf5c056e73aa2bec7a0403b62 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 19 Nov 2015 16:00:16 -0800 Subject: [PATCH 03/35] Handle simple type validations. --- samples/decoded.bolt | 2 +- src/rules-decoder.ts | 57 +++++++++++++++++++++++++++++++--------- src/test/decoder-test.ts | 8 ++++-- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/samples/decoded.bolt b/samples/decoded.bolt index 65aa3a9..095694e 100644 --- a/samples/decoded.bolt +++ b/samples/decoded.bolt @@ -1,6 +1,6 @@ +// Bolt file auto-generated from JSON file. path /users/$uid { read() = auth != null; write() = auth != null && auth.uid == $uid; } - path /users/$uid/name is String; diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 2e3c4d5..ebe0c56 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -18,7 +18,13 @@ import ast = require('./ast'); var parser = require('./rules-parser'); -export var PREAMBLE = "// Bolt file auto-generated from JSON file.\n"; +export let PREAMBLE = "// Bolt file auto-generated from JSON file.\n"; + +let typeIndicators = { + "newData.isString()": "String", + "newData.isNumber()": "Number", + "newData.isBoolean()": "Boolean" +}; export function decodeJSON(json: Object): string { var formatter = new Formatter; @@ -26,8 +32,18 @@ export function decodeJSON(json: Object): string { return formatter.toString(); } +class PathConstraints { + type: ast.ExpType; + methods: { [name: string]: string }; + + constructor(typeName: string) { + this.type = ast.typeType('Any'); + this.methods = {}; + } +} + class Formatter { - exps: { [path: string]: { [method: string]: string } }; + exps: { [path: string]: PathConstraints }; indent: number; constructor() { @@ -45,30 +61,45 @@ class Formatter { } emit(path: string, method: string, expString: string) { + // Normalize expression + expString = ast.decodeExpression(parse(expString)); + if (this.exps[path] === undefined) { - this.exps[path] = {}; + this.exps[path] = new PathConstraints('Any'); + } + + let pc = this.exps[path]; + + if (method === 'validate' && typeIndicators[expString]) { + pc.type = ast.typeType(typeIndicators[expString]); + } else { + pc.methods[method] = expString; } - this.exps[path][method] = decodeJSONExpression(expString); } toString(): string { let lines = []; Object.keys(this.exps).sort().forEach((path) => { - let methods = this.exps[path]; - lines.push("path " + path + " {"); - for (let method in methods) { - lines.push(" " + method + "() = " + methods[method] + ";"); + let pc = this.exps[path]; + let line = "path " + path; + + if (( pc.type).name !== 'Any') { + line += " is " + ast.decodeExpression(pc.type); + } + if (Object.keys(pc.methods).length === 0) { + lines.push(line + ";"); + return; + } + lines.push(line + " {"); + for (let method in pc.methods) { + lines.push(" " + method + "() = " + pc.methods[method] + ";"); } lines.push("}"); }); - return PREAMBLE + lines.join('\n') + '\n'; + return PREAMBLE + lines.join('\n'); } } -function decodeJSONExpression(expString: string): string { - return ast.decodeExpression(parse(expString)); -} - function parse(s: string): ast.Exp { var result = parser.parse("f() = " + s + ";"); return result.functions.f.body; diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index ef38fd8..ed5ec48 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -29,11 +29,15 @@ suite("JSON Rules Decoder", function() { suite("Basic Samples", function() { var tests = [ { data: { rules: {".read": "true", ".write": "true"} }, - expect: decoder.PREAMBLE + "path / {\n read() = true;\n write() = true;\n}\n", + expect: decoder.PREAMBLE + "path / {\n read() = true;\n write() = true;\n}", }, { data: { rules: { "a": { ".read": "true", ".write": "true"}} }, - expect: decoder.PREAMBLE + "path /a {\n read() = true;\n write() = true;\n}\n", + expect: decoder.PREAMBLE + "path /a {\n read() = true;\n write() = true;\n}", + }, + + { data: { rules: { "a": { ".validate": "newData.isString()"}} }, + expect: decoder.PREAMBLE + "path /a is String;", }, ]; From cf8e5d43c989ec43eda7cd1944f0a20d6a0fd92b Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 19 Nov 2015 18:04:02 -0800 Subject: [PATCH 04/35] Able to parse JSON with comments. --- bin/firebase-bolt | 7 ++++++- package.json | 3 ++- src/rules-decoder.ts | 6 +++++- src/test/util-test.ts | 16 ---------------- src/util.ts | 8 -------- 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/bin/firebase-bolt b/bin/firebase-bolt index 6c786e6..72b49ff 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -23,6 +23,7 @@ var parseArgs = require('minimist'); var util = require('../lib/util'); var bolt = require('../lib/bolt'); var pkg = require('../package.json'); +var stripComments = require('strip-json-comments'); var VERSION_STRING = "Firebase Bolt v" + pkg.version; @@ -53,7 +54,11 @@ var commands = { var inputFileName = getInputFileName(args._[1], 'json'); var outputFileName = getOutputFileName(args.output, inputFileName, 'bolt'); readInputFile(inputFileName, 'JSON', function(jsonData) { - jsonData = util.stripComments(jsonData); + jsonData = stripComments(jsonData); + jsonData = jsonData.replace(/"([^\\"]|\\[^])*"/g, function(s) { + return s.replace(/\n/g, '\\n'); + }); + console.log(jsonData); writeOutputFile(outputFileName, bolt.decodeJSON(JSON.parse(jsonData))); }); } diff --git a/package.json b/package.json index b5cce06..2247843 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "minimist": "^1.2.0", "mocha": "^2.2.5", "node-uuid": "^1.4.3", - "promise": "^7.0.4" + "promise": "^7.0.4", + "strip-json-comments": "^2.0.0" } } diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index ebe0c56..68111e3 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -62,7 +62,11 @@ class Formatter { emit(path: string, method: string, expString: string) { // Normalize expression - expString = ast.decodeExpression(parse(expString)); + try { + expString = ast.decodeExpression(parse(expString)); + } catch (e) { + throw new Error("Could not parse expression: '" + expString + "'"); + } if (this.exps[path] === undefined) { this.exps[path] = new PathConstraints('Any'); diff --git a/src/test/util-test.ts b/src/test/util-test.ts index ba97634..3891db0 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -41,20 +41,4 @@ suite("Util", function() { assert.deepEqual(data, expect); }); }); - - suite("stripComments", () => { - var tests = [ - [ "abc", "abc" ], - [ "a /* comment */ c", "a c" ], - [ "a /* comment */ c /* comment */ d", "a c d" ], - [ "a /* comment\n */ c\n /* comment\n */ d", "a c\n d" ], - [ "a // comment", "a " ], - [ "a // comment \nb", "a \nb" ], - ]; - - helper.dataDrivenTest(tests, (data, expect) => { - let result = util.stripComments(data); - assert.equal(result, expect); - }); - }); }); diff --git a/src/util.ts b/src/util.ts index 105f859..30d4b62 100644 --- a/src/util.ts +++ b/src/util.ts @@ -257,11 +257,3 @@ function fillString(s: string, n: number): string { } return s; } - -// Remove all single and multi-line command from string. -// Note use of *? - "lazy" match (not greedy). -export function stripComments(s: string) : string { - return s - .replace(/\/\*[^]*?\*\//g, '') - .replace(/\/\/.*/g, ''); -} From 36e0ee0b22912711d1298368b7016aefa0c57b76 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Fri, 20 Nov 2015 16:33:40 -0800 Subject: [PATCH 05/35] Checkpoint - passing. --- bin/firebase-bolt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/firebase-bolt b/bin/firebase-bolt index 72b49ff..c5bb5d0 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -58,7 +58,7 @@ var commands = { jsonData = jsonData.replace(/"([^\\"]|\\[^])*"/g, function(s) { return s.replace(/\n/g, '\\n'); }); - console.log(jsonData); + // console.log(jsonData); writeOutputFile(outputFileName, bolt.decodeJSON(JSON.parse(jsonData))); }); } From d42c5bf1526068b7bf9143f29f75a345b0a05bea Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Fri, 20 Nov 2015 17:23:25 -0800 Subject: [PATCH 06/35] Tests - some failing - decoded samples. --- bin/firebase-bolt | 8 +------ src/bolt.ts | 2 +- src/rules-decoder.ts | 45 ++++++++++++++++++++++++++++++++-------- src/test/decoder-test.ts | 39 ++++++++++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/bin/firebase-bolt b/bin/firebase-bolt index c5bb5d0..fff55aa 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -23,7 +23,6 @@ var parseArgs = require('minimist'); var util = require('../lib/util'); var bolt = require('../lib/bolt'); var pkg = require('../package.json'); -var stripComments = require('strip-json-comments'); var VERSION_STRING = "Firebase Bolt v" + pkg.version; @@ -54,12 +53,7 @@ var commands = { var inputFileName = getInputFileName(args._[1], 'json'); var outputFileName = getOutputFileName(args.output, inputFileName, 'bolt'); readInputFile(inputFileName, 'JSON', function(jsonData) { - jsonData = stripComments(jsonData); - jsonData = jsonData.replace(/"([^\\"]|\\[^])*"/g, function(s) { - return s.replace(/\n/g, '\\n'); - }); - // console.log(jsonData); - writeOutputFile(outputFileName, bolt.decodeJSON(JSON.parse(jsonData))); + writeOutputFile(outputFileName, bolt.decodeRules(jsonData)); }); } }; diff --git a/src/bolt.ts b/src/bolt.ts index 57470f7..f148eb1 100644 --- a/src/bolt.ts +++ b/src/bolt.ts @@ -29,7 +29,7 @@ export var generate = util.maybePromise(generateSync); export var Generator = generator.Generator; export var ast = astReal; export var decodeExpression = ast.decodeExpression; -export var decodeJSON = decoder.decodeJSON; +export var decodeRules = decoder.decodeRules; export var rulesSuite = simulator.rulesSuite; // Usage: diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 68111e3..b6d4cd3 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -17,6 +17,7 @@ // import util = require('./util'); import ast = require('./ast'); var parser = require('./rules-parser'); +var stripComments = require('strip-json-comments'); export let PREAMBLE = "// Bolt file auto-generated from JSON file.\n"; @@ -26,6 +27,18 @@ let typeIndicators = { "newData.isBoolean()": "Boolean" }; +export function decodeRules(jsonString: string): string { + return decodeJSON(JSON.parse(cleanJSONString(jsonString))); +} + +export function cleanJSONString(jsonString: string): string { + jsonString = stripComments(jsonString); + jsonString = jsonString.replace(/"([^\\"]|\\[^])*"/g, function(s) { + return s.replace(/\n/g, '\\n'); + }); + return jsonString; +} + export function decodeJSON(json: Object): string { var formatter = new Formatter; formatter.decodeParts('/', json['rules']); @@ -60,12 +73,16 @@ class Formatter { } } - emit(path: string, method: string, expString: string) { - // Normalize expression - try { - expString = ast.decodeExpression(parse(expString)); - } catch (e) { - throw new Error("Could not parse expression: '" + expString + "'"); + emit(path: string, method: string, exp: string | Array) { + let expString = exp; + + if (method !== 'indexOn') { + // Normalize expression + try { + expString = ast.decodeExpression(parse(expString)); + } catch (e) { + throw new Error("Could not parse expression: '" + expString + "'"); + } } if (this.exps[path] === undefined) { @@ -74,10 +91,20 @@ class Formatter { let pc = this.exps[path]; - if (method === 'validate' && typeIndicators[expString]) { - pc.type = ast.typeType(typeIndicators[expString]); - } else { + switch (method) { + case 'indexOn': + console.log("IO"); + break; + case 'validate': + if (typeIndicators[expString]) { + pc.type = ast.typeType(typeIndicators[expString]); + } else { + pc.methods[method] = expString; + } + break; + default: pc.methods[method] = expString; + break; } } diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index ed5ec48..dd51b32 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -17,8 +17,10 @@ /// /// -import decoder = require('../rules-decoder'); +import bolt = require('../bolt'); +import fileio = require('../file-io'); import helper = require('./test-helper'); +import decoder = require('../rules-decoder'); import chai = require('chai'); chai.config.truncateThreshold = 1000; @@ -43,7 +45,40 @@ suite("JSON Rules Decoder", function() { helper.dataDrivenTest(tests, function(data, expect) { var result = decoder.decodeJSON(data); - assert.deepEqual(result, expect); + assert.equal(result, expect); + }); + }); + + suite("Decode Sample JSON", function() { + var files = ["all_access", + "userdoc", + "mail", + "type-extension", + "children", + "functional", + "user-security", + "generics", + "groups", + "multi-update", + "chat", + "serialized", + "map-scalar", + "regexp", + "decoded" + ]; + + helper.dataDrivenTest(files, function(filename) { + filename = 'samples/' + filename + '.json'; + return fileio.readFile(filename) + .then(function(response) { + let json = JSON.parse(decoder.cleanJSONString(response.content)); + let boltString = decoder.decodeJSON(json); + let generatedJSON = bolt.generate(boltString); + assert.deepEqual(generatedJSON, json); + }) + .catch(function(error) { + assert.ok(false, error.message); + }); }); }); }); From 6fe9d7373bab24c338c1a3488cb1389d50589862 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Fri, 20 Nov 2015 17:42:36 -0800 Subject: [PATCH 07/35] Update compare tool to check decoding. --- gulpfile.js | 10 ++++++++-- tools/compare-sample | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 8d5213f..e815146 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,8 +36,14 @@ var TS_SOURCES = ['src/*.ts', 'src/test/*.ts']; // Subset of tests required for 'gulp test'. -var TEST_FILES = ['lib/test/generator-test.js', 'lib/test/parser-test.js', - 'lib/test/ast-test.js', 'lib/test/util-test.js', 'lib/test/cli-test.js']; +var TEST_FILES = [ + 'lib/test/ast-test.js', + 'lib/test/cli-test.js', + 'lib/test/decoder-test.js', + 'lib/test/generator-test.js', + 'lib/test/parser-test.js', + 'lib/test/util-test.js' +]; // Ignore ts-compile errors while watching (but not in normal builds). var watching = false; diff --git a/tools/compare-sample b/tools/compare-sample index 02532e6..5836da3 100755 --- a/tools/compare-sample +++ b/tools/compare-sample @@ -4,4 +4,11 @@ set -e cd $PROJ_DIR/samples +echo "Compare $1.bolt => $1.json:" diff <(firebase-bolt < $1.bolt | flatten-json.py) <(flatten-json.py < $1.json) + +firebase-bolt migrate $1.json --output ../tmp/$1.bolt +firebase-bolt ../tmp/$1.bolt + +echo -e "\nCompare decoded $.json roundtrip:" +diff <(flatten-json.py < $1.json) <(flatten-json.py < ../tmp/$1.json) From b1b047ff20202311a59af4d2bd55e91b775eb8a3 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 23 Nov 2015 10:24:00 -0800 Subject: [PATCH 08/35] Support index() in decoder. --- src/rules-decoder.ts | 2 +- src/test/decoder-test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index b6d4cd3..49c274d 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -93,7 +93,7 @@ class Formatter { switch (method) { case 'indexOn': - console.log("IO"); + pc.methods['index'] = JSON.stringify(exp); break; case 'validate': if (typeIndicators[expString]) { diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index dd51b32..cd67989 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -41,6 +41,14 @@ suite("JSON Rules Decoder", function() { { data: { rules: { "a": { ".validate": "newData.isString()"}} }, expect: decoder.PREAMBLE + "path /a is String;", }, + + { data: { rules: { "a": { ".indexOn": "prop"}} }, + expect: decoder.PREAMBLE + "path /a {\n index() = \"prop\";\n}", + }, + + { data: { rules: { "a": { ".indexOn": ["prop1", "prop2"]}} }, + expect: decoder.PREAMBLE + "path /a {\n index() = [\"prop1\",\"prop2\"];\n}", + }, ]; helper.dataDrivenTest(tests, function(data, expect) { From 67a798903d81df3e6779829ee685a060f73cd7f4 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 23 Nov 2015 11:26:53 -0800 Subject: [PATCH 09/35] Allow decode to convert right-associative to left-associative boolean exp. --- samples/chat.json | 8 ++++---- samples/mail.json | 4 ++-- samples/user-security.json | 2 +- src/ast.ts | 13 +++++++++---- src/test/ast-test.ts | 4 ++++ src/test/parser-test.ts | 8 +++++++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/samples/chat.json b/samples/chat.json index 3832561..2492d17 100644 --- a/samples/chat.json +++ b/samples/chat.json @@ -7,7 +7,7 @@ ".validate": "newData.val().length > 0 && newData.val().length <= 32" }, "creator": { - ".validate": "newData.isString() && (auth != null && newData.val() == auth.uid)" + ".validate": "newData.isString() && auth != null && newData.val() == auth.uid" }, "members": { "$key2": { @@ -21,7 +21,7 @@ "$other": { ".validate": "false" }, - ".write": "data.val() == null && (auth != null && $key2 == auth.uid)" + ".write": "data.val() == null && auth != null && $key2 == auth.uid" } }, "$other": { @@ -37,7 +37,7 @@ "$postid": { ".validate": "newData.hasChildren(['from', 'message', 'created'])", "from": { - ".validate": "newData.isString() && (auth != null && newData.val() == auth.uid)" + ".validate": "newData.isString() && auth != null && newData.val() == auth.uid" }, "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 140" @@ -45,7 +45,7 @@ "$other": { ".validate": "false" }, - ".write": "data.val() == null && (root.child('rooms').child($roomid).child('members').child(auth.uid).val() != null && !(root.child('rooms').child($roomid).child('members').child(auth.uid).child('isBanned').val() == true))", + ".write": "data.val() == null && root.child('rooms').child($roomid).child('members').child(auth.uid).val() != null && !(root.child('rooms').child($roomid).child('members').child(auth.uid).child('isBanned').val() == true)", "created": { ".validate": "newData.isNumber() && newData.val() == (data.val() == null ? now : data.val())" } diff --git a/samples/mail.json b/samples/mail.json index d076a62..38f8428 100644 --- a/samples/mail.json +++ b/samples/mail.json @@ -4,7 +4,7 @@ "$userid": { "inbox": { "$msg": { - ".validate": "newData.hasChildren(['from', 'to', 'message']) && data.val() == null && (auth != null && auth.uid == newData.child('from').val())", + ".validate": "newData.hasChildren(['from', 'to', 'message']) && data.val() == null && auth != null && auth.uid == newData.child('from').val()", "from": { ".validate": "newData.isString()" }, @@ -23,7 +23,7 @@ }, "outbox": { "$msg": { - ".validate": "newData.hasChildren(['from', 'to', 'message']) && data.val() == null && (auth != null && auth.uid == newData.child('from').val())", + ".validate": "newData.hasChildren(['from', 'to', 'message']) && data.val() == null && auth != null && auth.uid == newData.child('from').val()", "from": { ".validate": "newData.isString()" }, diff --git a/samples/user-security.json b/samples/user-security.json index c054508..5d4ecd8 100644 --- a/samples/user-security.json +++ b/samples/user-security.json @@ -21,7 +21,7 @@ "$message_id": { ".validate": "newData.hasChildren(['user', 'message', 'timestamp']) && this == null", "user": { - ".validate": "newData.isString() && (auth != null && auth.uid == newData.val())" + ".validate": "newData.isString() && auth != null && auth.uid == newData.val()" }, "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" diff --git a/src/ast.ts b/src/ast.ts index 0ac2bbf..025758e 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -612,13 +612,18 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string { if (expOp.args.length === 1) { result = rep + decodeExpression(expOp.args[0], innerPrecedence); } else if (expOp.args.length === 2) { + // All ops are left associative - so nudge the innerPrecendence + // down on the right hand side to force () for right-associating + // operations (but ignore right-associating && and || since + // short-circuiting makes it moot). + let nudge = 1; + if (rep === '&&' || rep === '||') { + nudge = 0; + } result = decodeExpression(expOp.args[0], innerPrecedence) + ' ' + rep + ' ' + - // All ops are left associative - so nudge the innerPrecendence - // down on the right hand side to force () for right-associating - // operations. - decodeExpression(expOp.args[1], innerPrecedence + 1); + decodeExpression(expOp.args[1], innerPrecedence + nudge); } else if (expOp.args.length === 3) { result = decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' + diff --git a/src/test/ast-test.ts b/src/test/ast-test.ts index 891e654..4e0f896 100644 --- a/src/test/ast-test.ts +++ b/src/test/ast-test.ts @@ -206,7 +206,11 @@ suite("Abstract Syntax Tree (AST)", function() { [ "a == 1 || b <= 2" ], [ "a && b && c" ], [ "a || b || c" ], + // Converts right-associatvie to left-associative for && and || + [ "a && (b && c)", "a && b && c" ], + [ "a || (b || c)", "a || b || c" ], [ "a && b || c && d" ], + [ "(a || b) && (c || d)" ], [ "a ? b : c", ], [ "a || b ? c : d" ], ]; diff --git a/src/test/parser-test.ts b/src/test/parser-test.ts index fbb8356..f3f8e64 100644 --- a/src/test/parser-test.ts +++ b/src/test/parser-test.ts @@ -124,13 +124,19 @@ suite("Rules Parser Tests", function() { ast.lte(ast.variable('b'), ast.number(2))) ], [ "a == 1 || b <= 2", ast.or(ast.eq(ast.variable('a'), ast.number(1)), ast.lte(ast.variable('b'), ast.number(2))) ], - // Left associative (even though execution is short-circuited! + // Normal left associative && and || [ "a && b && c", ast.and(ast.and(ast.variable('a'), ast.variable('b')), ast.variable('c')) ], [ "a || b || c", ast.or(ast.or(ast.variable('a'), ast.variable('b')), ast.variable('c')) ], + [ "a && (b && c)", ast.and(ast.variable('a'), + ast.and(ast.variable('b'), + ast.variable('c'))) ], + [ "a || (b || c)", ast.or(ast.variable('a'), + ast.or(ast.variable('b'), + ast.variable('c'))) ], // && over || precendence [ "a && b || c && d", ast.or(ast.and(ast.variable('a'), ast.variable('b')), From fe16414670c4b7a8f22139fad5b99d77019b4620 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 24 Nov 2015 14:33:55 -0800 Subject: [PATCH 10/35] Failing tests for decoding. --- src/rules-decoder.ts | 4 +-- src/test/decoder-test.ts | 57 +++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 49c274d..9e20890 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -19,8 +19,6 @@ import ast = require('./ast'); var parser = require('./rules-parser'); var stripComments = require('strip-json-comments'); -export let PREAMBLE = "// Bolt file auto-generated from JSON file.\n"; - let typeIndicators = { "newData.isString()": "String", "newData.isNumber()": "Number", @@ -127,7 +125,7 @@ class Formatter { } lines.push("}"); }); - return PREAMBLE + lines.join('\n'); + return lines.join('\n'); } } diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index cd67989..67f5914 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -31,23 +31,23 @@ suite("JSON Rules Decoder", function() { suite("Basic Samples", function() { var tests = [ { data: { rules: {".read": "true", ".write": "true"} }, - expect: decoder.PREAMBLE + "path / {\n read() = true;\n write() = true;\n}", + expect: "path / {\n read() = true;\n write() = true;\n}", }, { data: { rules: { "a": { ".read": "true", ".write": "true"}} }, - expect: decoder.PREAMBLE + "path /a {\n read() = true;\n write() = true;\n}", + expect: "path /a {\n read() = true;\n write() = true;\n}", }, { data: { rules: { "a": { ".validate": "newData.isString()"}} }, - expect: decoder.PREAMBLE + "path /a is String;", + expect: "path /a is String;", }, { data: { rules: { "a": { ".indexOn": "prop"}} }, - expect: decoder.PREAMBLE + "path /a {\n index() = \"prop\";\n}", + expect: "path /a {\n index() = \"prop\";\n}", }, { data: { rules: { "a": { ".indexOn": ["prop1", "prop2"]}} }, - expect: decoder.PREAMBLE + "path /a {\n index() = [\"prop1\",\"prop2\"];\n}", + expect: "path /a {\n index() = [\"prop1\",\"prop2\"];\n}", }, ]; @@ -57,7 +57,52 @@ suite("JSON Rules Decoder", function() { }); }); - suite("Decode Sample JSON", function() { + suite("Data references", function() { + var tests = [ + { data: { rules: { "a": { ".read": "data.child('prop').val() > 0"}} }, + expect: "path /a {\n read() = this.prop > 0;", + }, + + { data: { rules: { "$a": { ".read": "data.child($a).val() > 0"}} }, + expect: "path /a {\n read() = this[$a] > 0;", + }, + + { data: { rules: { "a": { ".read": "data.exists()"}} }, + expect: "path /a {\n read() = this != null;", + }, + + { data: { rules: { "a": { ".validate": "newData.val() == data.val()"}} }, + expect: "path /a {\n validate() = this == prior(this);", + }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + var result = decoder.decodeJSON(data); + assert.equal(result, expect); + }); + }); + + suite("String methods", function() { + var tests = [ + [ "length > 0", "length > 0" ], + [ "contains('x')", "includes('x')" ], + [ "beginsWith('x')", "startsWith('x')" ], + [ "endsWith('x')", "endsWith('x')" ], + [ "replace('a', 'b')", "replace('a', 'b')" ], + [ "matches(/\d+/)", "test(/\d+/)" ], + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let rules = { "rules": { + "a": { ".read": "data.val()." + data} + }}; + let bolt = "path /a {\n read() = this." + expect + ";"; + var result = decoder.decodeJSON(rules); + assert.equal(result, bolt); + }); + }); + + suite("Samples decoder round-trip", function() { var files = ["all_access", "userdoc", "mail", From 391ae73e72febd06896c2182f535223918fe1554 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Wed, 25 Nov 2015 10:40:05 -0800 Subject: [PATCH 11/35] Additional string method precedence tests. --- src/test/generator-test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/generator-test.ts b/src/test/generator-test.ts index 81eb5d4..422ab57 100644 --- a/src/test/generator-test.ts +++ b/src/test/generator-test.ts @@ -168,6 +168,10 @@ suite("Rules Generator Tests", function() { expect: "newData.val().toUpperCase()" }, { data: "'ababa'.test(/bab/)", expect: "'ababa'.matches(/bab/)" }, + { data: "('ababa' + 'c').test(/bab/)", + expect: "('ababa' + 'c').matches(/bab/)" }, + { data: "(this + ' ').test(/\d+/)", + expect: "(newData.val() + ' ').matches(/\d+/)" }, ]; helper.dataDrivenTest(tests, function(data, expect) { @@ -319,6 +323,9 @@ suite("Rules Generator Tests", function() { expect: {'.validate': "newData.isString() && newData.parent().val() == 'new' && root.val() == 'old'", 'x': {'.read': "root.val() == 'old'"} } }, + + { data: "type T extends String { validate() = (this + ' ').test(/\d+/); }", + expect: {'.validate': "newData.isString() && (newData.val() + ' ').matches(/\d+/)"} }, ]; helper.dataDrivenTest(tests, function(data, expect) { From cc2edb73e34212bfd03c410d640ae50a5ac183fa Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Wed, 25 Nov 2015 14:54:05 -0800 Subject: [PATCH 12/35] Basic nest paths generated. --- src/rules-decoder.ts | 57 +++++++++++++++++++++++++++++++++++----- src/test/decoder-test.ts | 13 ++++++--- src/test/util-test.ts | 16 +++++++++++ src/util.ts | 28 +++++++++++++++++++- 4 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 9e20890..bb399e3 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -16,6 +16,7 @@ /// // import util = require('./util'); import ast = require('./ast'); +import util = require('./util'); var parser = require('./rules-parser'); var stripComments = require('strip-json-comments'); @@ -108,23 +109,49 @@ class Formatter { toString(): string { let lines = []; - Object.keys(this.exps).sort().forEach((path) => { + let paths = Object.keys(this.exps).sort(); + let openPaths: string[] = []; + let parts: string[]; + + function closeOpenPaths(path: string) { + while (openPaths.length > 0 && openPaths.slice(-1)[0].length > path.length) { + openPaths.pop(); + lines.push(indent(openPaths.length) + "}"); + } + } + + for (let i = 0; i < paths.length; i++) { + let path = paths[i]; let pc = this.exps[path]; - let line = "path " + path; + parts = pathParts(path); + let isParent = i < paths.length - 1 && util.isPrefix(parts, pathParts(paths[i + 1])); + + closeOpenPaths(path); + + let childPath: string; + if (openPaths.length === 0) { + childPath = path; + } else { + childPath = path.slice(openPaths.slice(-1)[0].length); + } + let line = indent(openPaths.length) + (openPaths.length === 0 ? 'path ' : '') + childPath; if (( pc.type).name !== 'Any') { line += " is " + ast.decodeExpression(pc.type); } - if (Object.keys(pc.methods).length === 0) { + if (Object.keys(pc.methods).length === 0 && !isParent) { lines.push(line + ";"); - return; + continue; } lines.push(line + " {"); + openPaths.push(path); for (let method in pc.methods) { - lines.push(" " + method + "() = " + pc.methods[method] + ";"); + lines.push(indent(openPaths.length) + method + "() = " + pc.methods[method] + ";"); } - lines.push("}"); - }); + } + + closeOpenPaths(''); + return lines.join('\n'); } } @@ -140,3 +167,19 @@ function childPath(path: string, child: string): string { } return path + '/' + child; } + +function pathParts(path: string): string[] { + if (path === undefined) { + return []; + } + // Remove initial slash. + path = path.slice(1); + if (path === '') { + return []; + } + return path.split('/'); +} + +function indent(n: number): string { + return util.repeatString(' ', 2 * n); +} diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index 67f5914..4fb5007 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -31,15 +31,15 @@ suite("JSON Rules Decoder", function() { suite("Basic Samples", function() { var tests = [ { data: { rules: {".read": "true", ".write": "true"} }, - expect: "path / {\n read() = true;\n write() = true;\n}", + expect: "path / {\n read() = true;\n write() = true;\n}" }, { data: { rules: { "a": { ".read": "true", ".write": "true"}} }, - expect: "path /a {\n read() = true;\n write() = true;\n}", + expect: "path /a {\n read() = true;\n write() = true;\n}" }, { data: { rules: { "a": { ".validate": "newData.isString()"}} }, - expect: "path /a is String;", + expect: "path /a is String;" }, { data: { rules: { "a": { ".indexOn": "prop"}} }, @@ -47,7 +47,12 @@ suite("JSON Rules Decoder", function() { }, { data: { rules: { "a": { ".indexOn": ["prop1", "prop2"]}} }, - expect: "path /a {\n index() = [\"prop1\",\"prop2\"];\n}", + expect: "path /a {\n index() = [\"prop1\",\"prop2\"];\n}" + }, + + { data: { rules: { "a": { ".read": "true", + "b": { ".write": "true" }}} }, + expect: "path /a {\n read() = true;\n /b {\n write() = true;\n }\n}" }, ]; diff --git a/src/test/util-test.ts b/src/test/util-test.ts index 3891db0..bf3c0a2 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -41,4 +41,20 @@ suite("Util", function() { assert.deepEqual(data, expect); }); }); + + suite("commonPrefix", () => { + var tests = [ + [ ["abc", "acd"], "a" ], + [ ["abc", "def"], "" ], + [ ["", "abc"], "" ], + [ [[1, 2, 3], [1, 3, 4]], [1] ], + [ [[1, 2, 3], [5, 3, 4]], [] ], + [ [[], [5, 3, 4]], [] ], + ]; + + helper.dataDrivenTest(tests, (data, expect) => { + let result = util.commonPrefix(data[0], data[1]); + assert.deepEqual(result, expect); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 30d4b62..bd36bd1 100644 --- a/src/util.ts +++ b/src/util.ts @@ -246,7 +246,7 @@ export function formatColumns(indent: number, lines: string[][]): string[] { return result; } -function repeatString(s: string, n: number): string { +export function repeatString(s: string, n: number): string { return new Array(n + 1).join(s); } @@ -257,3 +257,29 @@ function fillString(s: string, n: number): string { } return s; } + +// String or Array conform +// TODO: Yuck! Replace with polymorphic this when upgrade to TS 1.7. +export interface ArrayLike> { + length: number; + slice: (start: number, end: number) => T; +} + +export function commonPrefix>(s1: T, s2: T): T { + let last = commonPrefixSize(s1, s2); + return s1.slice(0, last); +} + +export function isPrefix>(prefix: T, other: T): boolean { + return commonPrefixSize(prefix, other) === prefix.length; +} + +export function commonPrefixSize>(s1: T, s2: T): number { + let last = Math.min(s1.length, s2.length); + for (let i = 0; i < last; i++) { + if (s1[i] !== s2[i]) { + return i; + } + } + return last; +} From 46916b355850889f6f9fe2b388a21c7275a27d9b Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Wed, 25 Nov 2015 17:31:01 -0800 Subject: [PATCH 13/35] Fix bug in sibling path. --- src/rules-decoder.ts | 20 +++++++++++--------- src/test/decoder-test.ts | 8 +++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index bb399e3..bb7fc63 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -111,29 +111,31 @@ class Formatter { let lines = []; let paths = Object.keys(this.exps).sort(); let openPaths: string[] = []; - let parts: string[]; function closeOpenPaths(path: string) { - while (openPaths.length > 0 && openPaths.slice(-1)[0].length > path.length) { + while (openPaths.length > 0 && + (path === '' || !util.isPrefix(pathParts(currentPath()), pathParts(path)))) { openPaths.pop(); lines.push(indent(openPaths.length) + "}"); } } + function currentPath(): string { + if (openPaths.length === 0) { + return ''; + } + return openPaths.slice(-1)[0]; + } + for (let i = 0; i < paths.length; i++) { let path = paths[i]; let pc = this.exps[path]; - parts = pathParts(path); - let isParent = i < paths.length - 1 && util.isPrefix(parts, pathParts(paths[i + 1])); + let isParent = i < paths.length - 1 && util.isPrefix(pathParts(path), pathParts(paths[i + 1])); closeOpenPaths(path); let childPath: string; - if (openPaths.length === 0) { - childPath = path; - } else { - childPath = path.slice(openPaths.slice(-1)[0].length); - } + childPath = currentPath() === '/' ? path : path.slice(currentPath().length); let line = indent(openPaths.length) + (openPaths.length === 0 ? 'path ' : '') + childPath; if (( pc.type).name !== 'Any') { diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index 4fb5007..60d4bb3 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -54,11 +54,17 @@ suite("JSON Rules Decoder", function() { "b": { ".write": "true" }}} }, expect: "path /a {\n read() = true;\n /b {\n write() = true;\n }\n}" }, + + { data: { rules: { "a": { ".read": "true", + "b": { ".write": "true" }, + "c": { ".write": "false" }}} }, + expect: "path /a {\n read() = true;\n /b {\n write() = true;\n }\n /c {\n write() = false;\n }\n}" + }, ]; helper.dataDrivenTest(tests, function(data, expect) { var result = decoder.decodeJSON(data); - assert.equal(result, expect); + assert.equal(result, expect, result); }); }); From 898523d07a7947996b4ec9a2c83d97454c606dee Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Wed, 25 Nov 2015 23:59:10 -0800 Subject: [PATCH 14/35] Initial AST matching. --- src/ast-matcher.ts | 133 +++++++++++++++++++++++++++++++++++++++ src/ast.ts | 65 ++++++++++++++++++- src/bolt.ts | 19 +++--- src/rules-parser.pegjs | 7 ++- src/simulator.ts | 5 +- src/test/ast-test.ts | 4 +- src/test/matcher-test.ts | 52 +++++++++++++++ src/test/parser-test.ts | 9 +-- src/test/test-helper.ts | 11 +++- 9 files changed, 282 insertions(+), 23 deletions(-) create mode 100644 src/ast-matcher.ts create mode 100644 src/test/matcher-test.ts diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts new file mode 100644 index 0000000..5b92324 --- /dev/null +++ b/src/ast-matcher.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +import ast = require('./ast'); + +/* + * Post-order iterator over AST nodes. + */ +export class Match { + path: {exp: ast.Exp, + index: number}[]; + index: number; + + constructor(public exp: ast.Exp) { + this.path = []; + this.index = 0; + this.advance(); + } + + advance(): ast.Exp { + while (ast.childCount(this.exp) > this.index) { + this.path.push({exp: this.exp, index: this.index}); + this.exp = ast.getChild(this.exp, this.index); + this.index = 0; + } + this.index = -1; + return this.exp; + } + + next(): ast.Exp { + // Already finished enumeration. + if (this.exp === null) { + throw new Error("Continued past end of AST enumeration."); + } + + // No more children in current - go to parent. + this.pop(); + if (this.exp === null) { + return null; + } + + // Parent has no more children to enumerate - return the parent + // and mark as visited (index == -1). + if (this.index >= ast.childCount(this.exp)) { + this.index = -1; + return this.exp; + } + + return this.advance(); + } + + pop() { + if (this.path.length === 0) { + this.exp = null; + return; + } + let node = this.path.pop(); + this.exp = node.exp; + this.index = node.index + 1; + } +} + +export function forEachExp(pattern: ast.Exp, + exp: ast.Exp, + params?: string[], + emit?: (match: Match) => void): Match { + let match = new Match(exp); + + while (match.exp !== null) { + if (equivalent(pattern, match.exp, params)) { + if (emit === undefined) { + return match; + } else { + emit(match); + } + } + match.next(); + } + return match; +} + +function equivalent(pattern: ast.Exp, exp: ast.Exp, params?: string[]): boolean { + if (pattern.type !== exp.type) { + return false; + } + + switch (pattern.type) { + case 'Null': + return true; + + case 'Boolean': + case 'Number': + case 'String': + case 'RegExp': + return ast.cmpValues( pattern, exp); + + case 'Array': + case 'call': + case 'op': + case 'ref': + case 'union': + case 'generic': + let patternCount = ast.childCount(pattern); + if (patternCount !== ast.childCount(exp)) { + return false; + } + for (let i = 0; i < patternCount; i++) { + if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), params)) { + return false; + } + } + return true; + + case 'var': + return ( pattern).name === ( exp).name; + + default: + return false; + } +} diff --git a/src/ast.ts b/src/ast.ts index 6dcdf39..70176d8 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -303,8 +303,14 @@ export function regexp(pattern: string, modifiers = ""): RegExpValue { }; } -function cmpValues(v1: ExpValue, v2: ExpValue): boolean { - return v1.type === v2.type && v1.value === v2.value; +export function cmpValues(v1: ExpValue, v2: ExpValue): boolean { + if (v1.type !== v2.type || v1.value !== v2.value) { + return false; + } + if (v1.type === 'RegExp' && ( v1).modifiers !== ( v2).modifiers) { + return false; + } + return true; } function isOp(opType: string, exp: Exp): boolean { @@ -684,3 +690,58 @@ function precedenceOf(exp: Exp): number { return result; } + +export function childCount(exp: Exp): number { + switch (exp.type) { + default: + return 0; + + case 'Array': + return ( exp).value.length; + + case 'ref': + return 2; + + case 'call': + return 1 + ( exp).args.length; + + case 'op': + return ( exp).args.length; + + case 'union': + return ( exp).types.length; + + case 'generic': + return ( exp).params.length; + } +} + +export function getChild(exp: Exp, index: number): Exp { + switch (exp.type) { + default: + return null; + + case 'Array': + return ( exp).value[index]; + + case 'ref': + let expRef = exp; + return [expRef.base, expRef.accessor][index]; + + case 'call': + let expCall = exp; + if (index === 0) { + return expCall.ref; + } + return expCall.args[index - 1]; + + case 'op': + return ( exp).args[index]; + + case 'union': + return ( exp).types[index]; + + case 'generic': + return ( exp).params[index]; + } +} diff --git a/src/bolt.ts b/src/bolt.ts index f148eb1..8907b64 100644 --- a/src/bolt.ts +++ b/src/bolt.ts @@ -20,24 +20,23 @@ import generator = require('./rules-generator'); import decoder = require('./rules-decoder'); import simulator = require('./simulator'); import astReal = require('./ast'); -import util = require('./util'); export var FILE_EXTENSION = 'bolt'; -export var parse = util.maybePromise(parser.parse); -export var generate = util.maybePromise(generateSync); +export var parse = parser.parse; export var Generator = generator.Generator; export var ast = astReal; export var decodeExpression = ast.decodeExpression; export var decodeRules = decoder.decodeRules; export var rulesSuite = simulator.rulesSuite; -// Usage: -// json = bolt.generate(bolt-text) -function generateSync(symbols: string | astReal.Symbols): generator.Validator { - if (typeof symbols === 'string') { - symbols = parser.parse(symbols); - } - var gen = new generator.Generator( symbols); +export function generate(boltText: string): generator.Validator { + let symbols = parser.parse(boltText); + var gen = new generator.Generator(symbols); return gen.generateRules(); } + +export function parseExpression(expression: string): astReal.Exp { + var result = parse('function f() {return ' + expression + ';}'); + return result.functions.f.body; +} diff --git a/src/rules-parser.pegjs b/src/rules-parser.pegjs index 12fc381..2e143d7 100644 --- a/src/rules-parser.pegjs +++ b/src/rules-parser.pegjs @@ -420,7 +420,12 @@ Literal Null = "null" { return ast.nullType() } -ArrayLiteral = "[" _ elements:ArgumentList? _ "]" { return ast.array(elements); } +ArrayLiteral = "[" _ elements:ArgumentList? _ "]" { + if (elements === null) { + elements = []; + } + return ast.array(elements); +} BooleanLiteral = "true" { return ast.boolean(true); } diff --git a/src/simulator.ts b/src/simulator.ts index 3e5c521..ad1bee8 100644 --- a/src/simulator.ts +++ b/src/simulator.ts @@ -70,8 +70,9 @@ util.methods(RulesSuite, { self.databaseReady = resolve; }); - var rulesJSON = bolt.generate(util.getProp(fileIO.readFile(rulesPath), - 'content')); + let rulesJSON = fileIO.readFile(rulesPath).then(function(result) { + return bolt.generate(result.content); + }); self.ready = Promise.all([rulesJSON, database]) .then(self.onRulesReady.bind(self)); diff --git a/src/test/ast-test.ts b/src/test/ast-test.ts index a3c797a..647a6e0 100644 --- a/src/test/ast-test.ts +++ b/src/test/ast-test.ts @@ -19,7 +19,6 @@ import helper = require('./test-helper'); import ast = require('../ast'); var bolt = (typeof(window) !== 'undefined' && window.bolt) || require('../bolt'); -var parse = bolt.parse; suite("Abstract Syntax Tree (AST)", function() { suite("Left Associative Operators (AND OR)", function() { @@ -219,8 +218,7 @@ suite("Abstract Syntax Tree (AST)", function() { helper.dataDrivenTest(tests, function(data, expect) { // Decode to self by default expect = expect || data; - var result = parse('function f() {return ' + data + ';}'); - var exp = result.functions.f.body; + var exp = bolt.parseExpression(data); var decode = bolt.decodeExpression(exp); assert.equal(decode, expect); }); diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts new file mode 100644 index 0000000..83d3ad6 --- /dev/null +++ b/src/test/matcher-test.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import chai = require('chai'); +var assert = chai.assert; +import helper = require('./test-helper'); + +import bolt = require('../bolt'); +import util = require('../util'); +import matcher = require('../ast-matcher'); + +suite("AST Matching", function() { + suite("Values", () => { + let tests = ["false", "1", "'a'", "a", "[]"]; + + helper.dataDrivenTest(tests, function(data, expect) { + let exp = bolt.parseExpression(data); + let match = matcher.forEachExp(bolt.parseExpression(data), exp); + assert.deepEqual(match.exp, exp); + }, helper.expFormat); + }); + + suite("Value expressions", () => { + let tests = [ + { data: { pattern: "false", exp: "true || false" }, + expect: "false"}, + { data: { pattern: "false", exp: "true || true" }, + expect: null }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let match = matcher.forEachExp(bolt.parseExpression(data.pattern), + bolt.parseExpression(data.exp)); + if (util.isType(expect, 'string')) { + expect = bolt.parseExpression(expect); + } + assert.deepEqual(match.exp, expect); + }, helper.expFormat); + }); +}); diff --git a/src/test/parser-test.ts b/src/test/parser-test.ts index 4b794bc..7e3eb2e 100644 --- a/src/test/parser-test.ts +++ b/src/test/parser-test.ts @@ -68,6 +68,7 @@ suite("Rules Parser Tests", function() { [ "+3", ast.number(3) ], [ "-3", ast.number(-3) ], [ "0x2", ast.number(2) ], + [ "[]", ast.array([]) ], [ "[1, 2, 3]", ast.array([ast.number(1), ast.number(2), ast.number(3)]) ], [ "\"string\"", ast.string("string") ], [ "'string'", ast.string("string") ], @@ -80,8 +81,8 @@ suite("Rules Parser Tests", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - var result = parse("function f() { return " + data + ";}"); - assert.deepEqual(result.functions.f.body, expect); + let exp = bolt.parseExpression(data); + assert.deepEqual(exp, expect); }); }); @@ -149,8 +150,8 @@ suite("Rules Parser Tests", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - var result = parse("function f() { return " + data + ";}"); - assert.deepEqual(result.functions.f.body, expect); + let exp = bolt.parseExpression(data); + assert.deepEqual(exp, expect); }); }); diff --git a/src/test/test-helper.ts b/src/test/test-helper.ts index 5b669a2..c59553e 100644 --- a/src/test/test-helper.ts +++ b/src/test/test-helper.ts @@ -51,7 +51,7 @@ export function dataDrivenTest(tests, testIt, formatter?) { data = util.extend({}, data); delete data.expect; } - expect = tests[i].expect || tests[i][1]; + expect = firstDefined(tests[i].expect, tests[i][1]); label = tests[i].label; if (label === undefined) { if (expect !== undefined) { @@ -97,3 +97,12 @@ export function expFormat(x) { } return JSON.stringify(x); } + +function firstDefined(...args: any[]) { + for (let i = 0; i < args.length; i++) { + if (args[i] !== undefined) { + return args[i]; + } + } + return undefined; +} From 6c8468e0f60c0cdba853e0def67f054ef5b101ff Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 26 Nov 2015 08:21:20 -0800 Subject: [PATCH 15/35] More matching tests. --- src/test/matcher-test.ts | 46 ++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 83d3ad6..083b4a0 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -18,12 +18,11 @@ var assert = chai.assert; import helper = require('./test-helper'); import bolt = require('../bolt'); -import util = require('../util'); import matcher = require('../ast-matcher'); suite("AST Matching", function() { - suite("Values", () => { - let tests = ["false", "1", "'a'", "a", "[]"]; + suite("Values to values", () => { + let tests = ["false", "1", "'a'", "a", "[]", "1.2", "null", "[1,2]"]; helper.dataDrivenTest(tests, function(data, expect) { let exp = bolt.parseExpression(data); @@ -32,21 +31,40 @@ suite("AST Matching", function() { }, helper.expFormat); }); - suite("Value expressions", () => { + suite("Values in expressions", () => { let tests = [ - { data: { pattern: "false", exp: "true || false" }, - expect: "false"}, - { data: { pattern: "false", exp: "true || true" }, - expect: null }, + { pattern: "false", exp: "true || false" }, + { pattern: "a", exp: "a + 1" }, + { pattern: "a", exp: "1 + a" }, + { pattern: "1", exp: "2 + 3 + 1 + 5" }, + { pattern: "'a'", exp: "2 + 3 + 'a' + 5" }, + { pattern: "3", exp: "2 * (4 + 3)" }, ]; helper.dataDrivenTest(tests, function(data, expect) { - let match = matcher.forEachExp(bolt.parseExpression(data.pattern), - bolt.parseExpression(data.exp)); - if (util.isType(expect, 'string')) { - expect = bolt.parseExpression(expect); - } - assert.deepEqual(match.exp, expect); + let pattern = bolt.parseExpression(data.pattern); + let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + assert.deepEqual(match.exp, pattern); + }, helper.expFormat); + }); + + suite("Sub-expressions in expressions", () => { + let tests = [ + { pattern: "a + 1", exp: "a + 1" }, + { pattern: "a || b", exp: "a || b" }, + /* + { pattern: "a + 1", exp: "a + 1 + 2" }, + { pattern: "a || b", exp: "a || b || c" }, + { pattern: "b || c", exp: "a || b || c" }, + { pattern: "a || c", exp: "a || b || c" }, + */ + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let pattern = bolt.parseExpression(data.pattern); + let exp = bolt.parseExpression(data.exp); + let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + assert.deepEqual(match.exp, exp); }, helper.expFormat); }); }); From eac5c5e201dbf2ca5bbc1982df220c6f18d14bc9 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 26 Nov 2015 09:35:56 -0800 Subject: [PATCH 16/35] And and or expression flattened to one level instead of binary. --- src/ast.ts | 14 ++++++++------ src/rules-parser.pegjs | 24 ++++++++++++++++-------- src/test/parser-test.ts | 24 ++++++++++++------------ 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 70176d8..3bf69bb 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -361,7 +361,7 @@ function leftAssociateGen(opType: string, identityValue: ExpValue, zeroValue: Ex var result = []; for (i = 0; i < flat.length; i++) { - // Remove identifyValues from array. + // Remove identityValues from array. if (cmpValues(flat[i], identityValue)) { continue; } @@ -617,6 +617,11 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string { var rep = JS_OPS[expOp.op].rep === undefined ? expOp.op : JS_OPS[expOp.op].rep; if (expOp.args.length === 1) { result = rep + decodeExpression(expOp.args[0], innerPrecedence); + } else if (expOp.op === '?:') { + result = + decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' + + decodeExpression(expOp.args[1], innerPrecedence) + ' : ' + + decodeExpression(expOp.args[2], innerPrecedence); } else if (expOp.args.length === 2) { // All ops are left associative - so nudge the innerPrecendence // down on the right hand side to force () for right-associating @@ -630,11 +635,8 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string { decodeExpression(expOp.args[0], innerPrecedence) + ' ' + rep + ' ' + decodeExpression(expOp.args[1], innerPrecedence + nudge); - } else if (expOp.args.length === 3) { - result = - decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' + - decodeExpression(expOp.args[1], innerPrecedence) + ' : ' + - decodeExpression(expOp.args[2], innerPrecedence); + } else { + result = expOp.args.map(decodeExpression).join(' ' + rep + ' '); } break; diff --git a/src/rules-parser.pegjs b/src/rules-parser.pegjs index 2e143d7..d908596 100644 --- a/src/rules-parser.pegjs +++ b/src/rules-parser.pegjs @@ -380,19 +380,27 @@ EqualityExpression EqualityOperator = ("===" / "==") { return "=="; } / ("!==" / "!=") { return "!="; } -LogicalANDExpression - = head:EqualityExpression - tail:(_ op:LogicalANDOperator _ exp:EqualityExpression { return {op: op, exp: exp}; })* { - return leftAssociative(head, tail); +LogicalANDExpression = + head:EqualityExpression + tail:(_ op:LogicalANDOperator _ exp:EqualityExpression { return exp; })* { + if (tail.length === 0) { + return head; } + tail.unshift(head); + return ast.op('&&', ast.flatten('&&', ast.op("&&", tail))); + } LogicalANDOperator = ("&&" / "and") { return "&&"; } -LogicalORExpression - = head:LogicalANDExpression - tail:(_ op:LogicalOROperator _ exp:LogicalANDExpression { return {op: op, exp: exp}; })* { - return leftAssociative(head, tail); +LogicalORExpression = + head:LogicalANDExpression + tail:(_ op:LogicalOROperator _ exp:LogicalANDExpression { return exp; })* { + if (tail.length === 0) { + return head; } + tail.unshift(head); + return ast.op('||', ast.flatten('||', ast.op("||", tail))); + } LogicalOROperator = ("||" / "or") { return "||"; } diff --git a/src/test/parser-test.ts b/src/test/parser-test.ts index 7e3eb2e..2c66cb6 100644 --- a/src/test/parser-test.ts +++ b/src/test/parser-test.ts @@ -126,18 +126,18 @@ suite("Rules Parser Tests", function() { [ "a == 1 || b <= 2", ast.or(ast.eq(ast.variable('a'), ast.number(1)), ast.lte(ast.variable('b'), ast.number(2))) ], // Normal left associative && and || - [ "a && b && c", ast.and(ast.and(ast.variable('a'), - ast.variable('b')), - ast.variable('c')) ], - [ "a || b || c", ast.or(ast.or(ast.variable('a'), - ast.variable('b')), - ast.variable('c')) ], - [ "a && (b && c)", ast.and(ast.variable('a'), - ast.and(ast.variable('b'), - ast.variable('c'))) ], - [ "a || (b || c)", ast.or(ast.variable('a'), - ast.or(ast.variable('b'), - ast.variable('c'))) ], + [ "a && b && c", ast.op('&&', [ast.variable('a'), + ast.variable('b'), + ast.variable('c')]) ], + [ "a || b || c", ast.op('||', [ast.variable('a'), + ast.variable('b'), + ast.variable('c')]) ], + [ "a && (b && c)", ast.op('&&', [ast.variable('a'), + ast.variable('b'), + ast.variable('c')]) ], + [ "a || (b || c)", ast.op('||', [ast.variable('a'), + ast.variable('b'), + ast.variable('c')]) ], // && over || precendence [ "a && b || c && d", ast.or(ast.and(ast.variable('a'), ast.variable('b')), From f80c7ee307782d93b644a87bd156f93c292aa556 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Thu, 26 Nov 2015 20:31:19 -0800 Subject: [PATCH 17/35] Simple equivalence. --- src/ast-matcher.ts | 61 ++++++++++++++++++++++++++++++++++++---- src/test/matcher-test.ts | 57 +++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 5b92324..3886ce1 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -15,6 +15,7 @@ */ /// import ast = require('./ast'); +import util = require('./util'); /* * Post-order iterator over AST nodes. @@ -92,11 +93,29 @@ export function forEachExp(pattern: ast.Exp, return match; } -function equivalent(pattern: ast.Exp, exp: ast.Exp, params?: string[]): boolean { +function equivalent(pattern: ast.Exp, exp: ast.Exp, params = []): boolean { + if (pattern.type === 'var' && + util.arrayIncludes(params, ( pattern).name)) { + return true; + } + if (pattern.type !== exp.type) { return false; } + function equivalentChildren(): boolean { + let patternCount = ast.childCount(pattern); + if (patternCount !== ast.childCount(exp)) { + return false; + } + for (let i = 0; i < patternCount; i++) { + if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), params)) { + return false; + } + } + return true; + } + switch (pattern.type) { case 'Null': return true; @@ -109,21 +128,51 @@ function equivalent(pattern: ast.Exp, exp: ast.Exp, params?: string[]): boolean case 'Array': case 'call': - case 'op': case 'ref': case 'union': + return equivalentChildren(); + case 'generic': - let patternCount = ast.childCount(pattern); - if (patternCount !== ast.childCount(exp)) { + if (( pattern).name !== ( exp).name) { return false; } - for (let i = 0; i < patternCount; i++) { - if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), params)) { + // NYI + return false; + + case 'op': + let patternOp = pattern; + let expOp = exp; + if (patternOp.op !== expOp.op) { + return false; + } + + // Any non-boolean operator requires arguments be in same order. + // Note that '+' is also not commutative when use for string args! + if (!(patternOp.op === '||' || patternOp.op === '&&')) { + return equivalentChildren(); + } + + // Find any (unique) occurance for all children of the pattern. + let matches: number[] = []; + while (matches.length < patternOp.args.length) { + let j: number; + for (j = 0; j < expOp.args.length; j++) { + if (util.arrayIncludes(matches, j)) { + continue; + } + if (equivalent(patternOp.args[matches.length], expOp.args[j], params)) { + matches.push(j); + break; + } + } + if (j >= expOp.args.length) { return false; } } + return true; + case 'literal': case 'var': return ( pattern).name === ( exp).name; diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 083b4a0..4a6f8ea 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -18,6 +18,7 @@ var assert = chai.assert; import helper = require('./test-helper'); import bolt = require('../bolt'); +import ast = require('../ast'); import matcher = require('../ast-matcher'); suite("AST Matching", function() { @@ -52,19 +53,65 @@ suite("AST Matching", function() { let tests = [ { pattern: "a + 1", exp: "a + 1" }, { pattern: "a || b", exp: "a || b" }, - /* - { pattern: "a + 1", exp: "a + 1 + 2" }, + { pattern: "a || b", exp: "b || a" }, { pattern: "a || b", exp: "a || b || c" }, { pattern: "b || c", exp: "a || b || c" }, { pattern: "a || c", exp: "a || b || c" }, - */ + { pattern: "a || c", exp: "c || b || a" }, + { pattern: "a || c", exp: "b && (a || b || c)" }, + { pattern: "a.test(/x/)", exp: "a + a.test(/x/)" }, ]; helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let exp = bolt.parseExpression(data.exp); let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); - assert.deepEqual(match.exp, exp); + assert.equal(( match.exp).op, ( match.exp).op); + }, helper.expFormat); + }); + + suite("Sub-expressions not in expressions", () => { + let tests = [ + { pattern: "a + 1", exp: "1 + a" }, + { pattern: "a || c", exp: "c || b || b" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let pattern = bolt.parseExpression(data.pattern); + let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + assert.equal(match.exp, null); + }, helper.expFormat); + }); + + suite("Sub-expressions with params", () => { + let tests = [ + { vars: ['a'], pattern: "a + 1", exp: "a + 1" }, + { vars: ['a'], pattern: "a + 1", exp: "x + 1" }, + { vars: ['x'], pattern: "x || true", exp: "a || b || true || c" }, + { vars: ['x'], pattern: "x || x", exp: "a || b || a" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let pattern = bolt.parseExpression(data.pattern); + let match = matcher.forEachExp(pattern, + bolt.parseExpression(data.exp), + data.vars); + assert.ok(match.exp !== null && ( match.exp).op === ( match.exp).op); + }, helper.expFormat); + }); + + suite("Sub-expressions with params not present", () => { + let tests = [ + { vars: ['a'], pattern: "a + 1", exp: "a + 2" }, + { vars: ['a'], pattern: "a + 1", exp: "x + 2" }, + { vars: ['x'], pattern: "x || x", exp: "a || b || c" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let pattern = bolt.parseExpression(data.pattern); + let match = matcher.forEachExp(pattern, + bolt.parseExpression(data.exp), + data.vars); + assert.ok(match.exp == null); }, helper.expFormat); }); }); From cfcb359d717b1a050ea845639c2ab8b73a895c57 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 29 Nov 2015 18:00:50 -0800 Subject: [PATCH 18/35] Commutative operators matching. --- gulpfile.js | 1 + src/ast-matcher.ts | 62 +++++++++++++++++++++++++++------------- src/test/matcher-test.ts | 7 +++++ 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index e815146..b8f3c5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -41,6 +41,7 @@ var TEST_FILES = [ 'lib/test/cli-test.js', 'lib/test/decoder-test.js', 'lib/test/generator-test.js', + 'lib/test/matcher-test.js', 'lib/test/parser-test.js', 'lib/test/util-test.js' ]; diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 3886ce1..6c076f2 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -17,6 +17,12 @@ import ast = require('./ast'); import util = require('./util'); +let reverseOp = { + '<': '>', + '>': '<', + '<=': '>=', +}; + /* * Post-order iterator over AST nodes. */ @@ -141,35 +147,51 @@ function equivalent(pattern: ast.Exp, exp: ast.Exp, params = []): bool case 'op': let patternOp = pattern; + let op = patternOp.op; let expOp = exp; - if (patternOp.op !== expOp.op) { - return false; + if (op !== expOp.op) { + if (reverseOp[op] === expOp.op) { + op = expOp.op; + exp = ast.copyExp(expOp); + ( exp).args = [expOp.args[1], expOp.args[0]]; + } else { + return false; + } } - // Any non-boolean operator requires arguments be in same order. - // Note that '+' is also not commutative when use for string args! - if (!(patternOp.op === '||' || patternOp.op === '&&')) { + switch (patternOp.op) { + default: return equivalentChildren(); - } - // Find any (unique) occurance for all children of the pattern. - let matches: number[] = []; - while (matches.length < patternOp.args.length) { - let j: number; - for (j = 0; j < expOp.args.length; j++) { - if (util.arrayIncludes(matches, j)) { - continue; + case '==': + case '!=': + if (equivalentChildren()) { + return true; + } + exp = ast.copyExp(expOp); + ( exp).args = [expOp.args[1], expOp.args[0]]; + return equivalentChildren(); + + case '||': + case '&&': + // Find any (unique) occurance for all children of the pattern. + let matches: number[] = []; + while (matches.length < patternOp.args.length) { + let j: number; + for (j = 0; j < expOp.args.length; j++) { + if (util.arrayIncludes(matches, j)) { + continue; + } + if (equivalent(patternOp.args[matches.length], expOp.args[j], params)) { + matches.push(j); + break; + } } - if (equivalent(patternOp.args[matches.length], expOp.args[j], params)) { - matches.push(j); - break; + if (j >= expOp.args.length) { + return false; } } - if (j >= expOp.args.length) { - return false; - } } - return true; case 'literal': diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 4a6f8ea..7440602 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -60,6 +60,13 @@ suite("AST Matching", function() { { pattern: "a || c", exp: "c || b || a" }, { pattern: "a || c", exp: "b && (a || b || c)" }, { pattern: "a.test(/x/)", exp: "a + a.test(/x/)" }, + { pattern: "a < b", exp: "a < b" }, + { pattern: "a < b", exp: "b > a" }, + { pattern: "a <= b", exp: "b >= a" }, + { pattern: "a == b", exp: "a == b" }, + { pattern: "a == b", exp: "b == a" }, + { pattern: "a != b", exp: "a != b" }, + { pattern: "a != b", exp: "b != a" }, ]; helper.dataDrivenTest(tests, function(data, expect) { From 7b16e57f38dda0c88f67cfc9cf7e65cbf6153d39 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 30 Nov 2015 14:32:33 -0800 Subject: [PATCH 19/35] Parametric expression matching. --- src/ast-matcher.ts | 43 +++++++++++++++++++++++++--------------- src/ast.ts | 6 +++++- src/test/matcher-test.ts | 20 +++++++++---------- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 6c076f2..c3ee2cd 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -30,10 +30,12 @@ export class Match { path: {exp: ast.Exp, index: number}[]; index: number; + params: ast.ExpParams; constructor(public exp: ast.Exp) { this.path = []; this.index = 0; + this.params = {}; this.advance(); } @@ -80,29 +82,38 @@ export class Match { } } -export function forEachExp(pattern: ast.Exp, - exp: ast.Exp, - params?: string[], - emit?: (match: Match) => void): Match { +export function findExp(pattern: ast.Exp, + exp: ast.Exp, + paramNames?: string[]) { let match = new Match(exp); while (match.exp !== null) { - if (equivalent(pattern, match.exp, params)) { - if (emit === undefined) { - return match; - } else { - emit(match); - } + let params: ast.ExpParams = {}; + if (equivalent(pattern, match.exp, paramNames, params)) { + match.params = params; + return match; } match.next(); } return match; } -function equivalent(pattern: ast.Exp, exp: ast.Exp, params = []): boolean { - if (pattern.type === 'var' && - util.arrayIncludes(params, ( pattern).name)) { - return true; +function equivalent(pattern: ast.Exp, + exp: ast.Exp, + paramNames?: string[], + params: ast.ExpParams = {} + ): boolean { + if (paramNames !== undefined && pattern.type === 'var') { + let name = ( pattern).name; + if (util.arrayIncludes(paramNames, name)) { + if (params[name] === undefined) { + console.log(name + " = " + ast.decodeExpression(exp)); + params[name] = ast.copyExp(exp); + return true; + } else { + return equivalent(params[name], exp, paramNames, params); + } + } } if (pattern.type !== exp.type) { @@ -115,7 +126,7 @@ function equivalent(pattern: ast.Exp, exp: ast.Exp, params = []): bool return false; } for (let i = 0; i < patternCount; i++) { - if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), params)) { + if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), paramNames, params)) { return false; } } @@ -182,7 +193,7 @@ function equivalent(pattern: ast.Exp, exp: ast.Exp, params = []): bool if (util.arrayIncludes(matches, j)) { continue; } - if (equivalent(patternOp.args[matches.length], expOp.args[j], params)) { + if (equivalent(patternOp.args[matches.length], expOp.args[j], paramNames, params)) { matches.push(j); break; } diff --git a/src/ast.ts b/src/ast.ts index 3bf69bb..640b8f6 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -61,7 +61,11 @@ export interface ExpCall extends Exp { args: Exp[]; } -export type BuiltinFunction = (args: Exp[], params: { [name: string]: Exp; }) => Exp; +export interface ExpParams { + [name: string]: Exp; +} + +export type BuiltinFunction = (args: Exp[], params: ExpParams) => Exp; export interface ExpBuiltin extends Exp { fn: BuiltinFunction; diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 7440602..2cb9775 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -27,7 +27,7 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let exp = bolt.parseExpression(data); - let match = matcher.forEachExp(bolt.parseExpression(data), exp); + let match = matcher.findExp(bolt.parseExpression(data), exp); assert.deepEqual(match.exp, exp); }, helper.expFormat); }); @@ -44,7 +44,7 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.deepEqual(match.exp, pattern); }, helper.expFormat); }); @@ -71,7 +71,7 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.equal(( match.exp).op, ( match.exp).op); }, helper.expFormat); }); @@ -84,7 +84,7 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let match = matcher.forEachExp(pattern, bolt.parseExpression(data.exp)); + let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.equal(match.exp, null); }, helper.expFormat); }); @@ -99,9 +99,9 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let match = matcher.forEachExp(pattern, - bolt.parseExpression(data.exp), - data.vars); + let match = matcher.findExp(pattern, + bolt.parseExpression(data.exp), + data.vars); assert.ok(match.exp !== null && ( match.exp).op === ( match.exp).op); }, helper.expFormat); }); @@ -115,9 +115,9 @@ suite("AST Matching", function() { helper.dataDrivenTest(tests, function(data, expect) { let pattern = bolt.parseExpression(data.pattern); - let match = matcher.forEachExp(pattern, - bolt.parseExpression(data.exp), - data.vars); + let match = matcher.findExp(pattern, + bolt.parseExpression(data.exp), + data.vars); assert.ok(match.exp == null); }, helper.expFormat); }); From 84194e0b370713f71b28906a93e813e9138d2ca7 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 1 Dec 2015 07:14:16 -0800 Subject: [PATCH 20/35] Permutation helper. --- src/permutation.ts | 89 ++++++++++++++++++++++++++++++++++++ src/test/permutation-test.ts | 61 ++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/permutation.ts create mode 100644 src/test/permutation-test.ts diff --git a/src/permutation.ts b/src/permutation.ts new file mode 100644 index 0000000..515a7d1 --- /dev/null +++ b/src/permutation.ts @@ -0,0 +1,89 @@ +/* + * AST builders for Firebase Rules Language. + * + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +export class Permutation { + current: number[] = []; + locations: number[] = []; + remaining: number; + + constructor(private n: number, private k?: number) { + if (k === undefined) { + this.k = n; + } + this.remaining = 1; + for (let i = 0; i < this.k; i++) { + this.set(i, i); + this.remaining *= n - i; + } + this.remaining -= 1; + } + + getCurrent(): number[] { + if (this.current === null) { + return null; + } + return this.current.slice(); + } + + next(): number[] { + if (this.remaining === 0) { + this.current = null; + } + if (this.current === null) { + return null; + } + this.advance(); + return this.current; + } + + private advance() { + let location = this.k - 1; + for (; location >= 0; location--) { + let value = this.nextValue(location, this.current[location] + 1); + this.set(location, value); + if (value !== undefined) { + break; + } + } + for (location += 1; location < this.k; location++) { + this.set(location, this.nextValue(location, 0)); + } + this.remaining -= 1; + } + + private set(location: number, value?: number) { + let oldValue = this.current[location]; + if (oldValue !== undefined) { + this.locations[oldValue] = undefined; + } + this.current[location] = value; + if (value !== undefined) { + this.locations[value] = location; + } + } + + private nextValue(location: number, minValue: number): number { + for (let value = minValue; value < this.n; value++) { + if (this.locations[value] === undefined || this.locations[value] > location) { + return value; + } + } + return undefined; + } +} diff --git a/src/test/permutation-test.ts b/src/test/permutation-test.ts new file mode 100644 index 0000000..f213873 --- /dev/null +++ b/src/test/permutation-test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import chai = require('chai'); +var assert = chai.assert; +import helper = require('./test-helper'); + +import {Permutation} from '../permutation'; + +suite("Permutation", () => { + let tests = [ + { data: [1], + expect: { values: [[0]], count: 1 } }, + { data: [1, 1], + expect: { values: [[0]], count: 1 } }, + { data: [2], + expect: { values: [[0, 1], [1, 0]], count: 2 } }, + { data: [3], + expect: { values: [[0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0]], + count: 6 } }, + { data: [3, 1], + expect: { values: [[0], [1], [2]], + count: 3 } }, + { data: [3, 2], + expect: { values: [[0, 1], + [0, 2], + [1, 0], + [1, 2], + [2, 0], + [2, 1]], + count: 6 } }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let p = new Permutation(data[0], data[1]); + assert.equal(p.remaining + 1, expect.count); + let results = []; + while (p.getCurrent() !== null) { + results.push(p.getCurrent()); + p.next(); + } + assert.deepEqual(results, expect.values); + }); +}); From 8f00ddbc35d17507b7f3b829376bcd0b756bb701 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 1 Dec 2015 07:15:22 -0800 Subject: [PATCH 21/35] Checkpoint - re-write rules. --- gulpfile.js | 1 + src/ast-matcher.ts | 82 ++++++++++++++++++++++++++++++++- src/ast.ts | 68 ++++++++++++++++++++++++++++ src/rules-generator.ts | 2 +- src/test/matcher-test.ts | 98 +++++++++++++++++++++++++++++++++++++--- src/test/parser-test.ts | 3 ++ 6 files changed, 245 insertions(+), 9 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b8f3c5c..e26f8aa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -43,6 +43,7 @@ var TEST_FILES = [ 'lib/test/generator-test.js', 'lib/test/matcher-test.js', 'lib/test/parser-test.js', + 'lib/test/permutation-test.js', 'lib/test/util-test.js' ]; diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index c3ee2cd..50bd0b3 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -15,6 +15,7 @@ */ /// import ast = require('./ast'); +import bolt = require('./bolt'); import util = require('./util'); let reverseOp = { @@ -80,16 +81,93 @@ export class Match { this.exp = node.exp; this.index = node.index + 1; } + + replaceExp(replacement: ast.Exp) { + if (this.path.length === 0) { + return replacement; + } + let parentPart = this.path.slice(-1)[0]; + ast.setChild(replacement, parentPart.exp, parentPart.index); + return this.path[0].exp; + } + +} + +// "[()] => " +// E.g. "(a) a.val() => a" +let descriptorRegexp = /^\s*(?:\((.*)\))?\s*(.*\S)\s*=>\s*(.*\S)\s*$/; + +export class Rewriter { + constructor(public paramNames: string[], + public pattern: ast.Exp, + public replacement: ast.Exp) { + } + + static fromDescriptor(descriptor: string): Rewriter { + let match = descriptorRegexp.exec(descriptor); + if (match === null) { + return null; + } + let paramNames: string[]; + if (match[1] === undefined) { + paramNames = []; + } else { + paramNames = match[1].split(/,\s+/); + if (paramNames.length === 1 && paramNames[0] === '') { + paramNames = []; + } + } + return new Rewriter(paramNames, + bolt.parseExpression(match[2]), + bolt.parseExpression(match[3])); + } + + apply(exp: ast.Exp): ast.Exp { + let match: Match; + let limit = 50; + + while (match = findExp(this.pattern, exp, this.paramNames)) { + if (match.exp === null) { + break; + } + console.log("A", ast.decodeExpression(match.exp)); + if (limit-- <= 0) { + throw new Error("Too many patterns (" + ast.decodeExpression(this.pattern) + + ") in expression: " + ast.decodeExpression(exp)); + } + let replacement = replaceVars(this.replacement, match.params); + exp = match.replaceExp(replacement); + } + return exp; + } +} + +export function replaceVars(exp: ast.Exp, params: ast.ExpParams): ast.Exp { + exp = ast.deepCopy(exp); + let match = new Match(exp); + + while (match.exp !== null) { + if (match.exp.type === 'var') { + let expVar = match.exp; + if (params[expVar.name] !== undefined) { + exp = match.replaceExp(ast.deepCopy(params[expVar.name])); + } + } + match.next(); + } + return exp; } export function findExp(pattern: ast.Exp, exp: ast.Exp, - paramNames?: string[]) { + paramNames?: string[]): Match { let match = new Match(exp); while (match.exp !== null) { let params: ast.ExpParams = {}; + console.log("FE", ast.decodeExpression(match.exp)); if (equivalent(pattern, match.exp, paramNames, params)) { + console.log("FE2", ast.decodeExpression(match.exp)); match.params = params; return match; } @@ -107,7 +185,7 @@ function equivalent(pattern: ast.Exp, let name = ( pattern).name; if (util.arrayIncludes(paramNames, name)) { if (params[name] === undefined) { - console.log(name + " = " + ast.decodeExpression(exp)); + console.log("EQ", name + " = " + ast.decodeExpression(exp)); params[name] = ast.copyExp(exp); return true; } else { diff --git a/src/ast.ts b/src/ast.ts index 640b8f6..4de18f4 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -434,6 +434,10 @@ export function genericType(typeName: string, params: ExpType[]): ExpGenericType return { type: "generic", valueType: "type", name: typeName, params: params }; } +function isExpType(typeName): boolean { + return (typeName === 'type' || typeName === 'union' || typeName === 'generic'); +} + export class Symbols { functions: { [name: string]: Method }; paths: { [name: string]: Path }; @@ -751,3 +755,67 @@ export function getChild(exp: Exp, index: number): Exp { return ( exp).params[index]; } } + +// Mutate the parent to set the ith child element. +export function setChild(expChild: Exp, expParent: Exp, index: number) { + switch (expParent.type) { + default: + throw new Error("AST node has no children."); + + case 'Array': + ( expParent).value[index] = expChild; + break; + + case 'ref': + if (index > 1) { + throw new Error("Reference node has only two children."); + } + let expRef = expParent; + if (index === 0) { + expRef.base = expChild; + } else { + expRef.accessor = expChild; + } + break; + + case 'call': + let expCall = expParent; + if (index === 0) { + if (expChild.type !== 'var' && expChild.type !== 'ref') { + throw new Error(errors.typeMismatch + "expected Variable or Reference (not " + + expChild.type + ")"); + } + expCall.ref = expChild; + } else { + expCall.args[index - 1] = expChild; + } + break; + + case 'op': + ( expParent).args[index] = expChild; + break; + + case 'union': + if (!isExpType(expChild)) { + throw new Error(errors.typeMismatch + "expected Type (not " + expChild.type + ")"); + } + ( expParent).types[index] = expChild; + break; + + case 'generic': + if (!isExpType(expChild)) { + throw new Error(errors.typeMismatch + "expected Type (not " + expChild.type + ")"); + } + ( expParent).params[index] = expChild; + break; + } +} + +export function deepCopy(exp: Exp): Exp { + exp = copyExp(exp); + let c = childCount(exp); + for (let i = 0; i < c; i++) { + setChild(deepCopy(getChild(exp, i)), exp, i); + } + return exp; +} diff --git a/src/rules-generator.ts b/src/rules-generator.ts index b4fe7e7..61e8f03 100644 --- a/src/rules-generator.ts +++ b/src/rules-generator.ts @@ -602,7 +602,7 @@ export class Generator { // - Expand snapshot references using child('ref'). // - Coerce snapshot references to values as needed. partialEvalReal(exp: ast.Exp, - params: { [name: string]: ast.Exp } = {}, + params: ast.ExpParams = {}, functionCalls: { [name: string]: boolean } = {}) : ast.Exp { var self = this; diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 2cb9775..03e1adb 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -29,7 +29,7 @@ suite("AST Matching", function() { let exp = bolt.parseExpression(data); let match = matcher.findExp(bolt.parseExpression(data), exp); assert.deepEqual(match.exp, exp); - }, helper.expFormat); + }); }); suite("Values in expressions", () => { @@ -46,7 +46,7 @@ suite("AST Matching", function() { let pattern = bolt.parseExpression(data.pattern); let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.deepEqual(match.exp, pattern); - }, helper.expFormat); + }); }); suite("Sub-expressions in expressions", () => { @@ -73,7 +73,7 @@ suite("AST Matching", function() { let pattern = bolt.parseExpression(data.pattern); let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.equal(( match.exp).op, ( match.exp).op); - }, helper.expFormat); + }); }); suite("Sub-expressions not in expressions", () => { @@ -86,7 +86,7 @@ suite("AST Matching", function() { let pattern = bolt.parseExpression(data.pattern); let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); assert.equal(match.exp, null); - }, helper.expFormat); + }); }); suite("Sub-expressions with params", () => { @@ -103,7 +103,7 @@ suite("AST Matching", function() { bolt.parseExpression(data.exp), data.vars); assert.ok(match.exp !== null && ( match.exp).op === ( match.exp).op); - }, helper.expFormat); + }); }); suite("Sub-expressions with params not present", () => { @@ -119,6 +119,92 @@ suite("AST Matching", function() { bolt.parseExpression(data.exp), data.vars); assert.ok(match.exp == null); - }, helper.expFormat); + }); + }); + + suite("Re-writing descriptors", () => { + let tests = [ + { data: "a => b", + expect: { params: [], pattern: "a", replacement: "b" } }, + { data: "() a => b", + expect: { params: [], pattern: "a", replacement: "b" } }, + { data: "()a => b", + expect: { params: [], pattern: "a", replacement: "b" } }, + { data: "(a) a => b", + expect: { params: ['a'], pattern: "a", replacement: "b" } }, + { data: "(a, b) a => b", + expect: { params: ['a', 'b'], pattern: "a", replacement: "b" } }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let rule = matcher.Rewriter.fromDescriptor(data); + assert.deepEqual(rule.paramNames, expect.params); + assert.equal(ast.decodeExpression(rule.pattern), expect.pattern); + assert.equal(ast.decodeExpression(rule.replacement), expect.replacement); + }); + }); + + suite("ReplaceVars", () => { + let tests = [ + { data: { exp: "a + 1", params: { a: "2" } }, + expect: "2 + 1" }, + { data: { exp: "a", params: { a: "2" } }, + expect: "2" }, + { data: { exp: "a.val()", params: { a: "newData" } }, + expect: "newData.val()" }, + { data: { exp: "a.val() + a", params: { a: "newData" } }, + expect: "newData.val() + newData" }, + // .a is a property name - not a variable + { data: { exp: "a.a()", params: { a: "newData" } }, + expect: "newData.a()" }, + { data: { exp: "a.val() != b.val()", params: { a: "this", b: "prior(this)" } }, + expect: "this.val() != prior(this).val()" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let exp = bolt.parseExpression(data.exp); + let params: ast.ExpParams = {}; + Object.keys(data.params).forEach((key) => { + params[key] = bolt.parseExpression(data.params[key]); + }); + let result = matcher.replaceVars(exp, params); + assert.equal(ast.decodeExpression(result), expect); + }); + }); + + suite("Expression re-writing", () => { + let tests = [ + { data: { rule: "newData => this", exp: "newData.val() != data.val()"}, + expect: "this.val() != data.val()" }, + { data: { rule: "data => prior(this)", exp: "newData.val() != data.val()"}, + expect: "newData.val() != prior(this).val()" }, + { data: { rule: "(a) a.val() => a", exp: "newData.val() != data.val()"}, + expect: "newData != data" }, + + { data: { rule: "(a) a || true => true", exp: "one || two"}, + expect: "one || two" }, + { data: { rule: "(a) a || true => true", exp: "one || true"}, + expect: "true" }, + { data: { rule: "(a) a || true => true", exp: "one || two || true"}, + expect: "true" }, + { data: { rule: "(a) a || true => true", exp: "true || one || two"}, + expect: "true" }, + + { data: { rule: "(a) a || false => a", exp: "one || two"}, + expect: "one || two" }, + { data: { rule: "(a) a || false => a", exp: "one || false"}, + expect: "one" }, + { data: { rule: "(a) a || false => a", exp: "one || two || false"}, + expect: "one || two" }, + { data: { rule: "(a) a || false => a", exp: "false || one || two"}, + expect: "one || two" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let exp = bolt.parseExpression(data.exp); + let rule = matcher.Rewriter.fromDescriptor(data.rule); + let result = rule.apply(exp); + assert.equal(ast.decodeExpression(result), expect); + }); }); }); diff --git a/src/test/parser-test.ts b/src/test/parser-test.ts index 2c66cb6..744525c 100644 --- a/src/test/parser-test.ts +++ b/src/test/parser-test.ts @@ -152,6 +152,9 @@ suite("Rules Parser Tests", function() { helper.dataDrivenTest(tests, function(data, expect) { let exp = bolt.parseExpression(data); assert.deepEqual(exp, expect); + + exp = ast.deepCopy(exp); + assert.deepEqual(exp, expect); }); }); From a0f18272d881e416c79e019a408f56fd399d83ce Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 5 Dec 2015 13:31:21 -0800 Subject: [PATCH 22/35] Permutation over a collection. --- src/permutation.ts | 54 ++++++++++++++---- src/test/permutation-test.ts | 106 ++++++++++++++++++++++------------- 2 files changed, 111 insertions(+), 49 deletions(-) diff --git a/src/permutation.ts b/src/permutation.ts index 515a7d1..67f558d 100644 --- a/src/permutation.ts +++ b/src/permutation.ts @@ -15,23 +15,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/// -export class Permutation { - current: number[] = []; - locations: number[] = []; - remaining: number; +export class IndexPermutation { + private current: number[] = []; + private locations: number[] = []; + private remaining: number; constructor(private n: number, private k?: number) { if (k === undefined) { this.k = n; } - this.remaining = 1; for (let i = 0; i < this.k; i++) { this.set(i, i); - this.remaining *= n - i; } - this.remaining -= 1; + this.remaining = this.getCount() - 1; + } + + getCount(): number { + let count = 1; + for (let i = 0; i < this.k; i++) { + count *= this.n - i; + } + return count; } getCurrent(): number[] { @@ -41,15 +46,14 @@ export class Permutation { return this.current.slice(); } - next(): number[] { + next() { if (this.remaining === 0) { this.current = null; } if (this.current === null) { - return null; + return; } this.advance(); - return this.current; } private advance() { @@ -87,3 +91,31 @@ export class Permutation { return undefined; } } + +export class Permutation { + collection: T[]; + p: IndexPermutation; + + constructor(collection: T[], k?: number) { + this.p = new IndexPermutation(collection.length, k); + this.collection = collection.slice(); + } + + getCount(): number { + return this.p.getCount(); + } + + getCurrent(): T[] { + let indexes = this.p.getCurrent(); + if (indexes === null) { + return null; + } + return indexes.map((i) => { + return this.collection[i]; + }); + } + + next() { + this.p.next(); + } +} diff --git a/src/test/permutation-test.ts b/src/test/permutation-test.ts index f213873..027cf0f 100644 --- a/src/test/permutation-test.ts +++ b/src/test/permutation-test.ts @@ -17,45 +17,75 @@ import chai = require('chai'); var assert = chai.assert; import helper = require('./test-helper'); -import {Permutation} from '../permutation'; +import {IndexPermutation, Permutation} from '../permutation'; -suite("Permutation", () => { - let tests = [ - { data: [1], - expect: { values: [[0]], count: 1 } }, - { data: [1, 1], - expect: { values: [[0]], count: 1 } }, - { data: [2], - expect: { values: [[0, 1], [1, 0]], count: 2 } }, - { data: [3], - expect: { values: [[0, 1, 2], - [0, 2, 1], - [1, 0, 2], - [1, 2, 0], - [2, 0, 1], - [2, 1, 0]], - count: 6 } }, - { data: [3, 1], - expect: { values: [[0], [1], [2]], - count: 3 } }, - { data: [3, 2], - expect: { values: [[0, 1], - [0, 2], - [1, 0], - [1, 2], - [2, 0], - [2, 1]], - count: 6 } }, - ]; +suite("Permutations", () => { + suite("IndexPermutation", () => { + let tests = [ + { data: [1], + expect: { values: [[0]], count: 1 } }, + { data: [1, 1], + expect: { values: [[0]], count: 1 } }, + { data: [2], + expect: { values: [[0, 1], [1, 0]], count: 2 } }, + { data: [3], + expect: { values: [[0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0]], + count: 6 } }, + { data: [3, 1], + expect: { values: [[0], [1], [2]], + count: 3 } }, + { data: [3, 2], + expect: { values: [[0, 1], + [0, 2], + [1, 0], + [1, 2], + [2, 0], + [2, 1]], + count: 6 } }, + ]; - helper.dataDrivenTest(tests, function(data, expect) { - let p = new Permutation(data[0], data[1]); - assert.equal(p.remaining + 1, expect.count); - let results = []; - while (p.getCurrent() !== null) { - results.push(p.getCurrent()); - p.next(); - } - assert.deepEqual(results, expect.values); + helper.dataDrivenTest(tests, function(data, expect) { + let p = new IndexPermutation(data[0], data[1]); + assert.equal(p.getCount(), expect.count); + let results = []; + while (p.getCurrent() !== null) { + results.push(p.getCurrent()); + p.next(); + } + assert.deepEqual(results, expect.values); + }); + }); + + suite("Permutation", () => { + let tests = [ + { data: { c: ['a'], k: undefined }, + expect: { values: [['a']], count: 1 } }, + { data: { c: ['a', 'b'], k: undefined }, + expect: { values: [['a', 'b'], ['b', 'a']], count: 2 } }, + { data: { c: ['a', 'b', 'c'], k: 2 }, + expect: { values: [['a', 'b'], + ['a', 'c'], + ['b', 'a'], + ['b', 'c'], + ['c', 'a'], + ['c', 'b']], + count: 6 } }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let p = new Permutation(data.c, data.k); + assert.equal(p.getCount(), expect.count); + let results = []; + while (p.getCurrent() !== null) { + results.push(p.getCurrent()); + p.next(); + } + assert.deepEqual(results, expect.values); + }); }); }); From 0d714cfd54d094c9ef594e177a63d684323f87e5 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 12 Dec 2015 19:00:53 -0800 Subject: [PATCH 23/35] Require free variable in boolean expression patterns. --- src/ast-matcher.ts | 65 +++++++++++++++++++++++++++++----------- src/test/matcher-test.ts | 15 ++++------ 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 50bd0b3..c37d27e 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -18,6 +18,8 @@ import ast = require('./ast'); import bolt = require('./bolt'); import util = require('./util'); +import {Permutation} from './permutation'; + let reverseOp = { '<': '>', '>': '<', @@ -90,7 +92,6 @@ export class Match { ast.setChild(replacement, parentPart.exp, parentPart.index); return this.path[0].exp; } - } // "[()] => " @@ -130,7 +131,6 @@ export class Rewriter { if (match.exp === null) { break; } - console.log("A", ast.decodeExpression(match.exp)); if (limit-- <= 0) { throw new Error("Too many patterns (" + ast.decodeExpression(this.pattern) + ") in expression: " + ast.decodeExpression(exp)); @@ -165,9 +165,7 @@ export function findExp(pattern: ast.Exp, while (match.exp !== null) { let params: ast.ExpParams = {}; - console.log("FE", ast.decodeExpression(match.exp)); if (equivalent(pattern, match.exp, paramNames, params)) { - console.log("FE2", ast.decodeExpression(match.exp)); match.params = params; return match; } @@ -176,6 +174,11 @@ export function findExp(pattern: ast.Exp, return match; } +/* + * Test for equivalence of two expressions. Allows for wildcard + * subexpressions (given in paramNames). When a match is found, + * the value of the wildcards is returnd in params. + */ function equivalent(pattern: ast.Exp, exp: ast.Exp, paramNames?: string[], @@ -185,7 +188,6 @@ function equivalent(pattern: ast.Exp, let name = ( pattern).name; if (util.arrayIncludes(paramNames, name)) { if (params[name] === undefined) { - console.log("EQ", name + " = " + ast.decodeExpression(exp)); params[name] = ast.copyExp(exp); return true; } else { @@ -263,25 +265,52 @@ function equivalent(pattern: ast.Exp, case '||': case '&&': - // Find any (unique) occurance for all children of the pattern. - let matches: number[] = []; - while (matches.length < patternOp.args.length) { - let j: number; - for (j = 0; j < expOp.args.length; j++) { - if (util.arrayIncludes(matches, j)) { - continue; - } - if (equivalent(patternOp.args[matches.length], expOp.args[j], paramNames, params)) { - matches.push(j); + // For boolean expressions the first clause of the pattern must be a "free variable". + // After matching remainder of pattern against a permutation of the arguments we assign + // the free variable to the unmatched clauses. + let freeName: string; + if (patternOp.args[0].type === 'var' || + util.arrayIncludes(paramNames, ( patternOp.args[0]).name)) { + freeName = ( patternOp.args[0]).name; + } else { + throw new Error("First clause of boolean pattern must be a free variable."); + } + let p: Permutation; + for (p = new Permutation(expOp.args, patternOp.args.length - 1); + p.getCurrent() != null; + p.next()) { + let tempParams = util.extend({}, params); + var args = p.getCurrent(); + let i: number; + for (i = 0; i < args.length; i++) { + if (!equivalent(patternOp.args[i + 1], args[i], paramNames, tempParams)) { break; } } - if (j >= expOp.args.length) { - return false; + + // Found a match! + if (i === args.length) { + util.extend(params, tempParams); + if (params[freeName] !== undefined) { + throw new Error("First clause of boolean expression cannot be repeated."); + } + var extraArgs: ast.Exp[] = []; + expOp.args.forEach((arg) => { + if (!util.arrayIncludes(args, arg)) { + extraArgs.push(arg); + } + }); + if (extraArgs.length === 1) { + params[freeName] = extraArgs[0]; + } else if (extraArgs.length > 1) { + params[freeName] = ast.op(patternOp.op, extraArgs); + } + return true; } } + return false; } - return true; + break; case 'literal': case 'var': diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 03e1adb..d2c397d 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -52,13 +52,6 @@ suite("AST Matching", function() { suite("Sub-expressions in expressions", () => { let tests = [ { pattern: "a + 1", exp: "a + 1" }, - { pattern: "a || b", exp: "a || b" }, - { pattern: "a || b", exp: "b || a" }, - { pattern: "a || b", exp: "a || b || c" }, - { pattern: "b || c", exp: "a || b || c" }, - { pattern: "a || c", exp: "a || b || c" }, - { pattern: "a || c", exp: "c || b || a" }, - { pattern: "a || c", exp: "b && (a || b || c)" }, { pattern: "a.test(/x/)", exp: "a + a.test(/x/)" }, { pattern: "a < b", exp: "a < b" }, { pattern: "a < b", exp: "b > a" }, @@ -79,7 +72,7 @@ suite("AST Matching", function() { suite("Sub-expressions not in expressions", () => { let tests = [ { pattern: "a + 1", exp: "1 + a" }, - { pattern: "a || c", exp: "c || b || b" }, + { pattern: "a + c", exp: "c + b + b" }, ]; helper.dataDrivenTest(tests, function(data, expect) { @@ -94,7 +87,8 @@ suite("AST Matching", function() { { vars: ['a'], pattern: "a + 1", exp: "a + 1" }, { vars: ['a'], pattern: "a + 1", exp: "x + 1" }, { vars: ['x'], pattern: "x || true", exp: "a || b || true || c" }, - { vars: ['x'], pattern: "x || x", exp: "a || b || a" }, + // Ignore this complex pattern for now. + // { vars: ['x'], pattern: "x || x", exp: "a || b || a" }, ]; helper.dataDrivenTest(tests, function(data, expect) { @@ -110,7 +104,8 @@ suite("AST Matching", function() { let tests = [ { vars: ['a'], pattern: "a + 1", exp: "a + 2" }, { vars: ['a'], pattern: "a + 1", exp: "x + 2" }, - { vars: ['x'], pattern: "x || x", exp: "a || b || c" }, + // Ignore complex expression + // { vars: ['x'], pattern: "x || x", exp: "a || b || c" }, ]; helper.dataDrivenTest(tests, function(data, expect) { From 5cca0e113be09c72002f2b7c335168eb7883165e Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 12 Dec 2015 20:07:31 -0800 Subject: [PATCH 24/35] Add expression transformation rules to decoder. --- src/rules-decoder.ts | 46 ++++++++++++++++++++++++++++++++++++---- src/test/decoder-test.ts | 10 ++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index bb7fc63..5f975a2 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -17,13 +17,14 @@ // import util = require('./util'); import ast = require('./ast'); import util = require('./util'); +import matcher = require('./ast-matcher'); var parser = require('./rules-parser'); var stripComments = require('strip-json-comments'); let typeIndicators = { - "newData.isString()": "String", - "newData.isNumber()": "Number", - "newData.isBoolean()": "Boolean" + "this.isString()": "String", + "this.isNumber()": "Number", + "this.isBoolean()": "Boolean" }; export function decodeRules(jsonString: string): string { @@ -54,12 +55,43 @@ class PathConstraints { } } +let readRules = [ + "data => this", +]; + +let writeRules = [ + "data => prior(this)", +]; + +let genRules = [ + "newData => this", + "(a, b) a.child(b) => a[b]", + "(a) a.val() => a", + "(a) a.exists() => a != null", + "(a, b) a.contains(b) => a.includes(b)", + "(a, b) a.beginsWith(b) => a.startsWith(b)", + "(a, b) a.matches(b) => this.test(b)", +]; + class Formatter { exps: { [path: string]: PathConstraints }; indent: number; + readRewriters: matcher.Rewriter[]; + writeRewriters: matcher.Rewriter[]; + genRewriters: matcher.Rewriter[]; constructor() { this.exps = {}; + this.readRewriters = readRules.map(matcher.Rewriter.fromDescriptor); + this.writeRewriters = writeRules.map(matcher.Rewriter.fromDescriptor); + this.genRewriters = genRules.map(matcher.Rewriter.fromDescriptor); + } + + applyRules(exp: ast.Exp, rules: matcher.Rewriter[]): ast.Exp { + rules.forEach((rewriter) => { + exp = rewriter.apply(exp); + }); + return exp; } decodeParts(path: string, json: Object) { @@ -78,7 +110,13 @@ class Formatter { if (method !== 'indexOn') { // Normalize expression try { - expString = ast.decodeExpression(parse(expString)); + let expIn = parse(expString); + let expOut = this.applyRules(expIn, + method === 'read' ? + this.readRewriters : + this.writeRewriters); + expOut = this.applyRules(expOut, this.genRewriters); + expString = ast.decodeExpression(expOut); } catch (e) { throw new Error("Could not parse expression: '" + expString + "'"); } diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index 60d4bb3..1b67798 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -71,19 +71,19 @@ suite("JSON Rules Decoder", function() { suite("Data references", function() { var tests = [ { data: { rules: { "a": { ".read": "data.child('prop').val() > 0"}} }, - expect: "path /a {\n read() = this.prop > 0;", + expect: "path /a {\n read() = this.prop > 0;\n}", }, { data: { rules: { "$a": { ".read": "data.child($a).val() > 0"}} }, - expect: "path /a {\n read() = this[$a] > 0;", + expect: "path /$a {\n read() = this[$a] > 0;\n}", }, { data: { rules: { "a": { ".read": "data.exists()"}} }, - expect: "path /a {\n read() = this != null;", + expect: "path /a {\n read() = this != null;\n}", }, { data: { rules: { "a": { ".validate": "newData.val() == data.val()"}} }, - expect: "path /a {\n validate() = this == prior(this);", + expect: "path /a {\n validate() = this == prior(this);\n}", }, ]; @@ -107,7 +107,7 @@ suite("JSON Rules Decoder", function() { let rules = { "rules": { "a": { ".read": "data.val()." + data} }}; - let bolt = "path /a {\n read() = this." + expect + ";"; + let bolt = "path /a {\n read() = this." + expect + ";\n}"; var result = decoder.decodeJSON(rules); assert.equal(result, bolt); }); From 487814908967c8b5d8bb23369e1838c783586f96 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 12 Dec 2015 20:22:16 -0800 Subject: [PATCH 25/35] Meaning of root is prior(root). --- src/rules-decoder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 5f975a2..7d44aa5 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -61,6 +61,8 @@ let readRules = [ let writeRules = [ "data => prior(this)", + "root => _oldRoot_", + "_oldRoot_ => prior(root)", ]; let genRules = [ From 861c4e577728ba0e99ae1f3717997f93d4a48387 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 12 Dec 2015 22:40:13 -0800 Subject: [PATCH 26/35] Fix rule typo. --- src/rules-decoder.ts | 2 +- src/test/decoder-test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 7d44aa5..7266a9d 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -72,7 +72,7 @@ let genRules = [ "(a) a.exists() => a != null", "(a, b) a.contains(b) => a.includes(b)", "(a, b) a.beginsWith(b) => a.startsWith(b)", - "(a, b) a.matches(b) => this.test(b)", + "(a, b) a.matches(b) => a.test(b)", ]; class Formatter { diff --git a/src/test/decoder-test.ts b/src/test/decoder-test.ts index 1b67798..0b0da2a 100644 --- a/src/test/decoder-test.ts +++ b/src/test/decoder-test.ts @@ -60,6 +60,9 @@ suite("JSON Rules Decoder", function() { "c": { ".write": "false" }}} }, expect: "path /a {\n read() = true;\n /b {\n write() = true;\n }\n /c {\n write() = false;\n }\n}" }, + + { data: { rules: {"a": { ".validate": "(newData.val() + '').matches(/^-?\\d+$/)" }}}, + expect: "path /a {\n validate() = (this + '').test(/^-?\\d+$/);\n}" }, ]; helper.dataDrivenTest(tests, function(data, expect) { From 3e6899ca5f3e06d7f0d8209cd3146f7932bafabf Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 14 Dec 2015 22:11:26 -0800 Subject: [PATCH 27/35] Simplify not exists rule. --- src/rules-decoder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 7266a9d..84624eb 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -73,6 +73,7 @@ let genRules = [ "(a, b) a.contains(b) => a.includes(b)", "(a, b) a.beginsWith(b) => a.startsWith(b)", "(a, b) a.matches(b) => a.test(b)", + "(a, b) !(a != b) => a == b", ]; class Formatter { From 0892ec3ee744a9df4ad0df1d46bfac7434d8e7a5 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 15 Dec 2015 17:17:42 -0800 Subject: [PATCH 28/35] Map hasChild to null check. --- src/rules-decoder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 84624eb..87dbf4a 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -70,6 +70,7 @@ let genRules = [ "(a, b) a.child(b) => a[b]", "(a) a.val() => a", "(a) a.exists() => a != null", + "(a, b) a.hasChild(b) => a[b] != null", "(a, b) a.contains(b) => a.includes(b)", "(a, b) a.beginsWith(b) => a.startsWith(b)", "(a, b) a.matches(b) => a.test(b)", From 84d9a5a039b7dfcb65f3699f02a1a3927197d4c6 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Fri, 18 Dec 2015 17:40:23 -0800 Subject: [PATCH 29/35] Checkpoint - rewrite function. --- bin/firebase-bolt | 22 +++++++++++++---- src/ast-matcher.ts | 51 +++++++++++++++++++++++++++++++++++++--- src/ast.ts | 27 ++++++++++++++++++++- src/rules-decoder.ts | 3 ++- src/test/matcher-test.ts | 33 ++++++++++++++++++++++++-- 5 files changed, 125 insertions(+), 11 deletions(-) diff --git a/bin/firebase-bolt b/bin/firebase-bolt index fff55aa..de4ffab 100755 --- a/bin/firebase-bolt +++ b/bin/firebase-bolt @@ -32,9 +32,10 @@ var opts = { boolean: ['version', 'help'], string: ['output'], alias: { - 'v': 'version', + 'f': 'functions', 'h': 'help', 'o': 'output', + 'v': 'version', }, unknown: function(flag) { if (flag[0] == '-') { @@ -50,10 +51,23 @@ var commands = { log("Missing JSON file name to migrate to bolt file."); process.exit(1); } + var functionsFileName = getInputFileName(args.functions, 'bolt'); + var functions; + if (functionsFileName !== undefined) { + readInputFile(functionsFileName, 'Bolt', function(boltText) { + try { + var symbols = bolt.parse(boltText); + functions = symbols.functions; + } catch (e) { + log(e.message, e.line, e.column); + process.exit(1); + } + }); + } var inputFileName = getInputFileName(args._[1], 'json'); var outputFileName = getOutputFileName(args.output, inputFileName, 'bolt'); readInputFile(inputFileName, 'JSON', function(jsonData) { - writeOutputFile(outputFileName, bolt.decodeRules(jsonData)); + writeOutputFile(outputFileName, bolt.decodeRules(jsonData, functions)); }); } }; @@ -167,12 +181,12 @@ function usage(code) { console.error(" " + cmdName + " < rules.bolt > rules.json"); console.error(" " + cmdName + " rules"); console.error(" (rules.bolt => rules.json)"); - console.error(" " + cmdName + " migrate rules.json"); + console.error(" " + cmdName + " migrate [--functions file] rules.json"); console.error(" (rules.json => rules.bolt)\n"); - console.error(" Options:\n"); console.error(util.formatColumns(4, [ + ["-f --functions file", "Include file of top level Bolt functions (for migrate)."], ["-h --help", "Display this helpful message."], ["-o --output file", "Output to file."], ["-v --version", "Display Firebase Bolt version."], diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index c37d27e..5066ae6 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -24,6 +24,7 @@ let reverseOp = { '<': '>', '>': '<', '<=': '>=', + '>=': '<=', }; /* @@ -90,12 +91,17 @@ export class Match { } let parentPart = this.path.slice(-1)[0]; ast.setChild(replacement, parentPart.exp, parentPart.index); + // When a boolean expression is collapsed to a single argument - hoist the argument + // to the parent. + if (parentPart.exp.type === 'op' && ( parentPart.exp).args.length === 1) { + this.path.slice(-1)[0].exp = ( parentPart.exp).args[0]; + } return this.path[0].exp; } } // "[()] => " -// E.g. "(a) a.val() => a" +// E.g. "(a, b) a.val() + b => a + b" let descriptorRegexp = /^\s*(?:\((.*)\))?\s*(.*\S)\s*=>\s*(.*\S)\s*$/; export class Rewriter { @@ -123,6 +129,32 @@ export class Rewriter { bolt.parseExpression(match[3])); } + static fromFunction(name: string, method: ast.Method) { + if (method.body.type === 'op') { + let op = ( method.body).op; + if (op === '&&' || op === '||') { + // f(a, b) = becomes (_x_, a, b) => _x_ f(a, b) + let free = ast.variable('_x_'); + let params = util.extendArray(['_x_'], method.params); + let body = ast.copyExp(method.body); + body.args = util.extendArray([free], body.args); + let result = new Rewriter(params, + body, + ast.op(op, [free, + ast.call(ast.variable(name), + method.params.map(ast.variable))])); + console.log(result.toString()); + return result; + } + } + + // f(a, b) = becomes (a, b) => f(a, b) + return new Rewriter(method.params, + method.body, + ast.call(ast.variable(name), + method.params.map(ast.variable))); + } + apply(exp: ast.Exp): ast.Exp { let match: Match; let limit = 50; @@ -140,6 +172,17 @@ export class Rewriter { } return exp; } + + toString(): string { + let result = ''; + if (this.paramNames.length > 0) { + result += '(' + this.paramNames.join(', ') + ') '; + } + result += ast.decodeExpression(this.pattern); + result += ' => '; + result += ast.decodeExpression(this.replacement); + return result; + } } export function replaceVars(exp: ast.Exp, params: ast.ExpParams): ast.Exp { @@ -300,9 +343,11 @@ function equivalent(pattern: ast.Exp, extraArgs.push(arg); } }); - if (extraArgs.length === 1) { + if (extraArgs.length === 0) { + params[freeName] = ast.voidType(); + } else if (extraArgs.length === 1) { params[freeName] = extraArgs[0]; - } else if (extraArgs.length > 1) { + } else { params[freeName] = ast.op(patternOp.op, extraArgs); } return true; diff --git a/src/ast.ts b/src/ast.ts index 4de18f4..fb0fdb2 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -37,6 +37,9 @@ export interface RegExpValue extends ExpValue { export interface ExpNull extends Exp { } +export interface ExpVoid extends Exp { +} + export interface ExpOp extends Exp { op: string; args: Exp[]; @@ -153,6 +156,10 @@ export function nullType(): ExpNull { return { type: 'Null', valueType: 'Null' }; } +export function voidType(): ExpVoid { + return { type: 'Void', valueType: 'Void' }; +} + export function reference(base: Exp, prop: Exp): ExpReference { return { type: 'ref', @@ -350,6 +357,7 @@ function leftAssociateGen(opType: string, identityValue: ExpValue, zeroValue: Ex return function(a: Exp[]): Exp { var i; + // TODO: No longer needed - just return flattened expression in one args array. function reducer(result, current) { if (result === undefined) { return current; @@ -596,6 +604,11 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string { result = 'null'; break; + case 'Void': + // Missing expression! + result = 'void'; + break; + case 'var': case 'literal': result = ( exp).name; @@ -792,7 +805,16 @@ export function setChild(expChild: Exp, expParent: Exp, index: number) { break; case 'op': - ( expParent).args[index] = expChild; + let expOp = expParent; + // Void indicates to removing element from parent. + if (expChild.type === 'Void') { + expOp.args.splice(index, 1); + if (expOp.args.length < 1) { + throw new Error("Removing last argument from " + expOp.op); + } + } else { + expOp.args[index] = expChild; + } break; case 'union': @@ -812,6 +834,9 @@ export function setChild(expChild: Exp, expParent: Exp, index: number) { } export function deepCopy(exp: Exp): Exp { + if (exp === null) { + return null; + } exp = copyExp(exp); let c = childCount(exp); for (let i = 0; i < c; i++) { diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 87dbf4a..3ae86b9 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -27,7 +27,8 @@ let typeIndicators = { "this.isBoolean()": "Boolean" }; -export function decodeRules(jsonString: string): string { +export function decodeRules(jsonString: string, + functions: {[ name: string ]: ast.Method}): string { return decodeJSON(JSON.parse(cleanJSONString(jsonString))); } diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index d2c397d..571bfe1 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -87,8 +87,7 @@ suite("AST Matching", function() { { vars: ['a'], pattern: "a + 1", exp: "a + 1" }, { vars: ['a'], pattern: "a + 1", exp: "x + 1" }, { vars: ['x'], pattern: "x || true", exp: "a || b || true || c" }, - // Ignore this complex pattern for now. - // { vars: ['x'], pattern: "x || x", exp: "a || b || a" }, + { vars: ['x'], pattern: "_x_ || x || x", exp: "a || b || a" }, ]; helper.dataDrivenTest(tests, function(data, expect) { @@ -202,4 +201,34 @@ suite("AST Matching", function() { assert.equal(ast.decodeExpression(result), expect); }); }); + + suite("Function rewriting", () => { + let tests = [ + { data: { functions: "isUser(a) = auth.uid == a;", + exp: "auth.uid == this" }, + expect: "isUser(this)" }, + { data: { functions: "add(a, b) = a + b;", + exp: "x + y" }, + expect: "add(x, y)" }, + { data: { functions: "add(a, b) = a + b; sub(a, b) = a - b;", + exp: "x + y - z" }, + expect: "sub(add(x, y), z)" }, + { data: { functions: "add(a, b) = a + b; sub(a, b) = a - b;", + exp: "x + (y - z)" }, + expect: "add(x, sub(y, z))" }, + { data: { functions: "or(a, b) = a || b;", + exp: "x || y" }, + expect: "or(x, y)" }, + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let exp = bolt.parseExpression(data.exp); + let functions = bolt.parse(data.functions).functions; + Object.keys(functions).forEach((name) => { + let rule = matcher.Rewriter.fromFunction(name, functions[name]); + exp = rule.apply(exp); + }); + assert.equal(ast.decodeExpression(exp), expect); + }); + }); }); From c4622d155fd299d3d0709be507032d44afe726b4 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sat, 19 Dec 2015 12:53:05 -0800 Subject: [PATCH 30/35] MultiFunctor for Rewriters --- src/ast-matcher.ts | 8 ++++---- src/bolt.ts | 12 ++++-------- src/parseUtil.ts | 24 ++++++++++++++++++++++++ src/rules-decoder.ts | 41 +++++++++-------------------------------- src/util.ts | 16 ++++++++++++++++ 5 files changed, 57 insertions(+), 44 deletions(-) create mode 100644 src/parseUtil.ts diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 5066ae6..4d063fe 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -15,7 +15,7 @@ */ /// import ast = require('./ast'); -import bolt = require('./bolt'); +import {parseExpression} from './parseUtil'; import util = require('./util'); import {Permutation} from './permutation'; @@ -104,7 +104,7 @@ export class Match { // E.g. "(a, b) a.val() + b => a + b" let descriptorRegexp = /^\s*(?:\((.*)\))?\s*(.*\S)\s*=>\s*(.*\S)\s*$/; -export class Rewriter { +export class Rewriter implements util.Functor { constructor(public paramNames: string[], public pattern: ast.Exp, public replacement: ast.Exp) { @@ -125,8 +125,8 @@ export class Rewriter { } } return new Rewriter(paramNames, - bolt.parseExpression(match[2]), - bolt.parseExpression(match[3])); + parseExpression(match[2]), + parseExpression(match[3])); } static fromFunction(name: string, method: ast.Method) { diff --git a/src/bolt.ts b/src/bolt.ts index 8907b64..1cb94c2 100644 --- a/src/bolt.ts +++ b/src/bolt.ts @@ -19,24 +19,20 @@ var parser = require('./rules-parser'); import generator = require('./rules-generator'); import decoder = require('./rules-decoder'); import simulator = require('./simulator'); -import astReal = require('./ast'); +import parseUtil = require('./parseUtil'); +export import ast = require('./ast'); export var FILE_EXTENSION = 'bolt'; export var parse = parser.parse; export var Generator = generator.Generator; -export var ast = astReal; export var decodeExpression = ast.decodeExpression; export var decodeRules = decoder.decodeRules; export var rulesSuite = simulator.rulesSuite; +export var parseExpression = parseUtil.parseExpression; export function generate(boltText: string): generator.Validator { - let symbols = parser.parse(boltText); + let symbols = parser.parse(boltText); var gen = new generator.Generator(symbols); return gen.generateRules(); } - -export function parseExpression(expression: string): astReal.Exp { - var result = parse('function f() {return ' + expression + ';}'); - return result.functions.f.body; -} diff --git a/src/parseUtil.ts b/src/parseUtil.ts new file mode 100644 index 0000000..4e796ca --- /dev/null +++ b/src/parseUtil.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +var parser = require('./rules-parser'); +import ast = require('./ast'); + +export function parseExpression(expression: string): ast.Exp { + var result = parser.parse('function f() {return ' + expression + ';}'); + return result.functions.f.body; +} diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index 3ae86b9..a764f79 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -18,7 +18,7 @@ import ast = require('./ast'); import util = require('./util'); import matcher = require('./ast-matcher'); -var parser = require('./rules-parser'); +import {parseExpression} from './parseUtil'; var stripComments = require('strip-json-comments'); let typeIndicators = { @@ -56,17 +56,15 @@ class PathConstraints { } } -let readRules = [ - "data => this", -]; +let readRewriter = matcher.Rewriter.fromDescriptor("data => this"); -let writeRules = [ +let writeRewriter = new util.MultiFunctor([ "data => prior(this)", "root => _oldRoot_", "_oldRoot_ => prior(root)", -]; +].map(matcher.Rewriter.fromDescriptor)); -let genRules = [ +let genRewriter = new util.MultiFunctor([ "newData => this", "(a, b) a.child(b) => a[b]", "(a) a.val() => a", @@ -76,27 +74,14 @@ let genRules = [ "(a, b) a.beginsWith(b) => a.startsWith(b)", "(a, b) a.matches(b) => a.test(b)", "(a, b) !(a != b) => a == b", -]; +].map(matcher.Rewriter.fromDescriptor)); class Formatter { exps: { [path: string]: PathConstraints }; indent: number; - readRewriters: matcher.Rewriter[]; - writeRewriters: matcher.Rewriter[]; - genRewriters: matcher.Rewriter[]; constructor() { this.exps = {}; - this.readRewriters = readRules.map(matcher.Rewriter.fromDescriptor); - this.writeRewriters = writeRules.map(matcher.Rewriter.fromDescriptor); - this.genRewriters = genRules.map(matcher.Rewriter.fromDescriptor); - } - - applyRules(exp: ast.Exp, rules: matcher.Rewriter[]): ast.Exp { - rules.forEach((rewriter) => { - exp = rewriter.apply(exp); - }); - return exp; } decodeParts(path: string, json: Object) { @@ -115,12 +100,9 @@ class Formatter { if (method !== 'indexOn') { // Normalize expression try { - let expIn = parse(expString); - let expOut = this.applyRules(expIn, - method === 'read' ? - this.readRewriters : - this.writeRewriters); - expOut = this.applyRules(expOut, this.genRewriters); + let expIn = parseExpression(expString); + let expOut = (method === 'read' ? readRewriter : writeRewriter).apply(expIn); + expOut = genRewriter.apply(expOut); expString = ast.decodeExpression(expOut); } catch (e) { throw new Error("Could not parse expression: '" + expString + "'"); @@ -201,11 +183,6 @@ class Formatter { } } -function parse(s: string): ast.Exp { - var result = parser.parse("f() = " + s + ";"); - return result.functions.f.body; -} - function childPath(path: string, child: string): string { if (path.slice(-1) === '/') { return path + child; diff --git a/src/util.ts b/src/util.ts index bd36bd1..693d829 100644 --- a/src/util.ts +++ b/src/util.ts @@ -283,3 +283,19 @@ export function commonPrefixSize>(s1: T, s2: T): number } return last; } + +export interface Functor { + apply(t: T): T; +} + +export class MultiFunctor implements Functor { + constructor(public funcs: Functor[]) { + } + + apply(t: T): T { + this.funcs.forEach((f) => { + t = f.apply(t); + }); + return t; + } +} From 3bf87e45084fed029a0a20f6ff1585ad68691ba9 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 20 Dec 2015 09:07:56 -0800 Subject: [PATCH 31/35] Fix expression simplification hanging bug. --- src/ast-matcher.ts | 105 +++++++++++++++++++++++---------------- src/permutation.ts | 3 ++ src/rules-decoder.ts | 22 ++++---- src/test/matcher-test.ts | 82 ++++++++++++++++++++---------- src/util.ts | 3 ++ 5 files changed, 136 insertions(+), 79 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 4d063fe..2c5a617 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -18,7 +18,7 @@ import ast = require('./ast'); import {parseExpression} from './parseUtil'; import util = require('./util'); -import {Permutation} from './permutation'; +import {IndexPermutation, Permutation} from './permutation'; let reverseOp = { '<': '>', @@ -124,26 +124,27 @@ export class Rewriter implements util.Functor { paramNames = []; } } - return new Rewriter(paramNames, - parseExpression(match[2]), - parseExpression(match[3])); + let result = new Rewriter(paramNames, + parseExpression(match[2]), + parseExpression(match[3])); + return result; } static fromFunction(name: string, method: ast.Method) { if (method.body.type === 'op') { let op = ( method.body).op; if (op === '&&' || op === '||') { - // f(a, b) = becomes (_x_, a, b) => _x_ f(a, b) - let free = ast.variable('_x_'); - let params = util.extendArray(['_x_'], method.params); + // f(a, b) = becomes (a, b, _x) OP _x => f(a, b) OP _x + let free = ast.variable('_x'); + let params = util.copyArray(method.params); + params.push('_x'); let body = ast.copyExp(method.body); - body.args = util.extendArray([free], body.args); + body.args.push(free); let result = new Rewriter(params, - body, - ast.op(op, [free, - ast.call(ast.variable(name), - method.params.map(ast.variable))])); - console.log(result.toString()); + body, + ast.op(op, [ast.call(ast.variable(name), + method.params.map(ast.variable)), + free])); return result; } } @@ -243,17 +244,33 @@ function equivalent(pattern: ast.Exp, return false; } - function equivalentChildren(): boolean { + function sameChildren(anyOrder = false): boolean { let patternCount = ast.childCount(pattern); if (patternCount !== ast.childCount(exp)) { return false; } - for (let i = 0; i < patternCount; i++) { - if (!equivalent(ast.getChild(pattern, i), ast.getChild(exp, i), paramNames, params)) { + for (let p = new IndexPermutation(patternCount); + p.getCurrent() != null; + p.next()) { + let indexes = p.getCurrent(); + let tempParams = util.extend({}, params); + let i: number; + for (i = 0; i < patternCount; i++) { + if (!equivalent(ast.getChild(pattern, i), + ast.getChild(exp, indexes[i]), + paramNames, tempParams)) { + break; + } + } + if (i === patternCount) { + util.extend(params, tempParams); + return true; + } + if (!anyOrder) { return false; } } - return true; + return false; } switch (pattern.type) { @@ -270,7 +287,7 @@ function equivalent(pattern: ast.Exp, case 'call': case 'ref': case 'union': - return equivalentChildren(); + return sameChildren(); case 'generic': if (( pattern).name !== ( exp).name) { @@ -295,38 +312,36 @@ function equivalent(pattern: ast.Exp, switch (patternOp.op) { default: - return equivalentChildren(); + return sameChildren(); case '==': case '!=': - if (equivalentChildren()) { - return true; - } - exp = ast.copyExp(expOp); - ( exp).args = [expOp.args[1], expOp.args[0]]; - return equivalentChildren(); + return sameChildren(true); case '||': case '&&': - // For boolean expressions the first clause of the pattern must be a "free variable". - // After matching remainder of pattern against a permutation of the arguments we assign - // the free variable to the unmatched clauses. - let freeName: string; - if (patternOp.args[0].type === 'var' || - util.arrayIncludes(paramNames, ( patternOp.args[0]).name)) { - freeName = ( patternOp.args[0]).name; - } else { - throw new Error("First clause of boolean pattern must be a free variable."); + // For boolean expressions if the last clause of the pattern is a "free variable", after + // matching remainder of pattern against a permutation of the arguments we assign the + // free variable to the unmatched clauses. + let lastArg = patternOp.args[patternOp.args.length - 1]; + if (lastArg.type !== 'var' || !util.arrayIncludes(paramNames, lastArg.name) || + params[lastArg.name] !== undefined) { + return sameChildren(true); + } + + let argCount = patternOp.args.length - 1; + if (argCount > expOp.args.length) { + return false; } let p: Permutation; - for (p = new Permutation(expOp.args, patternOp.args.length - 1); + for (p = new Permutation(expOp.args, argCount); p.getCurrent() != null; p.next()) { let tempParams = util.extend({}, params); var args = p.getCurrent(); let i: number; for (i = 0; i < args.length; i++) { - if (!equivalent(patternOp.args[i + 1], args[i], paramNames, tempParams)) { + if (!equivalent(patternOp.args[i], args[i], paramNames, tempParams)) { break; } } @@ -334,8 +349,8 @@ function equivalent(pattern: ast.Exp, // Found a match! if (i === args.length) { util.extend(params, tempParams); - if (params[freeName] !== undefined) { - throw new Error("First clause of boolean expression cannot be repeated."); + if (params[lastArg.name] !== undefined) { + throw new Error("Free-variable of boolean expression cannot be repeated."); } var extraArgs: ast.Exp[] = []; expOp.args.forEach((arg) => { @@ -344,11 +359,11 @@ function equivalent(pattern: ast.Exp, } }); if (extraArgs.length === 0) { - params[freeName] = ast.voidType(); + params[lastArg.name] = ast.voidType(); } else if (extraArgs.length === 1) { - params[freeName] = extraArgs[0]; + params[lastArg.name] = extraArgs[0]; } else { - params[freeName] = ast.op(patternOp.op, extraArgs); + params[lastArg.name] = ast.op(patternOp.op, extraArgs); } return true; } @@ -362,6 +377,12 @@ function equivalent(pattern: ast.Exp, return ( pattern).name === ( exp).name; default: - return false; + throw new Error("Unknown expression pattern: " + ast.decodeExpression(pattern)); } } + +export let simplifyRewriter = new util.MultiFunctor([ + "(_a, _x) _a && _a && _x => _a && _x", + "(_a, _x) _a || _a || _x => _a || _x", + "(_a, _b) !(_a != _b) => _a == _b", +].map(Rewriter.fromDescriptor)); diff --git a/src/permutation.ts b/src/permutation.ts index 67f558d..eb70537 100644 --- a/src/permutation.ts +++ b/src/permutation.ts @@ -25,6 +25,9 @@ export class IndexPermutation { if (k === undefined) { this.k = n; } + if (k > n) { + throw new Error("Illegal permutation size " + k + " > " + n); + } for (let i = 0; i < this.k; i++) { this.set(i, i); } diff --git a/src/rules-decoder.ts b/src/rules-decoder.ts index a764f79..48e3b11 100644 --- a/src/rules-decoder.ts +++ b/src/rules-decoder.ts @@ -60,20 +60,19 @@ let readRewriter = matcher.Rewriter.fromDescriptor("data => this"); let writeRewriter = new util.MultiFunctor([ "data => prior(this)", - "root => _oldRoot_", - "_oldRoot_ => prior(root)", + "root => _priorRoot", + "_priorRoot => prior(root)", ].map(matcher.Rewriter.fromDescriptor)); let genRewriter = new util.MultiFunctor([ "newData => this", - "(a, b) a.child(b) => a[b]", - "(a) a.val() => a", - "(a) a.exists() => a != null", - "(a, b) a.hasChild(b) => a[b] != null", - "(a, b) a.contains(b) => a.includes(b)", - "(a, b) a.beginsWith(b) => a.startsWith(b)", - "(a, b) a.matches(b) => a.test(b)", - "(a, b) !(a != b) => a == b", + "(_a, _b) _a.child(_b) => _a[_b]", + "(_a) _a.val() => _a", + "(_a) _a.exists() => _a != null", + "(_a, _b) _a.hasChild(_b) => _a[_b] != null", + "(_a, _b) _a.contains(_b) => _a.includes(_b)", + "(_a, _b) _a.beginsWith(_b) => _a.startsWith(_b)", + "(_a, _b) _a.matches(_b) => _a.test(_b)", ].map(matcher.Rewriter.fromDescriptor)); class Formatter { @@ -103,9 +102,10 @@ class Formatter { let expIn = parseExpression(expString); let expOut = (method === 'read' ? readRewriter : writeRewriter).apply(expIn); expOut = genRewriter.apply(expOut); + expOut = matcher.simplifyRewriter.apply(expOut); expString = ast.decodeExpression(expOut); } catch (e) { - throw new Error("Could not parse expression: '" + expString + "'"); + throw new Error("Could not parse expression: '" + expString + "' (" + e.stack + ")"); } } diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 571bfe1..b2fb4d3 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -17,6 +17,7 @@ import chai = require('chai'); var assert = chai.assert; import helper = require('./test-helper'); +import {parseExpression} from '../parseUtil'; import bolt = require('../bolt'); import ast = require('../ast'); import matcher = require('../ast-matcher'); @@ -26,8 +27,8 @@ suite("AST Matching", function() { let tests = ["false", "1", "'a'", "a", "[]", "1.2", "null", "[1,2]"]; helper.dataDrivenTest(tests, function(data, expect) { - let exp = bolt.parseExpression(data); - let match = matcher.findExp(bolt.parseExpression(data), exp); + let exp = parseExpression(data); + let match = matcher.findExp(parseExpression(data), exp); assert.deepEqual(match.exp, exp); }); }); @@ -43,8 +44,8 @@ suite("AST Matching", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - let pattern = bolt.parseExpression(data.pattern); - let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); + let pattern = parseExpression(data.pattern); + let match = matcher.findExp(pattern, parseExpression(data.exp)); assert.deepEqual(match.exp, pattern); }); }); @@ -63,8 +64,8 @@ suite("AST Matching", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - let pattern = bolt.parseExpression(data.pattern); - let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); + let pattern = parseExpression(data.pattern); + let match = matcher.findExp(pattern, parseExpression(data.exp)); assert.equal(( match.exp).op, ( match.exp).op); }); }); @@ -76,8 +77,8 @@ suite("AST Matching", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - let pattern = bolt.parseExpression(data.pattern); - let match = matcher.findExp(pattern, bolt.parseExpression(data.exp)); + let pattern = parseExpression(data.pattern); + let match = matcher.findExp(pattern, parseExpression(data.exp)); assert.equal(match.exp, null); }); }); @@ -86,14 +87,14 @@ suite("AST Matching", function() { let tests = [ { vars: ['a'], pattern: "a + 1", exp: "a + 1" }, { vars: ['a'], pattern: "a + 1", exp: "x + 1" }, - { vars: ['x'], pattern: "x || true", exp: "a || b || true || c" }, - { vars: ['x'], pattern: "_x_ || x || x", exp: "a || b || a" }, + { vars: ['_x'], pattern: "true || _x", exp: "a || b || true || c" }, + { vars: ['_x', 'x'], pattern: "x || x || _x", exp: "a || b || a" }, ]; helper.dataDrivenTest(tests, function(data, expect) { - let pattern = bolt.parseExpression(data.pattern); + let pattern = parseExpression(data.pattern); let match = matcher.findExp(pattern, - bolt.parseExpression(data.exp), + parseExpression(data.exp), data.vars); assert.ok(match.exp !== null && ( match.exp).op === ( match.exp).op); }); @@ -108,9 +109,9 @@ suite("AST Matching", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - let pattern = bolt.parseExpression(data.pattern); + let pattern = parseExpression(data.pattern); let match = matcher.findExp(pattern, - bolt.parseExpression(data.exp), + parseExpression(data.exp), data.vars); assert.ok(match.exp == null); }); @@ -156,10 +157,10 @@ suite("AST Matching", function() { ]; helper.dataDrivenTest(tests, function(data, expect) { - let exp = bolt.parseExpression(data.exp); + let exp = parseExpression(data.exp); let params: ast.ExpParams = {}; Object.keys(data.params).forEach((key) => { - params[key] = bolt.parseExpression(data.params[key]); + params[key] = parseExpression(data.params[key]); }); let result = matcher.replaceVars(exp, params); assert.equal(ast.decodeExpression(result), expect); @@ -175,27 +176,27 @@ suite("AST Matching", function() { { data: { rule: "(a) a.val() => a", exp: "newData.val() != data.val()"}, expect: "newData != data" }, - { data: { rule: "(a) a || true => true", exp: "one || two"}, + { data: { rule: "(_x) true || _x => true", exp: "one || two"}, expect: "one || two" }, - { data: { rule: "(a) a || true => true", exp: "one || true"}, + { data: { rule: "(_x) true || _x => true", exp: "one || true"}, expect: "true" }, - { data: { rule: "(a) a || true => true", exp: "one || two || true"}, + { data: { rule: "(_x) true || _x => true", exp: "one || two || true"}, expect: "true" }, - { data: { rule: "(a) a || true => true", exp: "true || one || two"}, + { data: { rule: "(_x) true || _x => true", exp: "true || one || two"}, expect: "true" }, - { data: { rule: "(a) a || false => a", exp: "one || two"}, + { data: { rule: "(_x) false || _x => _x", exp: "one || two"}, expect: "one || two" }, - { data: { rule: "(a) a || false => a", exp: "one || false"}, + { data: { rule: "(_x) false || _x => _x", exp: "one || false"}, expect: "one" }, - { data: { rule: "(a) a || false => a", exp: "one || two || false"}, + { data: { rule: "(_x) false || _x => _x", exp: "one || two || false"}, expect: "one || two" }, - { data: { rule: "(a) a || false => a", exp: "false || one || two"}, + { data: { rule: "(_x) false || _x => _x", exp: "false || one || two"}, expect: "one || two" }, ]; helper.dataDrivenTest(tests, function(data, expect) { - let exp = bolt.parseExpression(data.exp); + let exp = parseExpression(data.exp); let rule = matcher.Rewriter.fromDescriptor(data.rule); let result = rule.apply(exp); assert.equal(ast.decodeExpression(result), expect); @@ -219,10 +220,16 @@ suite("AST Matching", function() { { data: { functions: "or(a, b) = a || b;", exp: "x || y" }, expect: "or(x, y)" }, + { data: { functions: "and(a, b) = a && b;", + exp: "x && y" }, + expect: "and(x, y)" }, + { data: { functions: "and(a, b) = a && b;", + exp: "x && y && z" }, + expect: "and(and(x, y), z)" }, ]; helper.dataDrivenTest(tests, function(data, expect) { - let exp = bolt.parseExpression(data.exp); + let exp = parseExpression(data.exp); let functions = bolt.parse(data.functions).functions; Object.keys(functions).forEach((name) => { let rule = matcher.Rewriter.fromFunction(name, functions[name]); @@ -231,4 +238,27 @@ suite("AST Matching", function() { assert.equal(ast.decodeExpression(exp), expect); }); }); + + suite("Expression simplification", () => { + let tests = [ + [ "a && a", "a" ], + [ "a || a", "a" ], + [ "a && b || a && b", "a && b" ], + + [ "a && b && c && d && e", "a && b && c && d && e"], + [ "a && a && c && d && e", "a && c && d && e"], + [ "a && b && a && d && e", "a && b && d && e"], + [ "a && b && c && a && e", "a && b && c && e"], + [ "a && b && c && d && a", "a && b && c && d"], + + [ "hang && b || c && d && e", + "hang && b || c && d && e" ], + ]; + + helper.dataDrivenTest(tests, function(data, expect) { + let exp = parseExpression(data); + let result = matcher.simplifyRewriter.apply(exp); + assert.equal(ast.decodeExpression(result), expect); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 693d829..0d2e9d9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -284,10 +284,13 @@ export function commonPrefixSize>(s1: T, s2: T): number return last; } +// An object that looks like a function (has apply() function over +// values of type T). export interface Functor { apply(t: T): T; } +// Combine a sequence of Functors into one. export class MultiFunctor implements Functor { constructor(public funcs: Functor[]) { } From 6ba0dde03c2f58c8c7ce928f0fadcc4dce7985ec Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 20 Dec 2015 09:44:27 -0800 Subject: [PATCH 32/35] Fix unary op replacement bug. --- src/ast-matcher.ts | 12 +++++++++++- src/test/matcher-test.ts | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 2c5a617..a0031df 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -93,7 +93,10 @@ export class Match { ast.setChild(replacement, parentPart.exp, parentPart.index); // When a boolean expression is collapsed to a single argument - hoist the argument // to the parent. - if (parentPart.exp.type === 'op' && ( parentPart.exp).args.length === 1) { + let parentBool = parentPart.exp; + if (parentBool.type === 'op' && + (parentBool.op === '&&' || parentBool.op === '||') && + parentBool.args.length === 1) { this.path.slice(-1)[0].exp = ( parentPart.exp).args[0]; } return this.path[0].exp; @@ -384,5 +387,12 @@ function equivalent(pattern: ast.Exp, export let simplifyRewriter = new util.MultiFunctor([ "(_a, _x) _a && _a && _x => _a && _x", "(_a, _x) _a || _a || _x => _a || _x", + + "(_x) true || _x => true", + "(_x) true && _x => _x", + "(_x) false || _x => _x", + "(_x) false && _x => false", + "(_a, _b) !(_a != _b) => _a == _b", + "(_x) !!_x => _x", ].map(Rewriter.fromDescriptor)); diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index b2fb4d3..89a923f 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -253,6 +253,17 @@ suite("AST Matching", function() { [ "hang && b || c && d && e", "hang && b || c && d && e" ], + + [ "a || b || true || c", "true"], + [ "a && b && true && c", "a && b && c"], + + [ "a || b || false || c", "a || b || c"], + [ "a && b && false && c", "false"], + + [ "!a", "!a" ], + [ "!!a", "a" ], + [ "!!!a", "!a" ], + [ "!!!!a", "a" ], ]; helper.dataDrivenTest(tests, function(data, expect) { From 9bc9ab719b9d3227eef58f855ca6a3b6a7cca6b9 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 20 Dec 2015 11:53:12 -0800 Subject: [PATCH 33/35] Re-write rule tests. --- src/ast-matcher.ts | 29 ++++++++++++++++++++++++++++- src/ast.ts | 11 ++++++++++- src/rules-parser.pegjs | 4 ++-- src/test/matcher-test.ts | 10 ++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index a0031df..98dffbd 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -20,6 +20,8 @@ import util = require('./util'); import {IndexPermutation, Permutation} from './permutation'; +let DEBUG = false; + let reverseOp = { '<': '>', '>': '<', @@ -172,7 +174,14 @@ export class Rewriter implements util.Functor { ") in expression: " + ast.decodeExpression(exp)); } let replacement = replaceVars(this.replacement, match.params); + let oldExp: ast.Exp; + if (DEBUG) { + oldExp = ast.copyExp(exp); + } exp = match.replaceExp(replacement); + if (DEBUG) { + console.log("Rewrite: " + [oldExp, exp].map(ast.decodeExpression).join(' => ')); + } } return exp; } @@ -231,10 +240,22 @@ function equivalent(pattern: ast.Exp, paramNames?: string[], params: ast.ExpParams = {} ): boolean { + if (DEBUG) { + console.log([pattern, exp].map(ast.decodeExpression).join(' ~ ') + ' with {' + + Object.keys(params).map((name) => { + return name + ': ' + ast.decodeExpression(params[name]); + }).join(', ') + + '}' + ); + } + if (paramNames !== undefined && pattern.type === 'var') { let name = ( pattern).name; if (util.arrayIncludes(paramNames, name)) { if (params[name] === undefined) { + if (DEBUG) { + console.log(name + ' := ' + ast.decodeExpression(exp)); + } params[name] = ast.copyExp(exp); return true; } else { @@ -385,6 +406,8 @@ function equivalent(pattern: ast.Exp, } export let simplifyRewriter = new util.MultiFunctor([ + "(_x) !!_x => _x", + "(_a, _x) _a && _a && _x => _a && _x", "(_a, _x) _a || _a || _x => _a || _x", @@ -394,5 +417,9 @@ export let simplifyRewriter = new util.MultiFunctor([ "(_x) false && _x => false", "(_a, _b) !(_a != _b) => _a == _b", - "(_x) !!_x => _x", + + "(_a, _x) _a && !_a && _x => false", + "(_a, _x) _a || !_a || _x => true", + + "(_a, _x1, _x2) _a && _x1 || _a && _x2 => _a && (_x1 || _x2)", ].map(Rewriter.fromDescriptor)); diff --git a/src/ast.ts b/src/ast.ts index fb0fdb2..57105c5 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -393,6 +393,15 @@ function leftAssociateGen(opType: string, identityValue: ExpValue, zeroValue: Ex }; } +export function flattenOp(exp: Exp): Exp { + let expOp = exp; + if (expOp.type !== 'op' || (expOp.op !== '||' && expOp.op !== '&&')) { + return exp; + } + + return op(expOp.op, flatten(expOp.op, expOp)); +} + // Flatten the top level tree of op into a single flat array of expressions. export function flatten(opType: string, exp: Exp, flat?: Exp[]): Exp[] { var i; @@ -413,7 +422,7 @@ export function flatten(opType: string, exp: Exp, flat?: Exp[]): Exp[] { return flat; } -export function op(opType, args): ExpOp { +export function op(opType: string, args: Exp[]): ExpOp { return { type: 'op', // This is (multi-argument) operator. valueType: 'Any', diff --git a/src/rules-parser.pegjs b/src/rules-parser.pegjs index 12210aa..6352bed 100644 --- a/src/rules-parser.pegjs +++ b/src/rules-parser.pegjs @@ -389,7 +389,7 @@ LogicalANDExpression = return head; } tail.unshift(head); - return ast.op('&&', ast.flatten('&&', ast.op("&&", tail))); + return ast.flattenOp(ast.op("&&", tail)); } LogicalANDOperator = ("&&" / "and") { return "&&"; } @@ -401,7 +401,7 @@ LogicalORExpression = return head; } tail.unshift(head); - return ast.op('||', ast.flatten('||', ast.op("||", tail))); + return ast.flattenOp(ast.op("||", tail)); } LogicalOROperator = ("||" / "or") { return "||"; } diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 89a923f..7382610 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -251,6 +251,8 @@ suite("AST Matching", function() { [ "a && b && c && a && e", "a && b && c && e"], [ "a && b && c && d && a", "a && b && c && d"], +// [ "/* bug */ a || !!(a || c)", "a || c" ], + [ "hang && b || c && d && e", "hang && b || c && d && e" ], @@ -264,6 +266,14 @@ suite("AST Matching", function() { [ "!!a", "a" ], [ "!!!a", "!a" ], [ "!!!!a", "a" ], + + [ "a || !a", "true" ], + [ "a && !a", "false" ], + + [ "a && b || a && c", "a && (b || c)" ], + [ "a && b && c || a && b && d", "a && b && (c || d)" ], + [ "a && b && d || c && a && e", "a && (b && d || c && e)" ], +// [ "/* bug */ b && d && a || c && a && e", "a && (b && d || c && e)" ], ]; helper.dataDrivenTest(tests, function(data, expect) { From 23725b15432304f6d47a5fa48d08b7cf16d373e7 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 20 Dec 2015 17:13:24 -0800 Subject: [PATCH 34/35] Fix decoding of OR exp beneath AND exp. --- src/ast-matcher.ts | 4 ++-- src/ast.ts | 25 +++++++++++++------------ src/test/ast-test.ts | 3 +++ src/test/matcher-test.ts | 4 +++- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 98dffbd..244388b 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -92,14 +92,14 @@ export class Match { return replacement; } let parentPart = this.path.slice(-1)[0]; + let parentBool = parentPart.exp; ast.setChild(replacement, parentPart.exp, parentPart.index); // When a boolean expression is collapsed to a single argument - hoist the argument // to the parent. - let parentBool = parentPart.exp; if (parentBool.type === 'op' && (parentBool.op === '&&' || parentBool.op === '||') && parentBool.args.length === 1) { - this.path.slice(-1)[0].exp = ( parentPart.exp).args[0]; + parentPart.exp = parentBool.args[0]; } return this.path[0].exp; } diff --git a/src/ast.ts b/src/ast.ts index 57105c5..7d545dc 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -393,13 +393,15 @@ function leftAssociateGen(opType: string, identityValue: ExpValue, zeroValue: Ex }; } +// Mutates exp so nested boolean expressions are flattened. export function flattenOp(exp: Exp): Exp { let expOp = exp; if (expOp.type !== 'op' || (expOp.op !== '||' && expOp.op !== '&&')) { return exp; } - return op(expOp.op, flatten(expOp.op, expOp)); + expOp.args = flatten(expOp.op, expOp); + return expOp; } // Flatten the top level tree of op into a single flat array of expressions. @@ -652,21 +654,19 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string { decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' + decodeExpression(expOp.args[1], innerPrecedence) + ' : ' + decodeExpression(expOp.args[2], innerPrecedence); - } else if (expOp.args.length === 2) { + } else { // All ops are left associative - so nudge the innerPrecendence // down on the right hand side to force () for right-associating // operations (but ignore right-associating && and || since // short-circuiting makes it moot). - let nudge = 1; - if (rep === '&&' || rep === '||') { - nudge = 0; - } - result = - decodeExpression(expOp.args[0], innerPrecedence) + - ' ' + rep + ' ' + - decodeExpression(expOp.args[1], innerPrecedence + nudge); - } else { - result = expOp.args.map(decodeExpression).join(' ' + rep + ' '); + let nudge = 0; + result = expOp.args.map((term) => { + let innerResult = decodeExpression(term, innerPrecedence + nudge); + if (rep !== '&&' && rep !== '||') { + nudge = 1; + } + return innerResult; + }).join(' ' + rep + ' '); } break; @@ -824,6 +824,7 @@ export function setChild(expChild: Exp, expParent: Exp, index: number) { } else { expOp.args[index] = expChild; } + expParent = flattenOp(expParent); break; case 'union': diff --git a/src/test/ast-test.ts b/src/test/ast-test.ts index 647a6e0..664814a 100644 --- a/src/test/ast-test.ts +++ b/src/test/ast-test.ts @@ -192,6 +192,7 @@ suite("Abstract Syntax Tree (AST)", function() { [ "(a + b) + c", 'a + b + c' ], [ "a + b * c" ], [ "(a + b) * c" ], + [ "a * b * (d + d)" ], [ "a < 7" ], [ "a > 7" ], [ "a <= 7" ], @@ -208,6 +209,8 @@ suite("Abstract Syntax Tree (AST)", function() { // Converts right-associatvie to left-associative for && and || [ "a && (b && c)", "a && b && c" ], [ "a || (b || c)", "a || b || c" ], + [ "a && b && (c || d)" ], + [ "a || b || (c && d)", "a || b || c && d" ], [ "a && b || c && d" ], [ "(a || b) && (c || d)" ], [ "a ? b : c", ], diff --git a/src/test/matcher-test.ts b/src/test/matcher-test.ts index 7382610..469c72a 100644 --- a/src/test/matcher-test.ts +++ b/src/test/matcher-test.ts @@ -251,7 +251,7 @@ suite("AST Matching", function() { [ "a && b && c && a && e", "a && b && c && e"], [ "a && b && c && d && a", "a && b && c && d"], -// [ "/* bug */ a || !!(a || c)", "a || c" ], + [ "a || !!(a || c)", "a || c" ], [ "hang && b || c && d && e", "hang && b || c && d && e" ], @@ -271,7 +271,9 @@ suite("AST Matching", function() { [ "a && !a", "false" ], [ "a && b || a && c", "a && (b || c)" ], + [ "a && b && (c || d)", "a && b && (c || d)" ], [ "a && b && c || a && b && d", "a && b && (c || d)" ], + [ "a && b && d || c && a && e", "a && (b && d || c && e)" ], // [ "/* bug */ b && d && a || c && a && e", "a && (b && d || c && e)" ], ]; From e7e163eb7ff0463f15ddcc4b5bee0beb1dc6bcd9 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Sun, 20 Dec 2015 19:39:28 -0800 Subject: [PATCH 35/35] Iterator interface. --- src/ast-matcher.ts | 8 ++++---- src/permutation.ts | 36 +++++++++++++++++++++++------------- src/test/permutation-test.ts | 8 ++++---- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/ast-matcher.ts b/src/ast-matcher.ts index 244388b..1db771f 100644 --- a/src/ast-matcher.ts +++ b/src/ast-matcher.ts @@ -274,9 +274,9 @@ function equivalent(pattern: ast.Exp, return false; } for (let p = new IndexPermutation(patternCount); - p.getCurrent() != null; + p.current() != null; p.next()) { - let indexes = p.getCurrent(); + let indexes = p.current(); let tempParams = util.extend({}, params); let i: number; for (i = 0; i < patternCount; i++) { @@ -359,10 +359,10 @@ function equivalent(pattern: ast.Exp, } let p: Permutation; for (p = new Permutation(expOp.args, argCount); - p.getCurrent() != null; + p.current() != null; p.next()) { let tempParams = util.extend({}, params); - var args = p.getCurrent(); + var args = p.current(); let i: number; for (i = 0; i < args.length; i++) { if (!equivalent(patternOp.args[i], args[i], paramNames, tempParams)) { diff --git a/src/permutation.ts b/src/permutation.ts index eb70537..3455248 100644 --- a/src/permutation.ts +++ b/src/permutation.ts @@ -16,8 +16,18 @@ * limitations under the License. */ -export class IndexPermutation { - private current: number[] = []; +/* + * Usage: + * + * for(i = new Iterator(); i.current(); t.next()) { ... } + */ +export interface Iterator { + current(): T; + next(): void; +} + +export class IndexPermutation implements Iterator { + private values: number[] = []; private locations: number[] = []; private remaining: number; @@ -42,18 +52,18 @@ export class IndexPermutation { return count; } - getCurrent(): number[] { - if (this.current === null) { + current(): number[] { + if (this.values === null) { return null; } - return this.current.slice(); + return this.values.slice(); } next() { if (this.remaining === 0) { - this.current = null; + this.values = null; } - if (this.current === null) { + if (this.values === null) { return; } this.advance(); @@ -62,7 +72,7 @@ export class IndexPermutation { private advance() { let location = this.k - 1; for (; location >= 0; location--) { - let value = this.nextValue(location, this.current[location] + 1); + let value = this.nextValue(location, this.values[location] + 1); this.set(location, value); if (value !== undefined) { break; @@ -75,11 +85,11 @@ export class IndexPermutation { } private set(location: number, value?: number) { - let oldValue = this.current[location]; + let oldValue = this.values[location]; if (oldValue !== undefined) { this.locations[oldValue] = undefined; } - this.current[location] = value; + this.values[location] = value; if (value !== undefined) { this.locations[value] = location; } @@ -95,7 +105,7 @@ export class IndexPermutation { } } -export class Permutation { +export class Permutation implements Iterator { collection: T[]; p: IndexPermutation; @@ -108,8 +118,8 @@ export class Permutation { return this.p.getCount(); } - getCurrent(): T[] { - let indexes = this.p.getCurrent(); + current(): T[] { + let indexes = this.p.current(); if (indexes === null) { return null; } diff --git a/src/test/permutation-test.ts b/src/test/permutation-test.ts index 027cf0f..9c0e419 100644 --- a/src/test/permutation-test.ts +++ b/src/test/permutation-test.ts @@ -53,8 +53,8 @@ suite("Permutations", () => { let p = new IndexPermutation(data[0], data[1]); assert.equal(p.getCount(), expect.count); let results = []; - while (p.getCurrent() !== null) { - results.push(p.getCurrent()); + while (p.current() !== null) { + results.push(p.current()); p.next(); } assert.deepEqual(results, expect.values); @@ -81,8 +81,8 @@ suite("Permutations", () => { let p = new Permutation(data.c, data.k); assert.equal(p.getCount(), expect.count); let results = []; - while (p.getCurrent() !== null) { - results.push(p.getCurrent()); + while (p.current() !== null) { + results.push(p.current()); p.next(); } assert.deepEqual(results, expect.values);