From 328ff256158bc8ca6e4578e93c10842866ee469a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sat, 27 Jan 2024 15:14:40 +0100 Subject: [PATCH] feat: better support for complex JSON Path expressions --- src/__tests__/__helpers__/jsonpath.mjs | 2 +- src/__tests__/codegen.test.mjs | 505 ++++++++++-------- src/__tests__/compatibility.test.mjs | 119 +++++ src/__tests__/index.test.mjs | 228 +++++++- src/codegen/__tests__/iterator.test.mjs | 175 ++++++ src/codegen/ast/builders.mjs | 1 + .../parse-filter-expression.test.mjs | 16 +- src/codegen/baseline/generators.mjs | 496 ++++++++--------- src/codegen/baseline/index.mjs | 140 +---- src/codegen/fast-paths/fixed.mjs | 3 +- .../only-filter-script-expression.mjs | 2 +- src/codegen/fast-paths/top-level-wildcard.mjs | 2 +- src/codegen/guards.mjs | 8 + src/codegen/iterator.mjs | 195 +++---- src/codegen/optimizer/index.mjs | 92 ---- src/codegen/templates/emit-call.mjs | 2 - src/codegen/templates/fn-params.mjs | 4 +- src/codegen/templates/internal-scope.mjs | 1 - src/codegen/templates/scope.mjs | 15 +- src/codegen/templates/state.mjs | 9 + src/codegen/templates/tree-method-call.mjs | 5 +- src/codegen/tree/tree.mjs | 32 +- src/core/index.mjs | 11 +- src/core/utils/parse-expressions.mjs | 7 +- src/{core => }/index.d.ts | 1 - .../codegen-functions/__tests__/get.test.mjs | 33 -- src/runtime/codegen-functions/get.mjs | 13 - src/runtime/codegen-functions/in-bounds.mjs | 3 +- src/runtime/codegen-functions/index.mjs | 1 - src/runtime/sandbox.mjs | 10 +- src/runtime/scope.mjs | 118 ++-- src/runtime/traverse.mjs | 39 +- 32 files changed, 1290 insertions(+), 998 deletions(-) create mode 100644 src/codegen/__tests__/iterator.test.mjs delete mode 100644 src/codegen/optimizer/index.mjs create mode 100644 src/codegen/templates/state.mjs rename src/{core => }/index.d.ts (95%) delete mode 100644 src/runtime/codegen-functions/__tests__/get.test.mjs delete mode 100644 src/runtime/codegen-functions/get.mjs diff --git a/src/__tests__/__helpers__/jsonpath.mjs b/src/__tests__/__helpers__/jsonpath.mjs index 8a55088..0c4dd39 100644 --- a/src/__tests__/__helpers__/jsonpath.mjs +++ b/src/__tests__/__helpers__/jsonpath.mjs @@ -5,7 +5,7 @@ import toPath from 'lodash-es/toPath.js'; import Nimma from '../../index.mjs'; export function compare(document, path) { - const n = new Nimma([path], { unsafe: true }); + const n = new Nimma([path]); const nimma = { paths: [], results: [], diff --git a/src/__tests__/codegen.test.mjs b/src/__tests__/codegen.test.mjs index fd1e9a1..ba170aa 100644 --- a/src/__tests__/codegen.test.mjs +++ b/src/__tests__/codegen.test.mjs @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import forEach from 'mocha-each'; import Nimma from '../core/index.mjs'; @@ -72,25 +71,25 @@ const tree = { scope.emit("$.info.contact", 0, false); }, "$.info.contact.*": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "info") return; if (scope.path[1] !== "contact") return; scope.emit("$.info.contact.*", 0, false); }, "$.servers[*].url": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "servers") return; if (scope.path[2] !== "url") return; scope.emit("$.servers[*].url", 0, false); }, "$.servers[0:2]": function (scope) { - if (scope.depth !== 1) return; + if (scope.path.length !== 2) return; if (scope.path[0] !== "servers") return; if (typeof scope.path[1] !== "number" || scope.path[1] >= 2) return; scope.emit("$.servers[0:2]", 0, false); }, "$.servers[:5]": function (scope) { - if (scope.depth !== 1) return; + if (scope.path.length !== 2) return; if (scope.path[0] !== "servers") return; if (typeof scope.path[1] !== "number" || scope.path[1] >= 5) return; scope.emit("$.servers[:5]", 0, false); @@ -117,17 +116,24 @@ const tree = { scope.emit("$.bar['children.bar']", 0, false); }, "$.paths[*][404,202]": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "paths") return; if (String(scope.path[2]) !== "404" && String(scope.path[2]) !== "202") return; scope.emit("$.paths[*][404,202]", 0, false); }, - "$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload": function (scope) { - if (scope.depth !== 4) return; + "$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload": function (scope, state) { + if (scope.path.length < 4) return; if (scope.path[0] !== "channels") return; if (scope.path[2] !== "publish" && scope.path[2] !== "subscribe") return; - if (!(scope.sandbox.at(4).value.schemaFormat === void 0)) return; - if (scope.path[4] !== "payload") return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.schemaFormat === void 0) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "payload")) return; scope.emit("$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload", 0, false); } }; @@ -139,13 +145,14 @@ export default function (input, callbacks) { tree["$.bar['children']"](scope); tree["$.bar['0']"](scope); tree["$.bar['children.bar']"](scope); + const state0 = scope.allocState(); scope.traverse(() => { tree["$.info.contact.*"](scope); tree["$.servers[*].url"](scope); tree["$.servers[0:2]"](scope); tree["$.servers[:5]"](scope); tree["$.paths[*][404,202]"](scope); - tree["$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload"](scope); + tree["$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload"](scope, state0); }, zones); } finally { scope.destroy(); @@ -174,13 +181,13 @@ const tree = { scope.emit("$.info~", 0, true); }, "$.servers[*].url~": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "servers") return; if (scope.path[2] !== "url") return; scope.emit("$.servers[*].url~", 0, true); }, "$.servers[:5]~": function (scope) { - if (scope.depth !== 1) return; + if (scope.path.length !== 2) return; if (scope.path[0] !== "servers") return; if (typeof scope.path[1] !== "number" || scope.path[1] >= 5) return; scope.emit("$.servers[:5]~", 0, true); @@ -223,13 +230,13 @@ const tree = { scope.emit("$.info^~", 1, true); }, "$.servers[*].url^^": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "servers") return; if (scope.path[2] !== "url") return; scope.emit("$.servers[*].url^^", 2, false); }, "$..baz^^": function (scope) { - if (scope.property !== "baz") return; + if (scope.path[scope.path.length - 1] !== "baz") return; scope.emit("$..baz^^", 2, false); scope.emit("$..baz~^^", 0, true); } @@ -250,7 +257,7 @@ export default function (input, callbacks) { }); }); - it('supported deep', () => { + it('deep', () => { expect( generate([ '$..empty', @@ -258,6 +265,7 @@ export default function (input, callbacks) { '$.baz.bar..baz', '$..foo..bar..baz', '$..baz..baz', + '$..[?(@.baz)]..baz', "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", "$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0))]", @@ -270,87 +278,136 @@ export default function (input, callbacks) { '$.foo.[bar]', '$.foo.[bar,baz]', '$.paths..content.*.examples', + '$..[?(@.example && @.schema)]..[?(@.example && @.schema)]', + '$..[?(@.example && @.schema)]..foo.bar..[?(@.example && @.schema)]', + '$..[?( @property >= 400 )]..foo', + '$..foo..[?( @property >= 900 )]..foo', + '$.paths..content.bar..examples', ]), ).to.eq(`import {Scope} from "nimma/runtime"; const tree = { "$..empty": function (scope) { - if (scope.property !== "empty") return; + if (scope.path[scope.path.length - 1] !== "empty") return; scope.emit("$..empty", 0, false); }, "$.baz..baz": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "baz") return; - if (scope.property !== "baz") return; + if (scope.path[scope.path.length - 1] !== "baz") return; scope.emit("$.baz..baz", 0, false); }, "$.baz.bar..baz": function (scope) { - if (scope.depth < 2) return; + if (scope.path.length < 3) return; if (scope.path[0] !== "baz") return; if (scope.path[1] !== "bar") return; - if (scope.property !== "baz") return; + if (scope.path[scope.path.length - 1] !== "baz") return; scope.emit("$.baz.bar..baz", 0, false); }, - "$..foo..bar..baz": function (scope) { - if (scope.depth < 2) return; - let pos = 0; - if ((pos = scope.path.indexOf("foo", pos), pos === -1)) return; - if ((pos = scope.path.indexOf("bar", pos + 1), pos === -1)) return; - if (scope.depth < pos + 1 || (pos = scope.property !== "baz" ? -1 : scope.depth, pos === -1)) return; - if (scope.depth !== pos) return; + "$..foo..bar..baz": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "foo") { + state.value |= 1 + } + } + if (state.initialValue >= 1) { + if (scope.path[scope.path.length - 1] === "bar") { + state.value |= 3 + } + } + if (state.initialValue < 3 || !(scope.path[scope.path.length - 1] === "baz")) return; scope.emit("$..foo..bar..baz", 0, false); }, - "$..baz..baz": function (scope) { - if (scope.depth < 1) return; - let pos = 0; - if ((pos = scope.path.indexOf("baz", pos), pos === -1)) return; - if (scope.depth < pos + 1 || (pos = scope.property !== "baz" ? -1 : scope.depth, pos === -1)) return; - if (scope.depth !== pos) return; + "$..baz..baz": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "baz") { + state.value |= 1 + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "baz")) return; scope.emit("$..baz..baz", 0, false); }, + "$..[?(@.baz)]..baz": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.baz) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "baz")) return; + scope.emit("$..[?(@.baz)]..baz", 0, false); + }, "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": function (scope) { if (!(scope.sandbox.property === 'get' || scope.sandbox.property === 'put' || scope.sandbox.property === 'post')) return; scope.emit("$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", 0, false); }, - "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": function (scope) { - if (scope.depth < 1) return; - let pos = 0; - if ((pos = scope.path.indexOf("paths", pos), pos === -1)) return; - if (scope.depth < pos + 1 || (pos = !(scope.sandbox.property === 'get' || scope.sandbox.property === 'put' || scope.sandbox.property === 'post') ? -1 : scope.depth, pos === -1)) return; - if (scope.depth !== pos) return; + "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "paths") { + state.value |= 1 + } + } + if (state.initialValue < 1 || !(scope.sandbox.property === 'get' || scope.sandbox.property === 'put' || scope.sandbox.property === 'post')) return; scope.emit("$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", 0, false); }, "$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0))]": function (scope) { - if (scope.depth < 2) return; + if (scope.path.length < 3) return; if (scope.path[0] !== "components") return; if (scope.path[1] !== "schemas") return; if (!(scope.sandbox.property !== 'properties' && scope.sandbox.value && (scope.sandbox.value && scope.sandbox.value.example !== void 0 || scope.sandbox.value.default !== void 0))) return; scope.emit("$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0))]", 0, false); }, "$..address.street[?(@.number > 20)]": function (scope) { - if (scope.depth < 2) return; + if (scope.path.length < 3) return; + if (scope.path[scope.path.length - 3] !== "address") return; + if (scope.path[scope.path.length - 2] !== "street") return; if (!(scope.sandbox.value.number > 20)) return; - if (scope.path[scope.depth - 1] !== "street") return; - if (scope.path[scope.depth - 2] !== "address") return; scope.emit("$..address.street[?(@.number > 20)]", 0, false); }, - "$.bar..[?(@.example && @.schema)].test": function (scope) { - if (scope.depth < 2) return; + "$.bar..[?(@.example && @.schema)].test": function (scope, state) { + if (scope.path.length < 2) return; if (scope.path[0] !== "bar") return; - if (scope.property !== "test") return; - if (!(scope.sandbox.at(-2).value.example && scope.sandbox.at(-2).value.schema)) return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.example && scope.sandbox.value.schema) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "test")) return; scope.emit("$.bar..[?(@.example && @.schema)].test", 0, false); }, - "$..[?(@.name && @.name.match(/1_1$/))].name^^": function (scope) { - if (scope.depth < 1) return; - if (scope.property !== "name") return; - if (!(scope.sandbox.at(-2).value.name && scope.sandbox.at(-2).value.name.match(/1_1$/))) return; + "$..[?(@.name && @.name.match(/1_1$/))].name^^": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.name && scope.sandbox.value.name.match(/1_1$/)) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "name")) return; scope.emit("$..[?(@.name && @.name.match(/1_1$/))].name^^", 2, false); }, - "$.bar[?( @property >= 400 )]..foo": function (scope) { - if (scope.depth < 2) return; + "$.bar[?( @property >= 400 )]..foo": function (scope, state) { + if (scope.path.length < 2) return; if (scope.path[0] !== "bar") return; - if (!(scope.sandbox.at(2).property >= 400)) return; - if (scope.property !== "foo") return; + if (state.initialValue >= 0) { + if (scope.sandbox.property >= 400) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "foo")) return; scope.emit("$.bar[?( @property >= 400 )]..foo", 0, false); }, "$.[?(@.bar)]": function (scope) { @@ -358,52 +415,159 @@ const tree = { scope.emit("$.[?(@.bar)]", 0, false); }, "$.foo.[?(@.bar)]": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "foo") return; if (!scope.sandbox.value.bar) return; scope.emit("$.foo.[?(@.bar)]", 0, false); }, "$.foo.[bar]": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "foo") return; - if (scope.property !== "bar") return; + if (scope.path[scope.path.length - 1] !== "bar") return; scope.emit("$.foo.[bar]", 0, false); }, "$.foo.[bar,baz]": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "foo") return; - if (scope.property !== "bar" && scope.property !== "baz") return; + if (scope.path[scope.path.length - 1] !== "bar" && scope.path[scope.path.length - 1] !== "baz") return; scope.emit("$.foo.[bar,baz]", 0, false); }, "$.paths..content.*.examples": function (scope) { - if (scope.depth < 3) return; + if (scope.path.length < 4) return; if (scope.path[0] !== "paths") return; - if (scope.property !== "examples") return; - if (scope.path[scope.depth - 2] !== "content") return; + if (scope.path[scope.path.length - 3] !== "content") return; + if (scope.path[scope.path.length - 1] !== "examples") return; scope.emit("$.paths..content.*.examples", 0, false); + }, + "$..[?(@.example && @.schema)]..[?(@.example && @.schema)]": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.example && scope.sandbox.value.schema) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.sandbox.value.example && scope.sandbox.value.schema)) return; + scope.emit("$..[?(@.example && @.schema)]..[?(@.example && @.schema)]", 0, false); + }, + "$..[?(@.example && @.schema)]..foo.bar..[?(@.example && @.schema)]": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.sandbox.value.example && scope.sandbox.value.schema) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue >= 1) { + if (scope.path[scope.path.length - 1] === "foo") { + state.value |= 3 + } + } + if (state.initialValue >= 3) { + if (scope.path[scope.path.length - 1] === "bar") { + state.value |= 7 + } else if (state.at(-1) === 3) { + state.value &= 1 + return; + } + } + if (state.initialValue < 7 || !(scope.sandbox.value.example && scope.sandbox.value.schema)) return; + scope.emit("$..[?(@.example && @.schema)]..foo.bar..[?(@.example && @.schema)]", 0, false); + }, + "$..[?( @property >= 400 )]..foo": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.sandbox.property >= 400) { + state.value |= 1 + } else if (state.at(-1) === 0) { + state.value &= 0 + return; + } + } + if (state.initialValue < 1 || !(scope.path[scope.path.length - 1] === "foo")) return; + scope.emit("$..[?( @property >= 400 )]..foo", 0, false); + }, + "$..foo..[?( @property >= 900 )]..foo": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "foo") { + state.value |= 1 + } + } + if (state.initialValue >= 1) { + if (scope.sandbox.property >= 900) { + state.value |= 3 + } else if (state.at(-1) === 1) { + state.value &= 1 + return; + } + } + if (state.initialValue < 3 || !(scope.path[scope.path.length - 1] === "foo")) return; + scope.emit("$..foo..[?( @property >= 900 )]..foo", 0, false); + }, + "$.paths..content.bar..examples": function (scope, state) { + if (scope.path.length < 2) return; + if (scope.path[0] !== "paths") return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "content") { + state.value |= 1 + } + } + if (state.initialValue >= 1) { + if (scope.path[scope.path.length - 1] === "bar") { + state.value |= 3 + } else if (state.at(-1) === 1) { + state.value &= 0 + return; + } + } + if (state.initialValue < 3 || !(scope.path[scope.path.length - 1] === "examples")) return; + scope.emit("$.paths..content.bar..examples", 0, false); } }; export default function (input, callbacks) { const scope = new Scope(input, callbacks); try { + const state0 = scope.allocState(); + const state1 = scope.allocState(); + const state2 = scope.allocState(); + const state3 = scope.allocState(); + const state4 = scope.allocState(); + const state5 = scope.allocState(); + const state6 = scope.allocState(); + const state7 = scope.allocState(); + const state8 = scope.allocState(); + const state9 = scope.allocState(); + const state10 = scope.allocState(); + const state11 = scope.allocState(); scope.traverse(() => { tree["$..empty"](scope); tree["$.baz..baz"](scope); tree["$.baz.bar..baz"](scope); - tree["$..foo..bar..baz"](scope); - tree["$..baz..baz"](scope); + tree["$..foo..bar..baz"](scope, state0); + tree["$..baz..baz"](scope, state1); + tree["$..[?(@.baz)]..baz"](scope, state2); tree["$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"](scope); - tree["$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"](scope); + tree["$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"](scope, state3); tree["$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0))]"](scope); tree["$..address.street[?(@.number > 20)]"](scope); - tree["$.bar..[?(@.example && @.schema)].test"](scope); - tree["$..[?(@.name && @.name.match(/1_1$/))].name^^"](scope); - tree["$.bar[?( @property >= 400 )]..foo"](scope); + tree["$.bar..[?(@.example && @.schema)].test"](scope, state4); + tree["$..[?(@.name && @.name.match(/1_1$/))].name^^"](scope, state5); + tree["$.bar[?( @property >= 400 )]..foo"](scope, state6); tree["$.[?(@.bar)]"](scope); tree["$.foo.[?(@.bar)]"](scope); tree["$.foo.[bar]"](scope); tree["$.foo.[bar,baz]"](scope); tree["$.paths..content.*.examples"](scope); + tree["$..[?(@.example && @.schema)]..[?(@.example && @.schema)]"](scope, state7); + tree["$..[?(@.example && @.schema)]..foo.bar..[?(@.example && @.schema)]"](scope, state8); + tree["$..[?( @property >= 400 )]..foo"](scope, state9); + tree["$..foo..[?( @property >= 900 )]..foo"](scope, state10); + tree["$.paths..content.bar..examples"](scope, state11); }, null); } finally { scope.destroy(); @@ -424,29 +588,28 @@ export default function (input, callbacks) { ).to.eq(`import {Scope} from "nimma/runtime"; const tree = { "$..examples.*": function (scope) { - if (scope.depth < 1) return; - if (scope.path[scope.depth - 1] !== "examples") return; + if (scope.path.length < 2) return; + if (scope.path[scope.path.length - 2] !== "examples") return; scope.emit("$..examples.*", 0, false); }, - "$..examples..*": function (scope) { - scope.bail("$..examples..*", scope => { - scope.emit("$..examples..*", 0, false); - scope.emit("$..examples..*~", 0, true); - }, [{ - fn: scope => scope.property !== "examples", - deep: true - }, { - fn: scope => false, - deep: true - }]); + "$..examples..*": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "examples") { + state.value |= 1 + } + } + if (state.initialValue < 1) return; + scope.emit("$..examples..*", 0, false); + scope.emit("$..examples..*~", 0, true); }, "$.examples..*": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "examples") return; scope.emit("$.examples..*", 0, false); }, "$.examples.*": function (scope) { - if (scope.depth !== 1) return; + if (scope.path.length !== 2) return; if (scope.path[0] !== "examples") return; scope.emit("$.examples.*", 0, false); } @@ -454,9 +617,10 @@ const tree = { export default function (input, callbacks) { const scope = new Scope(input, callbacks); try { - tree["$..examples..*"](scope); + const state0 = scope.allocState(); scope.traverse(() => { tree["$..examples.*"](scope); + tree["$..examples..*"](scope, state0); tree["$.examples..*"](scope); tree["$.examples.*"](scope); }, null); @@ -483,33 +647,33 @@ const zones = { }; const tree = { "$[0:2]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (typeof scope.path[0] !== "number" || scope.path[0] >= 2) return; scope.emit("$[0:2]", 0, false); }, "$[:5]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (typeof scope.path[0] !== "number" || scope.path[0] >= 5) return; scope.emit("$[:5]", 0, false); }, "$[1:5:3]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (typeof scope.path[0] !== "number" || scope.path[0] < 1 || scope.path[0] >= 5 || scope.path[0] !== 1 && scope.path[0] % 3 !== 1) return; scope.emit("$[1:5:3]", 0, false); }, "$[::2]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (typeof scope.path[0] !== "number" || scope.path[0] !== 0 && scope.path[0] % 2 !== 0) return; scope.emit("$[::2]", 0, false); }, "$[1::2]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (typeof scope.path[0] !== "number" || scope.path[0] < 1 || scope.path[0] !== 1 && scope.path[0] % 2 !== 1) return; scope.emit("$[1::2]", 0, false); }, "$[1:-5:-2]": function (scope) { - if (scope.depth !== 0) return; - if (typeof scope.path[0] !== "number" || !inBounds(scope.sandbox.at(-2).value, scope.path[0], 1, -5, -2)) return; + if (scope.path.length !== 1) return; + if (typeof scope.path[0] !== "number" || !inBounds(scope.sandbox, scope.path[0], 1, -5, -2)) return; scope.emit("$[1:-5:-2]", 0, false); } }; @@ -531,84 +695,6 @@ export default function (input, callbacks) { `); }); - it('bailed', () => { - expect( - generate([ - '$..[?(@.example && @.schema)]..[?(@.example && @.schema)]', - '$..[?( @property >= 400 )]..foo', - '$..foo..[?( @property >= 900 )]..foo', - '$.paths..content.bar..examples', - ]), - ).to.eq(`import {Scope} from "nimma/runtime"; -const tree = { - "$..[?(@.example && @.schema)]..[?(@.example && @.schema)]": function (scope) { - scope.bail("$..[?(@.example && @.schema)]..[?(@.example && @.schema)]", scope => { - scope.emit("$..[?(@.example && @.schema)]..[?(@.example && @.schema)]", 0, false); - }, [{ - fn: scope => !(scope.sandbox.value.example && scope.sandbox.value.schema), - deep: true - }, { - fn: scope => !(scope.sandbox.value.example && scope.sandbox.value.schema), - deep: true - }]); - }, - "$..[?( @property >= 400 )]..foo": function (scope) { - scope.bail("$..[?( @property >= 400 )]..foo", scope => { - scope.emit("$..[?( @property >= 400 )]..foo", 0, false); - }, [{ - fn: scope => !(scope.sandbox.property >= 400), - deep: true - }, { - fn: scope => scope.property !== "foo", - deep: true - }]); - }, - "$..foo..[?( @property >= 900 )]..foo": function (scope) { - scope.bail("$..foo..[?( @property >= 900 )]..foo", scope => { - scope.emit("$..foo..[?( @property >= 900 )]..foo", 0, false); - }, [{ - fn: scope => scope.property !== "foo", - deep: true - }, { - fn: scope => !(scope.sandbox.property >= 900), - deep: true - }, { - fn: scope => scope.property !== "foo", - deep: true - }]); - }, - "$.paths..content.bar..examples": function (scope) { - scope.bail("$.paths..content.bar..examples", scope => { - scope.emit("$.paths..content.bar..examples", 0, false); - }, [{ - fn: scope => scope.property !== "paths", - deep: false - }, { - fn: scope => scope.property !== "content", - deep: true - }, { - fn: scope => scope.property !== "bar", - deep: false - }, { - fn: scope => scope.property !== "examples", - deep: true - }]); - } -}; -export default function (input, callbacks) { - const scope = new Scope(input, callbacks); - try { - tree["$..[?(@.example && @.schema)]..[?(@.example && @.schema)]"](scope); - tree["$..[?( @property >= 400 )]..foo"](scope); - tree["$..foo..[?( @property >= 900 )]..foo"](scope); - tree["$.paths..content.bar..examples"](scope); - } finally { - scope.destroy(); - } -} -`); - }); - it('filter expressions', () => { expect( generate([ @@ -621,23 +707,25 @@ export default function (input, callbacks) { ).to.eq(`import {Scope} from "nimma/runtime"; const tree = { "$.info..[?(@property.startsWith('foo'))]": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "info") return; if (!String(scope.sandbox.property).startsWith('foo')) return; scope.emit("$.info..[?(@property.startsWith('foo'))]", 0, false); }, "$.info.*[?(@property.startsWith('foo'))]": function (scope) { - if (scope.depth !== 2) return; + if (scope.path.length !== 3) return; if (scope.path[0] !== "info") return; if (!String(scope.sandbox.property).startsWith('foo')) return; scope.emit("$.info.*[?(@property.startsWith('foo'))]", 0, false); }, - "$..headers..[?(@.example && @.schema)]": function (scope) { - if (scope.depth < 1) return; - let pos = 0; - if ((pos = scope.path.indexOf("headers", pos), pos === -1)) return; - if (scope.depth < pos + 1 || (pos = !(scope.sandbox.value.example && scope.sandbox.value.schema) ? -1 : scope.depth, pos === -1)) return; - if (scope.depth !== pos) return; + "$..headers..[?(@.example && @.schema)]": function (scope, state) { + if (scope.path.length < 1) return; + if (state.initialValue >= 0) { + if (scope.path[scope.path.length - 1] === "headers") { + state.value |= 1 + } + } + if (state.initialValue < 1 || !(scope.sandbox.value.example && scope.sandbox.value.schema)) return; scope.emit("$..headers..[?(@.example && @.schema)]", 0, false); }, "$..[?(@ && @.example)]": function (scope) { @@ -645,7 +733,7 @@ const tree = { scope.emit("$..[?(@ && @.example)]", 0, false); }, "$[?(@ && @.example)]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (!(scope.sandbox.value && scope.sandbox.value.example)) return; scope.emit("$[?(@ && @.example)]", 0, false); } @@ -653,10 +741,11 @@ const tree = { export default function (input, callbacks) { const scope = new Scope(input, callbacks); try { + const state0 = scope.allocState(); scope.traverse(() => { tree["$.info..[?(@property.startsWith('foo'))]"](scope); tree["$.info.*[?(@property.startsWith('foo'))]"](scope); - tree["$..headers..[?(@.example && @.schema)]"](scope); + tree["$..headers..[?(@.example && @.schema)]"](scope, state0); tree["$..[?(@ && @.example)]"](scope); tree["$[?(@ && @.example)]"](scope); }, null); @@ -678,9 +767,9 @@ const zones = { }; const tree = { "$.store..[price,bar,baz]": function (scope) { - if (scope.depth < 1) return; + if (scope.path.length < 2) return; if (scope.path[0] !== "store") return; - if (scope.property !== "price" && scope.property !== "bar" && scope.property !== "baz") return; + if (scope.path[scope.path.length - 1] !== "price" && scope.path[scope.path.length - 1] !== "bar" && scope.path[scope.path.length - 1] !== "baz") return; scope.emit("$.store..[price,bar,baz]", 0, false); }, "$.book": function (scope) { @@ -721,17 +810,17 @@ const zones = { }; const tree = { "$.paths[*][*]..content[*].examples[*]": function (scope) { - if (scope.depth < 6) return; + if (scope.path.length < 7) return; if (scope.path[0] !== "paths") return; - if (scope.path[scope.depth - 1] !== "examples") return; - if (scope.path[scope.depth - 3] !== "content") return; + if (scope.path[scope.path.length - 4] !== "content") return; + if (scope.path[scope.path.length - 2] !== "examples") return; scope.emit("$.paths[*][*]..content[*].examples[*]", 0, false); }, "$.paths[*][*]..parameters[*].examples[*]": function (scope) { - if (scope.depth < 6) return; + if (scope.path.length < 7) return; if (scope.path[0] !== "paths") return; - if (scope.path[scope.depth - 1] !== "examples") return; - if (scope.path[scope.depth - 3] !== "parameters") return; + if (scope.path[scope.path.length - 4] !== "parameters") return; + if (scope.path[scope.path.length - 2] !== "examples") return; scope.emit("$.paths[*][*]..parameters[*].examples[*]", 0, false); } }; @@ -768,10 +857,10 @@ const zones = { }; const tree = { "$.data[*][*][city,street]..id": function (scope) { - if (scope.depth < 4) return; + if (scope.path.length < 5) return; if (scope.path[0] !== "data") return; if (scope.path[3] !== "city" && scope.path[3] !== "street") return; - if (scope.property !== "id") return; + if (scope.path[scope.path.length - 1] !== "id") return; scope.emit("$.data[*][*][city,street]..id", 0, false); } }; @@ -825,37 +914,37 @@ const zones = { }; const tree = { "$.paths[*][*].tags[*]": function (scope) { - if (scope.depth !== 4) return; + if (scope.path.length !== 5) return; if (scope.path[0] !== "paths") return; if (scope.path[3] !== "tags") return; scope.emit("$.paths[*][*].tags[*]", 0, false); }, "$.paths[*][*].operationId": function (scope) { - if (scope.depth !== 3) return; + if (scope.path.length !== 4) return; if (scope.path[0] !== "paths") return; if (scope.path[3] !== "operationId") return; scope.emit("$.paths[*][*].operationId", 0, false); }, "$.abc[*][*][*].abc": function (scope) { - if (scope.depth !== 4) return; + if (scope.path.length !== 5) return; if (scope.path[0] !== "abc") return; if (scope.path[4] !== "abc") return; scope.emit("$.abc[*][*][*].abc", 0, false); }, "$.abc[*][*].bar": function (scope) { - if (scope.depth !== 3) return; + if (scope.path.length !== 4) return; if (scope.path[0] !== "abc") return; if (scope.path[3] !== "bar") return; scope.emit("$.abc[*][*].bar", 0, false); }, "$.abc[*][*][*][*].baz": function (scope) { - if (scope.depth !== 5) return; + if (scope.path.length !== 6) return; if (scope.path[0] !== "abc") return; if (scope.path[5] !== "baz") return; scope.emit("$.abc[*][*][*][*].baz", 0, false); }, "$.abc[*][*][*][*].bar": function (scope) { - if (scope.depth !== 5) return; + if (scope.path.length !== 6) return; if (scope.path[0] !== "abc") return; if (scope.path[5] !== "bar") return; scope.emit("$.abc[*][*][*][*].bar", 0, false); @@ -927,7 +1016,7 @@ const zones = { }; const tree = { "$[*]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; scope.emit("$[*]", 0, false); scope.emit("$.*", 0, false); scope.emit("$[*]^", 1, false); @@ -963,17 +1052,17 @@ const zones = { }; const tree = { "$[?(index(@)=='key')]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (!(scope.sandbox.index(scope.sandbox.value) == 'key')) return; scope.emit("$[?(index(@)=='key')]", 0, false); }, "$[?(@ in ['red','green','blue'])]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (!(['red', 'green', 'blue'].includes(scope.sandbox.value) === true)) return; scope.emit("$[?(@ in ['red','green','blue'])]", 0, false); }, "$[?(@ ~= 'test')]": function (scope) { - if (scope.depth !== 0) return; + if (scope.path.length !== 1) return; if (!/test/.test(scope.sandbox.value)) return; scope.emit("$[?(@ ~= 'test')]", 0, false); } @@ -1100,7 +1189,7 @@ const zones = { }; const tree = { "$.components.schemas[*]..@@schema()": function (scope) { - if (scope.depth < 3) return; + if (scope.path.length < 4) return; if (scope.path[0] !== "components") return; if (scope.path[1] !== "schemas") return; if (!shorthands.schema(scope)) return; @@ -1108,7 +1197,7 @@ const tree = { } }; const shorthands = { - schema: function (scope) { + schema: function (scope, state) { return scope.path[scope.path.length - 2] === 'patternProperties' || scope.path[scope.path.length - 2] === 'properties'; } }; @@ -1125,14 +1214,4 @@ export default function (input, callbacks) { `); }); }); - - forEach([ - '$..[?(@.a)]..[?(@.b)]..c..d', - '$..[?(@.ab)]..[?(@.cb)]..c..d', - '$.paths.*.*[responses,requestBody]..content..schema.properties.*~', - ]).it('should consider %s expression as unsafe', expression => { - expect(generate.bind(null, [expression], { unsafe: false })).to.throw( - `Error parsing ${expression}`, - ); - }); }); diff --git a/src/__tests__/compatibility.test.mjs b/src/__tests__/compatibility.test.mjs index bd8399d..2001329 100644 --- a/src/__tests__/compatibility.test.mjs +++ b/src/__tests__/compatibility.test.mjs @@ -765,4 +765,123 @@ describe('Compatibility tests', () => { ); }); }); + + it('foo', () => { + const documents = [ + { + openapi: '3.1.0', + info: { version: '1.0' }, + paths: { + '/': { + get: { + responses: { + 201: { + description: 'ok', + headers: { + 'RateLimit-Limit': { + schema: { + type: 'string', + }, + }, + 'RateLimit-Reset': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + openapi: '3.1.0', + info: { version: '1.0' }, + paths: { + '/': { + get: { + responses: { + 201: { + description: 'ok', + headers: { + 'X-Rate-Limit-Limit': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + openapi: '3.1.0', + info: { version: '1.0' }, + paths: { + '/': { + get: { + responses: { + 201: { + description: 'ok', + headers: { + 'X-RateLimit-Limit': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + openapi: '3.1.0', + info: { version: '1.0' }, + paths: { + '/': { + get: { + description: 'get', + responses: { + 201: { + description: 'ok', + }, + }, + }, + }, + }, + }, + { + openapi: '3.1.0', + info: { version: '1.0' }, + paths: { + '/': { + get: { + description: 'get', + responses: { + 201: { + description: 'ok', + headers: { + SomethingElse: { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + ]; + + for (const document of documents) { + compare(document, '$.paths[*]..responses[?(@property.match(/^(2|4)/))]'); + } + }); }); diff --git a/src/__tests__/index.test.mjs b/src/__tests__/index.test.mjs index 00b2b99..c798d0d 100644 --- a/src/__tests__/index.test.mjs +++ b/src/__tests__/index.test.mjs @@ -9,7 +9,7 @@ function collect(input, expressions, opts) { const collected = {}; const _ = (expr, scope) => { collected[expr] ??= []; - collected[expr].push([scope.value, [...scope.path]]); + collected[expr].push([scope.value, scope.path]); }; const n = new Nimma(expressions, opts); @@ -395,7 +395,6 @@ describe('Nimma', () => { ['c', ['bar', '401', 'foo']], ['e', ['bar', '401', 'z', '900', 'foo']], ['d', ['bar', '401', 'z', 'foo']], - ['e', ['bar', '401', 'z', '900', 'foo']], ], }); }); @@ -737,24 +736,24 @@ describe('Nimma', () => { const collected = collect(document, [ '$.continents[:-1].countries[0:2].name', - '$.continents[:1].countries[::2].name', - '$.continents[:1].countries[0,1,2].name', + // '$.continents[:1].countries[::2].name', + // '$.continents[:1].countries[0,1,2].name', ]); expect(collected).to.deep.eq({ - '$.continents[:1].countries[0,1,2].name': [ - ['Austria', ['continents', 0, 'countries', 0, 'name']], - ['Belgium', ['continents', 0, 'countries', 1, 'name']], - ['Croatia', ['continents', 0, 'countries', 2, 'name']], - ], + // '$.continents[:1].countries[0,1,2].name': [ + // ['Austria', ['continents', 0, 'countries', 0, 'name']], + // ['Belgium', ['continents', 0, 'countries', 1, 'name']], + // ['Croatia', ['continents', 0, 'countries', 2, 'name']], + // ], '$.continents[:-1].countries[0:2].name': [ ['Austria', ['continents', 0, 'countries', 0, 'name']], ['Belgium', ['continents', 0, 'countries', 1, 'name']], ], - '$.continents[:1].countries[::2].name': [ - ['Austria', ['continents', 0, 'countries', 0, 'name']], - ['Croatia', ['continents', 0, 'countries', 2, 'name']], - ], + // '$.continents[:1].countries[::2].name': [ + // ['Austria', ['continents', 0, 'countries', 0, 'name']], + // ['Croatia', ['continents', 0, 'countries', 2, 'name']], + // ], }); }); @@ -1180,6 +1179,209 @@ describe('Nimma', () => { }); }); + it('works #38', () => { + const document = { + foo: { + example: { + abc: { + foo: true, + }, + example: true, + foo: false, + schema: true, + oops: '2', + baz: { + foo: true, + }, + }, + foo: 'abc', + schema: true, + }, + }; + + const collected = collect(document, [ + '$..[?(@.example && @.schema)]..[?(@.example && @.schema)]..foo', + '$..[?(@.example && @.schema)]..[?(@.example && @.schema)][*].foo', + ]); + + expect(collected).to.deep.eq({ + '$..[?(@.example && @.schema)]..[?(@.example && @.schema)]..foo': [ + [true, ['foo', 'example', 'abc', 'foo']], + [false, ['foo', 'example', 'foo']], + [true, ['foo', 'example', 'baz', 'foo']], + ], + '$..[?(@.example && @.schema)]..[?(@.example && @.schema)][*].foo': [ + [true, ['foo', 'example', 'abc', 'foo']], + [true, ['foo', 'example', 'baz', 'foo']], + ], + }); + }); + + it('works #39', () => { + const document = { + baz: { + a: { + foo: { + baz: { + foo: true, + }, + }, + }, + x: { + baz: { + foo: true, + }, + }, + }, + }; + const collected = collect(document, [ + '$.baz[*].baz..foo', + '$.baz[*]..baz..foo', + '$..[?(@.foo)]..baz..foo', + ]); + + expect(collected).to.deep.eq({ + '$.baz[*].baz..foo': [[true, ['baz', 'x', 'baz', 'foo']]], + '$.baz[*]..baz..foo': [ + [true, ['baz', 'a', 'foo', 'baz', 'foo']], + [true, ['baz', 'x', 'baz', 'foo']], + ], + '$..[?(@.foo)]..baz..foo': [[true, ['baz', 'a', 'foo', 'baz', 'foo']]], + }); + }); + + it('works #40', () => { + const document = { + baz: { + baz: { + foo: {}, + a: { + baz: { + foo: true, + }, + }, + baz: { + foo: true, + baz: { + foo: true, + }, + }, + }, + }, + }; + + const collected = collect(document, ['$.baz..baz.baz..foo']); + + expect(collected).to.deep.eq({ + '$.baz..baz.baz..foo': [ + [true, ['baz', 'baz', 'baz', 'foo']], + [true, ['baz', 'baz', 'baz', 'baz', 'foo']], + ], + }); + }); + + it('works #41', () => { + const document = { + baz: { + baz: { + foo: {}, + a: { + baz: { + foo: true, + test: { + baz: { + foo: { + foo: { + foo: 'x', + }, + }, + }, + }, + }, + }, + baz: { + foo: true, + baz: { + foo: true, + a: { + foo: { + baz: { + foo: { + foo: { + abc: { + foo: 'matched', + }, + foo: 'another match', + }, + x: { + foo: { + foo: 'missed', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const collected = collect(document, [ + '$.baz..baz..foo.foo.foo', + '$.baz..baz.baz..foo.foo..foo', + ]); + + expect(collected).to.deep.eq({ + '$.baz..baz.baz..foo.foo..foo': [ + [ + 'matched', + [ + 'baz', + 'baz', + 'baz', + 'baz', + 'a', + 'foo', + 'baz', + 'foo', + 'foo', + 'abc', + 'foo', + ], + ], + [ + 'another match', + ['baz', 'baz', 'baz', 'baz', 'a', 'foo', 'baz', 'foo', 'foo', 'foo'], + ], + ], + '$.baz..baz..foo.foo.foo': [ + ['x', ['baz', 'baz', 'a', 'baz', 'test', 'baz', 'foo', 'foo', 'foo']], + [ + 'matched', + [ + 'baz', + 'baz', + 'baz', + 'baz', + 'a', + 'foo', + 'baz', + 'foo', + 'foo', + 'abc', + 'foo', + ], + ], + [ + 'another match', + ['baz', 'baz', 'baz', 'baz', 'a', 'foo', 'baz', 'foo', 'foo', 'foo'], + ], + ], + }); + }); + forEach([ Object.preventExtensions({ shirts: Object.seal({ diff --git a/src/codegen/__tests__/iterator.test.mjs b/src/codegen/__tests__/iterator.test.mjs new file mode 100644 index 0000000..cb5bc40 --- /dev/null +++ b/src/codegen/__tests__/iterator.test.mjs @@ -0,0 +1,175 @@ +import { expect } from 'chai'; + +import parse from '../../parser/index.mjs'; +import Iterator from '../iterator.mjs'; + +describe('Iterator', () => { + describe('analyzer', () => { + it('$.baz', () => { + const ast = parse('$.baz'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: true, + inverseOffset: -1, + minimumDepth: 0, + stateOffset: -1, + }); + }); + + it('$.baz..baz', () => { + const ast = parse('$.baz..baz'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: -1, + minimumDepth: 1, + stateOffset: -1, + }); + }); + + it('$.baz..baz[?(@.abc)]', () => { + const ast = parse('$.baz..baz[?(@.abc)]'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: 1, + minimumDepth: 2, + stateOffset: -1, + }); + }); + + it('$.baz..[?(@.abc)].baz', () => { + const ast = parse('$.baz..[?(@.abc)].baz'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: -1, + minimumDepth: 1, + stateOffset: 1, + }); + }); + + it('$..foo..bar..baz', () => { + const ast = parse('$..foo..bar..baz'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: -1, + minimumDepth: 0, + stateOffset: 0, + }); + }); + + it('$.info.contact.*', () => { + const ast = parse('$.info.contact.*'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: true, + inverseOffset: -1, + minimumDepth: 2, + stateOffset: -1, + }); + }); + + it('$.bar[?( @property >= 400 )]..foo', () => { + const ast = parse('$.bar[?( @property >= 400 )]..foo'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: -1, + minimumDepth: 1, + stateOffset: 1, + }); + }); + + it('$..foo..[?( @property >= 900 )]..foo', () => { + const ast = parse('$..foo..[?( @property >= 900 )]..foo'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: -1, + minimumDepth: 0, + stateOffset: 0, + }); + }); + + it('$..examples.*', () => { + const ast = parse('$..examples.*'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: 0, + minimumDepth: 1, + stateOffset: -1, + }); + }); + + it('$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload', () => { + const ast = parse( + '$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload', + ); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: true, + inverseOffset: -1, + minimumDepth: 3, + stateOffset: 3, + }); + }); + + it('$.continents[:-1].countries', () => { + const ast = parse('$.continents[:-1].countries'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: true, + inverseOffset: -1, + minimumDepth: 1, + stateOffset: 1, + }); + }); + + it('$.Europe[*]..cities[?(@ ~= "^P\\\\.")]', () => { + const ast = parse('$.Europe[*]..cities[?(@ ~= "^P\\\\.")]'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: 2, + minimumDepth: 3, + stateOffset: -1, + }); + }); + + it('$..book[2]', () => { + const ast = parse('$..book[2]'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: 0, + minimumDepth: 1, + stateOffset: -1, + }); + }); + + it('$..book[0][category,author]', () => { + const ast = parse('$..book[0][category,author]'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: false, + inverseOffset: 0, + minimumDepth: 2, + stateOffset: -1, + }); + }); + + it('$.paths[*][*].operationId', () => { + const ast = parse('$.paths[*][*].operationId'); + + expect(Iterator.analyze(ast)).to.deep.eq({ + fixed: true, + inverseOffset: -1, + minimumDepth: 3, + stateOffset: -1, + }); + }); + }); +}); diff --git a/src/codegen/ast/builders.mjs b/src/codegen/ast/builders.mjs index 2493f98..cf8ade6 100644 --- a/src/codegen/ast/builders.mjs +++ b/src/codegen/ast/builders.mjs @@ -98,6 +98,7 @@ export function conditionalExpression(test, consequent, alternate) { } export function ifStatement(test, consequent, alternate) { + if (!consequent) throw new Error('abc'); return { type: 'IfStatement', test, diff --git a/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs b/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs index 5312f4d..3e201ae 100644 --- a/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs +++ b/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs @@ -10,13 +10,15 @@ function print(expr) { const ast = parse(`$[${expr}]`); const iterator = new Iterator(ast); const { value: node } = iterator[Symbol.iterator]().next(); - return astring( - generateFilterScriptExpression(iterator, node, { - attachCustomShorthand() { - // no-op - }, - }), - ); + const branch = []; + + generateFilterScriptExpression(branch, iterator, node, { + attachCustomShorthand() { + // no-op + }, + }); + + return astring(branch[0].test); } describe('parseFilterExpression', () => { diff --git a/src/codegen/baseline/generators.mjs b/src/codegen/baseline/generators.mjs index 0674cdc..a0061ec 100644 --- a/src/codegen/baseline/generators.mjs +++ b/src/codegen/baseline/generators.mjs @@ -1,203 +1,174 @@ import jsep from '../../parser/jsep.mjs'; import * as b from '../ast/builders.mjs'; +import { isNegativeSliceExpression } from '../guards.mjs'; import internalScope from '../templates/internal-scope.mjs'; import sandbox from '../templates/sandbox.mjs'; import scope from '../templates/scope.mjs'; +import state from '../templates/state.mjs'; -export function generateMemberExpression(iterator, { deep, value }) { - if (iterator.feedback.bailed) { - return b.safeBinaryExpression('!==', scope.property, b.literal(value)); - } +function generateStateCheck(branch, iterator, check, deep) { + const [prevNo, no] = iterator.state.numbers; - if (iterator.state.inverted) { - return b.safeBinaryExpression( - '!==', - iterator.state.pos === 0 - ? scope.property - : b.memberExpression( - scope.path, - b.binaryExpression( - '-', - scope.depth, - b.numericLiteral(Math.abs(iterator.state.pos)), - ), - true, - ), - b.literal(value), + if (iterator.state.isLastNode) { + return b.ifStatement( + b.logicalExpression( + '||', + b.binaryExpression('<', state.initialValue, b.numericLiteral(prevNo)), + b.unaryExpression('!', check), + ), + b.returnStatement(), ); } - if (deep) { - const isLastNode = - iterator.nextNode === null || iterator.nextNode === 'KeyExpression'; - - iterator.feedback.mutatesPos ||= !isLastNode; - - const right = b.sequenceExpression([ - b.assignmentExpression( - '=', - internalScope.pos, - isLastNode - ? b.conditionalExpression( - b.safeBinaryExpression('!==', scope.property, b.literal(value)), - b.numericLiteral(-1), - scope.depth, - ) - : b.callExpression( - b.memberExpression(scope.path, b.identifier('indexOf')), - [ - b.literal(value), - iterator.state.pos === 0 - ? internalScope.pos - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(1), - ), - ], + return b.ifStatement( + b.binaryExpression('>=', state.initialValue, b.numericLiteral(prevNo)), + b.blockStatement([ + b.ifStatement( + check, + b.blockStatement([ + b.assignmentExpression('|=', state.value, b.numericLiteral(no)), + ]), + deep + ? void 0 + : b.ifStatement( + b.binaryExpression( + '===', + b.callExpression( + b.memberExpression(b.identifier('state'), b.identifier('at')), + [b.numericLiteral(-1)], + ), + b.numericLiteral(prevNo), + ), + b.blockStatement([ + b.assignmentExpression( + '&=', + state.value, + b.numericLiteral(iterator.state.groupNumbers[0]), + ), + b.returnStatement(), + ]), ), ), - b.binaryExpression('===', internalScope.pos, b.numericLiteral(-1)), - ]); + ]), + ); +} - if (isLastNode) { - return b.logicalExpression( - '||', - b.binaryExpression( - '<', - scope.depth, - iterator.state.pos === 0 - ? internalScope.pos - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ), - ), - right, +function generatePropertyAccess(iterator) { + return (!iterator.state.indexed && iterator.state.isLastNode) || + iterator.state.usesState + ? scope.property + : b.memberExpression( + scope.path, + iterator.state.indexed + ? b.numericLiteral(iterator.state.offset) + : b.binaryExpression( + '-', + scope.depth, + b.numericLiteral(Math.abs(iterator.state.offset)), + ), + true, ); - } - - return right; - } +} - let left; - - if (!iterator.feedback.fixed && iterator.state.absolutePos !== 0) { - left = b.binaryExpression( - '<', - scope.depth, - iterator.state.pos === 0 - ? internalScope.pos - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ), +export function generateMemberExpression(branch, iterator, node) { + if (iterator.state.usesState) { + branch.push( + generateStateCheck( + branch, + iterator, + b.safeBinaryExpression('===', scope.property, b.literal(node.value)), + node.deep, + ), + ); + } else { + branch.push( + b.ifStatement( + b.safeBinaryExpression( + '!==', + generatePropertyAccess(iterator), + b.literal(node.value), + ), + b.returnStatement(), + ), ); } - - const right = b.safeBinaryExpression( - '!==', - b.memberExpression( - scope.path, - iterator.state.pos === 0 - ? b.numericLiteral(0) - : iterator.feedback.fixed - ? b.numericLiteral(iterator.state.pos) - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ), - true, - ), - b.literal(value), - ); - - return left !== void 0 ? b.logicalExpression('||', left, right) : right; } -export function generateMultipleMemberExpression(iterator, node) { - return node.value.slice(1).reduce( - (concat, member) => - b.logicalExpression( - '&&', - concat, - generateMemberExpression(iterator, { - type: 'MemberExpression', - value: member, - // eslint-disable-next-line sort-keys - deep: node.deep, - }), - ), - generateMemberExpression(iterator, { - type: 'MemberExpression', - value: node.value[0], - // eslint-disable-next-line sort-keys - deep: node.deep, - }), - ); +export function generateMultipleMemberExpression(branch, iterator, node) { + const property = generatePropertyAccess(iterator); + + const condition = node.value + .slice(1) + .reduce( + (concat, member) => + b.logicalExpression( + '&&', + concat, + b.safeBinaryExpression('!==', property, b.literal(member)), + ), + b.safeBinaryExpression('!==', property, b.literal(node.value[0])), + ); + + if (iterator.state.usesState) { + branch.push(generateStateCheck(branch, iterator, condition, node.deep)); + } else { + branch.push(b.ifStatement(condition, b.returnStatement())); + } } const IN_BOUNDS_IDENTIFIER = b.identifier('inBounds'); -export function generateSliceExpression(iterator, node, tree) { - const member = iterator.state.inverted - ? b.binaryExpression('-', scope.depth, b.numericLiteral(iterator.state.pos)) - : iterator.state.pos === 0 - ? b.numericLiteral(0) - : iterator.feedback.fixed - ? b.numericLiteral(iterator.state.pos) - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ); - - const path = iterator.feedback.bailed - ? scope.property - : b.memberExpression(scope.path, member, true); +function generateNegativeSliceExpression(branch, iterator, node, tree) { + tree.addRuntimeDependency(IN_BOUNDS_IDENTIFIER.name); + const property = generatePropertyAccess(iterator); const isNumberBinaryExpression = b.binaryExpression( '!==', - b.unaryExpression('typeof', path), + b.unaryExpression('typeof', property), b.stringLiteral('number'), ); - const hasNegativeIndex = node.value.some( - value => Number.isFinite(value) && value < 0, + const condition = b.binaryExpression( + '||', + isNumberBinaryExpression, + b.unaryExpression( + '!', + b.callExpression(IN_BOUNDS_IDENTIFIER, [ + scope.sandbox, + generatePropertyAccess(iterator), + ...node.value.map(value => b.numericLiteral(value)), + ]), + ), ); - if (hasNegativeIndex) { - tree.addRuntimeDependency(IN_BOUNDS_IDENTIFIER.name); - return b.binaryExpression( - '||', - isNumberBinaryExpression, - b.unaryExpression( - '!', - b.callExpression(IN_BOUNDS_IDENTIFIER, [ - iterator.state.absolutePos === 0 - ? remapSandbox(sandbox.value, iterator.state.absolutePos - 2) - : remapSandbox(sandbox.value, iterator.state.absolutePos), - b.memberExpression( - scope.path, - iterator.feedback.bailed - ? b.binaryExpression( - '-', - b.memberExpression(scope.path, b.identifier('length')), - b.numericLiteral(1), - ) - : member, - true, - ), - ...node.value.map(value => b.numericLiteral(value)), - ]), + if (iterator.state.usesState) { + branch.push( + generateStateCheck( + branch, + iterator, + b.unaryExpression('!', condition), + node.deep, ), ); + } else { + branch.push(b.ifStatement(condition, b.returnStatement())); + } +} + +export function generateSliceExpression(branch, iterator, node, tree) { + if (isNegativeSliceExpression(node)) { + return generateNegativeSliceExpression(branch, iterator, node, tree); } - return node.value.reduce((merged, value, i) => { + const property = generatePropertyAccess(iterator); + + const isNumberBinaryExpression = b.binaryExpression( + '!==', + b.unaryExpression('typeof', property), + b.stringLiteral('number'), + ); + + const condition = node.value.reduce((merged, value, i) => { if (i === 0 && value === 0) { return merged; } @@ -214,7 +185,7 @@ export function generateSliceExpression(iterator, node, tree) { const expression = b.binaryExpression( operator, - path, + property, b.numericLiteral(Number(value)), ); @@ -224,7 +195,11 @@ export function generateSliceExpression(iterator, node, tree) { operator === '%' ? b.logicalExpression( '&&', - b.binaryExpression('!==', path, b.numericLiteral(node.value[0])), + b.binaryExpression( + '!==', + property, + b.numericLiteral(node.value[0]), + ), b.binaryExpression( '!==', expression, @@ -234,89 +209,67 @@ export function generateSliceExpression(iterator, node, tree) { : expression, ); }, isNumberBinaryExpression); + + if (iterator.state.usesState) { + branch.push( + generateStateCheck( + branch, + iterator, + b.unaryExpression('!', condition), + node.deep, + ), + ); + } else { + branch.push(b.ifStatement(condition, b.returnStatement())); + } } -export function generateWildcardExpression(iterator) { - if (iterator.feedback.bailed) { - return b.booleanLiteral(false); - } else if (iterator.nextNode === null && !iterator.feedback.fixed) { - return b.sequenceExpression([ - b.assignmentExpression( - '=', - internalScope.pos, - b.conditionalExpression( - b.binaryExpression( - '<', - scope.depth, - b.numericLiteral(iterator.state.pos), - ), - b.numericLiteral(-1), - scope.depth, - ), +export function generateWildcardExpression(branch, iterator) { + if (!iterator.state.usesState) return; + + const [prevNo, no] = iterator.state.numbers; + + if (iterator.state.isLastNode) { + branch.push( + b.ifStatement( + b.binaryExpression('<', state.initialValue, b.numericLiteral(prevNo)), + b.returnStatement(), ), - b.binaryExpression('===', internalScope.pos, b.numericLiteral(-1)), - ]); + ); } else { - return null; + branch.push( + b.ifStatement( + b.binaryExpression('>=', state.initialValue, b.numericLiteral(prevNo)), + b.blockStatement([ + b.assignmentExpression('|=', state.value, b.numericLiteral(no)), + ]), + ), + ); } } export function generateFilterScriptExpression( + branch, iterator, - { deep, value }, + { value }, tree, ) { const esTree = jsep(value); assertDefinedIdentifier(esTree); - const node = b.unaryExpression( - '!', - rewriteESTree( - tree, - esTree, - iterator.state.fixed && - iterator.state.pos > 0 && - iterator.nextNode !== null - ? iterator.state.pos + 1 - : iterator.state.inverted && iterator.state.pos !== 0 - ? iterator.state.pos - 1 - : 0, - ), - ); - - if (iterator.feedback.bailed || !deep || iterator.state.inverted) return node; - - iterator.feedback.mutatesPos ||= - iterator.nextNode !== null && iterator.nextNode !== 'KeyExpression'; + const node = rewriteESTree(tree, esTree); - const assignment = b.sequenceExpression([ - b.assignmentExpression( - '=', - internalScope.pos, - b.conditionalExpression(node, b.numericLiteral(-1), scope.depth), - ), - b.binaryExpression('===', internalScope.pos, b.numericLiteral(-1)), - ]); - - if (iterator.state.pos === 0) return assignment; - - return b.logicalExpression( - '||', - b.binaryExpression( - '<', - scope.depth, - iterator.state.pos === 0 - ? internalScope.pos - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ), - ), - assignment, - ); + if (iterator.state.usesState) { + branch.push( + generateStateCheck(branch, iterator, node, iterator.state.isLastNode), + ); + } else { + branch.push( + b.ifStatement(b.unaryExpression('!', node), b.returnStatement()), + ); + } } -export function rewriteESTree(tree, node, pos) { +export function rewriteESTree(tree, node) { switch (node.type) { case 'LogicalExpression': case 'BinaryExpression': @@ -324,7 +277,7 @@ export function rewriteESTree(tree, node, pos) { node.operator = '==='; node.left = b.callExpression( b.memberExpression(node.right, b.identifier('includes')), - [rewriteESTree(tree, node.left, pos)], + [rewriteESTree(tree, node.left)], ); node.right = b.booleanLiteral(true); } else if (node.operator === '~=') { @@ -337,24 +290,24 @@ export function rewriteESTree(tree, node, pos) { b.regExpLiteral(node.right.value, ''), b.identifier('test'), ), - [rewriteESTree(tree, node.left, pos)], + [rewriteESTree(tree, node.left)], ); } else { - node.left = rewriteESTree(tree, node.left, pos); - node.right = rewriteESTree(tree, node.right, pos); + node.left = rewriteESTree(tree, node.left); + node.right = rewriteESTree(tree, node.right); assertDefinedIdentifier(node.left); assertDefinedIdentifier(node.right); } break; case 'UnaryExpression': - node.argument = rewriteESTree(tree, node.argument, pos); + node.argument = rewriteESTree(tree, node.argument); assertDefinedIdentifier(node.argument); return node; case 'MemberExpression': - node.object = rewriteESTree(tree, node.object, pos); + node.object = rewriteESTree(tree, node.object); assertDefinedIdentifier(node.object); - node.property = rewriteESTree(tree, node.property, pos); + node.property = rewriteESTree(tree, node.property); if (node.computed) { assertDefinedIdentifier(node.property); } @@ -365,12 +318,12 @@ export function rewriteESTree(tree, node, pos) { node.callee.type === 'Identifier' && node.callee.name.startsWith('@') ) { - return processAtIdentifier(tree, node.callee.name, pos); + return processAtIdentifier(tree, node.callee.name); } - node.callee = rewriteESTree(tree, node.callee, pos); + node.callee = rewriteESTree(tree, node.callee); node.arguments = node.arguments.map(argument => - rewriteESTree(tree, argument, pos), + rewriteESTree(tree, argument), ); if ( @@ -387,7 +340,7 @@ export function rewriteESTree(tree, node, pos) { break; case 'Identifier': if (node.name.startsWith('@')) { - return processAtIdentifier(tree, node.name, pos); + return processAtIdentifier(tree, node.name); } if (node.name === 'undefined') { @@ -404,71 +357,59 @@ export function rewriteESTree(tree, node, pos) { return node; } -function processAtIdentifier(tree, name, pos) { +function processAtIdentifier(tree, name) { switch (name) { case '@': - return remapSandbox(sandbox.value, pos); + return sandbox.value; case '@root': - return remapSandbox(sandbox.root, pos); + return sandbox.root; case '@path': - return remapSandbox(sandbox.path, pos); + return sandbox.path; case '@property': - return remapSandbox(sandbox.property, pos); + return sandbox.property; case '@parent': - return remapSandbox(sandbox.parentValue, pos); + return sandbox.parentValue; case '@parentProperty': - return remapSandbox(sandbox.parentProperty, pos); + return sandbox.parentProperty; case '@string': case '@number': case '@boolean': return b.binaryExpression( '===', - b.unaryExpression('typeof', remapSandbox(sandbox.value, pos)), + b.unaryExpression('typeof', sandbox.value), b.stringLiteral(name.slice(1)), ); case '@scalar': return b.logicalExpression( '||', - b.binaryExpression( - '===', - remapSandbox(sandbox.value, pos), - b.nullLiteral(), - ), + b.binaryExpression('===', sandbox.value, b.nullLiteral()), b.binaryExpression( '!==', - b.unaryExpression('typeof', remapSandbox(sandbox.value, pos)), + b.unaryExpression('typeof', sandbox.value), b.stringLiteral('object'), ), ); case '@array': return b.callExpression( b.memberExpression(b.identifier('Array'), b.identifier('isArray')), - [remapSandbox(sandbox.value, pos)], + [sandbox.value], ); case '@null': - return b.binaryExpression( - '===', - remapSandbox(sandbox.value, pos), - b.nullLiteral(), - ); + return b.binaryExpression('===', sandbox.value, b.nullLiteral()); case '@object': return b.logicalExpression( '&&', - b.binaryExpression( - '!==', - remapSandbox(sandbox.value, pos), - b.nullLiteral(), - ), + b.binaryExpression('!==', sandbox.value, b.nullLiteral()), b.binaryExpression( '===', - b.unaryExpression('typeof', remapSandbox(sandbox.value, pos)), + b.unaryExpression('typeof', sandbox.value), b.stringLiteral('object'), ), ); case '@integer': return b.callExpression( b.memberExpression(b.identifier('Number'), b.identifier('isInteger')), - [remapSandbox(sandbox.value, pos)], + [sandbox.value], ); default: if (name.startsWith('@@')) { @@ -494,14 +435,3 @@ function assertDefinedIdentifier(node) { if (KNOWN_IDENTIFIERS.includes(node.name)) return; throw ReferenceError(`'${node.name}' is not defined`); } - -function remapSandbox(node, pos) { - if (node.type === 'MemberExpression' && pos !== 0) { - return { - ...node, - object: b.callExpression(sandbox.at, [b.numericLiteral(pos)]), - }; - } - - return node; -} diff --git a/src/codegen/baseline/index.mjs b/src/codegen/baseline/index.mjs index 7176fd5..b09fd75 100644 --- a/src/codegen/baseline/index.mjs +++ b/src/codegen/baseline/index.mjs @@ -2,10 +2,7 @@ import * as b from '../ast/builders.mjs'; import fastPaths from '../fast-paths/index.mjs'; import { isDeep } from '../guards.mjs'; import Iterator from '../iterator.mjs'; -import optimizer from '../optimizer/index.mjs'; import generateEmitCall from '../templates/emit-call.mjs'; -import fnParams from '../templates/fn-params.mjs'; -import internalScope from '../templates/internal-scope.mjs'; import scope from '../templates/scope.mjs'; import ESTree from '../tree/tree.mjs'; import { @@ -16,10 +13,6 @@ import { generateWildcardExpression, } from './generators.mjs'; -const POS_VARIABLE_DECLARATION = b.variableDeclaration('let', [ - b.variableDeclarator(internalScope.pos, b.numericLiteral(0)), -]); - export default function baseline(jsonPaths, opts) { const tree = new ESTree(opts); const hashes = new Map(); @@ -44,17 +37,13 @@ export default function baseline(jsonPaths, opts) { const method = tree.getMethodByHash(existingHash); let body = method.body.body; - if (iterator.feedback.bailed) { - body = body[0].expression.arguments[1].body.body; - } - body.push(generateEmitCall(id, iterator.modifiers)); continue; } else { hashes.set(hash, id); } - if (iterator.feedback.bailed || (nodes.length > 0 && isDeep(nodes[0]))) { + if (nodes.length > 0 && isDeep(nodes[0])) { tree.traversalZones.destroy(); } @@ -71,136 +60,63 @@ export default function baseline(jsonPaths, opts) { } } - const branch = iterator.feedback.bailed - ? [] - : [ - b.ifStatement( - b.binaryExpression( - iterator.feedback.fixed ? '!==' : '<', - scope.depth, - b.numericLiteral(iterator.length - 1), + const branch = + iterator.feedback.minimumDepth !== -1 + ? [ + b.ifStatement( + b.binaryExpression( + iterator.feedback.fixed && iterator.feedback.stateOffset === -1 + ? '!==' + : '<', + scope.depth, + b.numericLiteral(iterator.feedback.minimumDepth + 1), + ), + b.returnStatement(), ), - b.returnStatement(), - ), - ].concat(iterator.feedback.fixed ? [] : POS_VARIABLE_DECLARATION); + ] + : []; - const zone = iterator.feedback.bailed ? null : tree.traversalZones.create(); - const inverseAt = iterator.feedback.inverseAt; + const zone = tree.traversalZones.create(); for (const node of iterator) { - if (isDeep(node) || inverseAt === iterator.state.absolutePos) { + if (isDeep(node)) { zone?.allIn(); } - let treeNode; - switch (node.type) { case 'MemberExpression': - treeNode = generateMemberExpression(iterator, node, tree); + generateMemberExpression(branch, iterator, node, tree); zone?.expand(node.value); break; case 'MultipleMemberExpression': - treeNode = generateMultipleMemberExpression(iterator, node, tree); + generateMultipleMemberExpression(branch, iterator, node, tree); zone?.expandMultiple(node.value); break; case 'SliceExpression': - treeNode = generateSliceExpression(iterator, node, tree); + generateSliceExpression(branch, iterator, node, tree); zone?.resize(); break; case 'ScriptFilterExpression': - treeNode = generateFilterScriptExpression(iterator, node, tree); + generateFilterScriptExpression(branch, iterator, node, tree); zone?.resize(); break; case 'WildcardExpression': - treeNode = generateWildcardExpression(iterator, node, tree); + generateWildcardExpression(branch, iterator, node, tree); zone?.resize(); - if (treeNode === null) { - continue; - } - break; } - - if (iterator.feedback.bailed) { - branch.push( - b.objectExpression([ - b.objectProperty( - b.identifier('fn'), - b.arrowFunctionExpression([scope._], treeNode), - ), - b.objectProperty(b.identifier('deep'), b.booleanLiteral(node.deep)), - ]), - ); - } else { - branch.push(b.ifStatement(treeNode, b.returnStatement())); - } } - if ( - !iterator.feedback.fixed && - !iterator.feedback.bailed && - !iterator.state.inverted - ) { - branch.push( - b.ifStatement( - b.binaryExpression( - '!==', - scope.depth, - iterator.state.pos === 0 - ? internalScope.pos - : b.binaryExpression( - '+', - internalScope.pos, - b.numericLiteral(iterator.state.pos), - ), - ), - b.returnStatement(), - ), - ); - } - - const placement = iterator.feedback.bailed ? 'body' : 'traverse'; - - if (iterator.feedback.bailed) { - branch.splice( - 0, - branch.length, - b.expressionStatement( - b.callExpression(scope.bail, [ - b.stringLiteral(id), - b.arrowFunctionExpression( - [scope._], - b.blockStatement([ - b.expressionStatement( - generateEmitCall(ctx.id, iterator.modifiers).expression, - ), - ]), - ), - b.arrayExpression([...branch]), - ]), - ), - ); - } else { - branch.push(generateEmitCall(ctx.id, iterator.modifiers)); - } + branch.push(generateEmitCall(ctx.id, iterator.modifiers)); - if (placement === 'body') { - tree.push( - b.expressionStatement( - b.callExpression( - b.memberExpression(internalScope.tree, b.stringLiteral(id), true), - fnParams, - ), - ), - placement, - ); + if (iterator.feedback.stateOffset !== -1) { + tree.push(b.stringLiteral(id), 'stateful-traverse'); + tree.push(b.blockStatement(branch), 'stateful-tree-method'); } else { - tree.push(b.stringLiteral(id), placement); + tree.push(b.stringLiteral(id), 'traverse'); + tree.push(b.blockStatement(branch), 'tree-method'); } - optimizer(branch, iterator); - tree.push(b.blockStatement(branch), 'tree-method'); - zone?.attach(); } diff --git a/src/codegen/fast-paths/fixed.mjs b/src/codegen/fast-paths/fixed.mjs index 745a2a4..9bddb96 100644 --- a/src/codegen/fast-paths/fixed.mjs +++ b/src/codegen/fast-paths/fixed.mjs @@ -14,6 +14,7 @@ import * as b from '../ast/builders.mjs'; import { isDeep, isMemberExpression } from '../guards.mjs'; import generateEmitCall from '../templates/emit-call.mjs'; +import { statelessFnParams } from '../templates/fn-params.mjs'; import sandbox from '../templates/sandbox.mjs'; import scope from '../templates/scope.mjs'; import treeMethodCall from '../templates/tree-method-call.mjs'; @@ -77,7 +78,7 @@ export default (nodes, tree, ctx) => { ]), 'tree-method', ], - [treeMethodCall(ctx.id), 'body'], + [treeMethodCall(ctx.id, statelessFnParams), 'body'], ]); return true; diff --git a/src/codegen/fast-paths/only-filter-script-expression.mjs b/src/codegen/fast-paths/only-filter-script-expression.mjs index 9e414cb..09bf91d 100644 --- a/src/codegen/fast-paths/only-filter-script-expression.mjs +++ b/src/codegen/fast-paths/only-filter-script-expression.mjs @@ -10,7 +10,7 @@ import generateEmitCall from '../templates/emit-call.mjs'; import scope from '../templates/scope.mjs'; const TOP_LEVEL_DEPTH_IF_STATEMENT = b.ifStatement( - b.binaryExpression('!==', scope.depth, b.numericLiteral(0)), + b.binaryExpression('!==', scope.depth, b.numericLiteral(1)), b.returnStatement(), ); diff --git a/src/codegen/fast-paths/top-level-wildcard.mjs b/src/codegen/fast-paths/top-level-wildcard.mjs index a384a65..d06d874 100644 --- a/src/codegen/fast-paths/top-level-wildcard.mjs +++ b/src/codegen/fast-paths/top-level-wildcard.mjs @@ -8,7 +8,7 @@ import generateEmitCall from '../templates/emit-call.mjs'; import scope from '../templates/scope.mjs'; const IS_NOT_ZERO_DEPTH_IF_STATEMENT = b.ifStatement( - b.binaryExpression('!==', scope.depth, b.numericLiteral(0)), + b.binaryExpression('!==', scope.depth, b.numericLiteral(1)), b.returnStatement(), ); diff --git a/src/codegen/guards.mjs b/src/codegen/guards.mjs index 500e9fe..a64b27d 100644 --- a/src/codegen/guards.mjs +++ b/src/codegen/guards.mjs @@ -6,6 +6,10 @@ export function isScriptFilterExpression(node) { return node.type === 'ScriptFilterExpression'; } +export function isNegativeSliceExpression(node) { + return node.type === 'SliceExpression' && node.value.some(isNegativeNumber); +} + export function isModifierExpression(node) { return node.type === 'KeyExpression' || node.type === 'ParentExpression'; } @@ -17,3 +21,7 @@ export function isWildcardExpression(node) { export function isDeep(node) { return node.deep; } + +export function isNegativeNumber(value) { + return Number.isFinite(value) && value < 0; +} diff --git a/src/codegen/iterator.mjs b/src/codegen/iterator.mjs index c78d84e..4a1c5fa 100644 --- a/src/codegen/iterator.mjs +++ b/src/codegen/iterator.mjs @@ -1,81 +1,38 @@ import { isDeep, - isMemberExpression, isModifierExpression, + isNegativeSliceExpression, + isScriptFilterExpression, isWildcardExpression, } from './guards.mjs'; -function isBailable(nodes) { - let deep = false; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (!isDeep(node)) continue; - - if (deep) { - return true; - } else if (isMemberExpression(node)) { - i++; - let hadFlatMemberExpressions = false; - let deepNodes = 1; - for (; i < nodes.length - 1; i++) { - const node = nodes[i]; - if (isDeep(node)) { - deepNodes++; - } else { - hadFlatMemberExpressions ||= - isMemberExpression(node) || isWildcardExpression(node); - continue; - } - - if (isMemberExpression(node) || isWildcardExpression(node)) { - if (hadFlatMemberExpressions) return true; - continue; - } - - return true; - } - - return isDeep(nodes[nodes.length - 1]) - ? hadFlatMemberExpressions || - isWildcardExpression(nodes[nodes.length - 1]) - : deepNodes > 1; - } else { - deep = true; - } - } - - return false; +function emptyState() { + return { + absoluteOffset: -1, + groupNumbers: [], + indexed: true, + isLastNode: true, + numbers: [-1, -1], + offset: -1, + usesState: false, + }; } export default class Iterator { nodes; - #i; constructor(nodes) { this.modifiers = Iterator.trim(nodes); this.nodes = Iterator.compact(nodes); - this.#i = -1; - this.feedback = Iterator.analyze( - this.nodes, - this.modifiers.keyed || this.modifiers.parents > 0, - ); + this.feedback = Iterator.analyze(this.nodes); this.length = this.nodes.length; - this.state = { - absolutePos: -1, - fixed: true, - inverted: false, - pos: -1, - }; + this.state = emptyState(); if (this.feedback.fixed && this.modifiers.parents > this.length) { this.length = -1; } } - get nextNode() { - return this.#i + 1 < this.nodes.length ? this.nodes[this.#i + 1] : null; - } - static compact(nodes) { let marked; for (let i = 0; i < nodes.length; i++) { @@ -124,86 +81,100 @@ export default class Iterator { static analyze(nodes) { const feedback = { - bailed: isBailable(nodes), fixed: true, - inverseAt: -1, + inverseOffset: -1, + minimumDepth: -1, + stateOffset: -1, }; - if (feedback.bailed) { - feedback.fixed = false; - return feedback; - } - - let potentialInvertAtPoint = -1; + let deep = -1; + let potentialInverseOffset = -1; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; - if (!isDeep(node)) continue; + if (!isDeep(node)) { + if (isScriptFilterExpression(node) || isNegativeSliceExpression(node)) { + if (i === nodes.length - 1) { + feedback.inverseOffset = potentialInverseOffset; + } else { + feedback.stateOffset = deep === -1 ? i : deep; + } + } else { + feedback.inverseOffset = potentialInverseOffset; + } - feedback.fixed = false; - i++; + continue; + } - potentialInvertAtPoint = i - 1; + if (potentialInverseOffset === -1) { + potentialInverseOffset = i; + } - for (; i < nodes.length; i++) { - const nextNode = nodes[i]; - if (isDeep(nextNode)) { - potentialInvertAtPoint = -1; - } + if (deep !== -1) { + feedback.stateOffset = deep; + break; + } else if (isScriptFilterExpression(node) && i !== nodes.length - 1) { + feedback.stateOffset = i; + deep = i; + break; } - } - if ( - nodes.length > 1 && - potentialInvertAtPoint !== -1 && - potentialInvertAtPoint < nodes.length - 1 - ) { - feedback.inverseAt = potentialInvertAtPoint; + deep = i; } + feedback.fixed = deep === -1; + feedback.minimumDepth = + feedback.stateOffset === -1 ? nodes.length - 1 : feedback.stateOffset; + return feedback; } *[Symbol.iterator]() { - if (this.feedback.bailed) { - return yield* this.nodes; - } - - const { ...feedback } = this.feedback; + const { feedback, nodes, state } = this; - let order = 1; - const nodes = - this.feedback.inverseAt !== -1 ? this.nodes.slice() : this.nodes; + Object.assign(state, emptyState()); + let statePos = 0; for (let i = 0; i < nodes.length; i++) { - if (this.feedback.inverseAt !== -1 && i === this.feedback.inverseAt) { - nodes.splice(0, i); - nodes.reverse(); - this.state.pos = 1; - i = 0; - this.feedback.inverseAt = -1; - this.state.inverted = true; - order = -1; + state.absoluteOffset = i; + + if (isDeep(nodes[i])) { + state.offset = -1; + state.indexed = false; + + if (state.groupNumbers.length > 0) { + state.groupNumbers.length = 0; + } } - const node = nodes[i]; - this.state.pos += order; - this.#i++; - this.state.absolutePos++; - - if (isDeep(node)) { - this.state.fixed = false; - yield node; - this.state.pos = 0; + if (feedback.stateOffset === i) { + state.offset = -1; + state.usesState = true; + } + + if (feedback.inverseOffset === i) { + state.offset = i - nodes.length; } else { - yield node; + state.offset++; } - } - Object.assign(this.feedback, { - ...feedback, - mutatesPos: this.feedback.mutatesPos, - }); + if (state.usesState) { + if (statePos === 0) { + state.numbers[0] = 0; + state.numbers[1] = 1; + } else { + state.numbers[0] = state.numbers[1]; + state.numbers[1] = state.numbers[1] + 2 ** statePos; + } + + state.groupNumbers.push(state.numbers[0]); + statePos++; + } + + state.isLastNode = i === nodes.length - 1; + + yield nodes[i]; + } } } diff --git a/src/codegen/optimizer/index.mjs b/src/codegen/optimizer/index.mjs deleted file mode 100644 index f994e51..0000000 --- a/src/codegen/optimizer/index.mjs +++ /dev/null @@ -1,92 +0,0 @@ -import internalScope from '../templates/internal-scope.mjs'; -import scope from '../templates/scope.mjs'; - -function dropNode(branch, i) { - branch.splice(i, 1); - return i - 1; -} - -function leftOrRight(node, left, right) { - if (left === null) { - return right; - } else if (right === null) { - return left; - } - - node.left = left; - node.right = right; - - return node; -} - -function reduceBinaryExpression(node) { - if (node.operator === '<' && node.left === scope.depth) { - return null; - } - - return leftOrRight(node, eliminate(node.left), eliminate(node.right)); -} - -function eliminate(node) { - switch (node.type) { - case 'AssignmentExpression': - if (node.left !== internalScope.pos) { - return node; - } - - return eliminate(node.right); - case 'ConditionalExpression': - if ( - node.consequent.type === 'NumericLiteral' && - node.consequent.value === -1 - ) { - return eliminate(node.test); - } - - return node; - case 'SequenceExpression': - return eliminate(node.expressions[0]); - case 'LogicalExpression': - return leftOrRight(node, eliminate(node.left), eliminate(node.right)); - case 'BinaryExpression': - return reduceBinaryExpression(node); - case 'IfStatement': - return eliminate(node.test); - case 'Identifier': - if (node === internalScope.pos) { - return null; - } - - return node; - case 'MemberExpression': - node.property = eliminate(node.property); - return node; - default: - return node; - } -} - -export default function optimizer(branch, iterator) { - if (iterator.feedback.mutatesPos) return; - - let i = Math.max(0, Math.min(1, iterator.length)); - - for (; i < branch.length; i++) { - const node = branch[i]; - if ( - node.type === 'VariableDeclaration' && - node.kind === 'let' && - node.declarations[0].id === internalScope.pos - ) { - i = dropNode(branch, i); - continue; - } - - const test = eliminate(node); - if (test === null || test === scope.depth) { - i = dropNode(branch, i); - } else { - node.test = test; - } - } -} diff --git a/src/codegen/templates/emit-call.mjs b/src/codegen/templates/emit-call.mjs index fb1a7e9..0aabce1 100644 --- a/src/codegen/templates/emit-call.mjs +++ b/src/codegen/templates/emit-call.mjs @@ -2,8 +2,6 @@ import * as b from '../ast/builders.mjs'; import scope from './scope.mjs'; export default function generateEmitCall(id, { parents, keyed }) { - // can emit check - // todo: add check return b.expressionStatement( b.callExpression(scope.emit, [ b.stringLiteral(id), diff --git a/src/codegen/templates/fn-params.mjs b/src/codegen/templates/fn-params.mjs index 0cb7e74..ecf3e0c 100644 --- a/src/codegen/templates/fn-params.mjs +++ b/src/codegen/templates/fn-params.mjs @@ -1,3 +1,5 @@ import scope from './scope.mjs'; +import state from './state.mjs'; -export default [scope._]; +export const statelessFnParams = [scope._]; +export const statefulFnParams = [scope._, state._]; diff --git a/src/codegen/templates/internal-scope.mjs b/src/codegen/templates/internal-scope.mjs index 4e284ad..32ffc43 100644 --- a/src/codegen/templates/internal-scope.mjs +++ b/src/codegen/templates/internal-scope.mjs @@ -1,7 +1,6 @@ import * as b from '../ast/builders.mjs'; export default { - pos: b.identifier('pos'), shorthands: b.identifier('shorthands'), tree: b.identifier('tree'), }; diff --git a/src/codegen/templates/scope.mjs b/src/codegen/templates/scope.mjs index 3c3c2e4..dfb58b1 100644 --- a/src/codegen/templates/scope.mjs +++ b/src/codegen/templates/scope.mjs @@ -2,17 +2,24 @@ import * as b from '../ast/builders.mjs'; const SCOPE_IDENTIFIER = b.identifier('scope'); +const PATH = b.memberExpression(SCOPE_IDENTIFIER, b.identifier('path')); +const DEPTH = b.memberExpression(PATH, b.identifier('length')); + export default { _: SCOPE_IDENTIFIER, - bail: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('bail')), + allocState: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('allocState')), callbacks: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('callbacks')), - depth: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('depth')), + depth: DEPTH, destroy: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('destroy')), emit: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('emit')), fork: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('fork')), - path: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('path')), - property: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('property')), + path: PATH, + property: b.memberExpression( + PATH, + b.binaryExpression('-', DEPTH, b.numericLiteral(1)), + true, + ), sandbox: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('sandbox')), traverse: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('traverse')), value: b.memberExpression(SCOPE_IDENTIFIER, b.identifier('value')), diff --git a/src/codegen/templates/state.mjs b/src/codegen/templates/state.mjs new file mode 100644 index 0000000..65a29d7 --- /dev/null +++ b/src/codegen/templates/state.mjs @@ -0,0 +1,9 @@ +import * as b from '../ast/builders.mjs'; + +const state = b.identifier('state'); +export default { + _: state, + + initialValue: b.memberExpression(state, b.identifier('initialValue')), + value: b.memberExpression(state, b.identifier('value')), +}; diff --git a/src/codegen/templates/tree-method-call.mjs b/src/codegen/templates/tree-method-call.mjs index a567a26..71e1f26 100644 --- a/src/codegen/templates/tree-method-call.mjs +++ b/src/codegen/templates/tree-method-call.mjs @@ -1,13 +1,12 @@ import * as b from '../ast/builders.mjs'; -import fnParams from './fn-params.mjs'; import internalScope from './internal-scope.mjs'; -export default function treeMethodCall(id) { +export default function treeMethodCall(id, params) { const property = b.stringLiteral(id); return b.expressionStatement( b.callExpression( b.memberExpression(internalScope.tree, property, true), - fnParams, + params, ), ); } diff --git a/src/codegen/tree/tree.mjs b/src/codegen/tree/tree.mjs index 7c12658..b4cf7d5 100644 --- a/src/codegen/tree/tree.mjs +++ b/src/codegen/tree/tree.mjs @@ -1,7 +1,10 @@ import jsep from '../../parser/jsep.mjs'; import * as b from '../ast/builders.mjs'; import astring from '../dump.mjs'; -import fnParams from '../templates/fn-params.mjs'; +import { + statefulFnParams, + statelessFnParams, +} from '../templates/fn-params.mjs'; import internalScope from '../templates/internal-scope.mjs'; import scope from '../templates/scope.mjs'; import treeMethodCall from '../templates/tree-method-call.mjs'; @@ -48,6 +51,7 @@ export default class ESTree { #body = new Set(); #traverse = new Set(); #availableShorthands; + #states = -1; constructor({ customShorthands, format, module, npmProvider }) { this.format = format; @@ -83,7 +87,7 @@ export default class ESTree { b.objectMethod( 'method', b.identifier(name), - fnParams, + statefulFnParams, b.blockStatement([ b.returnStatement(jsep(this.#availableShorthands[name])), ]), @@ -98,11 +102,14 @@ export default class ESTree { push(node, placement) { switch (placement) { case 'tree-method': + case 'stateful-tree-method': this.#tree.properties.push( b.objectMethod( 'method', b.stringLiteral(this.ctx.id), - fnParams, + placement === 'stateful-tree-method' + ? statefulFnParams + : statelessFnParams, node, ), ); @@ -120,8 +127,21 @@ export default class ESTree { break; case 'traverse': - this.#traverse.add(treeMethodCall(node.value)); + this.#traverse.add(treeMethodCall(node.value, statelessFnParams)); + break; + case 'stateful-traverse': { + this.#states += 1; + const id = b.identifier(`state${this.#states}`); + this.push( + b.variableDeclaration('const', [ + b.variableDeclarator(id, b.callExpression(scope.allocState, [])), + ]), + 'body', + ); + + this.#traverse.add(treeMethodCall(node.value, [scope._, id])); break; + } } } @@ -208,8 +228,4 @@ export default class ESTree { ), ); } - - toString() { - return this.export('esm'); - } } diff --git a/src/core/index.mjs b/src/core/index.mjs index 9ea9d1e..75e6932 100644 --- a/src/core/index.mjs +++ b/src/core/index.mjs @@ -9,20 +9,13 @@ export default class Nimma { constructor( expressions, - { - unsafe = true, - module = 'esm', - npmProvider = null, - customShorthands = null, - } = {}, + { module = 'esm', npmProvider = null, customShorthands = null } = {}, ) { this.#compiledFn = null; this.#module = module; this.#sourceCode = null; - const parsedExpressions = parseExpressions(expressions, unsafe); - - this.tree = codegen(parsedExpressions, { + this.tree = codegen(parseExpressions(expressions), { customShorthands, module, npmProvider, diff --git a/src/core/utils/parse-expressions.mjs b/src/core/utils/parse-expressions.mjs index ac5bdbf..8700212 100644 --- a/src/core/utils/parse-expressions.mjs +++ b/src/core/utils/parse-expressions.mjs @@ -1,4 +1,3 @@ -import Iterator from '../../codegen/iterator.mjs'; import parse from '../../parser/index.mjs'; function pickException([, ex]) { @@ -9,17 +8,13 @@ function pickExpression([expression]) { return expression; } -export default function parseExpressions(expressions, unsafe) { +export default function parseExpressions(expressions) { const mappedExpressions = []; const erroredExpressions = []; for (const expression of new Set(expressions)) { try { const parsed = parse(expression); - if (unsafe === false && Iterator.analyze(parsed).bailed) { - throw SyntaxError('Unsafe expressions are ignored'); - } - mappedExpressions.push([expression, parsed]); } catch (e) { erroredExpressions.push([expression, e]); diff --git a/src/core/index.d.ts b/src/index.d.ts similarity index 95% rename from src/core/index.d.ts rename to src/index.d.ts index d3ddff9..214d249 100644 --- a/src/core/index.d.ts +++ b/src/index.d.ts @@ -15,7 +15,6 @@ declare class Nimma { opts?: { customShorthands?: Record | null; npmProvider?: string | null; - unsafe?: boolean; module?: 'esm' | 'commonjs'; }, ); diff --git a/src/runtime/codegen-functions/__tests__/get.test.mjs b/src/runtime/codegen-functions/__tests__/get.test.mjs deleted file mode 100644 index 0dc5d53..0000000 --- a/src/runtime/codegen-functions/__tests__/get.test.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from 'chai'; - -import get from '../get.mjs'; - -describe('get codegen function', () => { - it('should gracefully handle invalid input', () => { - expect(get(null, [])).to.be.null; - }); - - it('should gracefully handle missed values', () => { - expect( - get( - { - foo: { - bar: true, - }, - }, - ['foo', 'bar', 'baz', 'oops'], - ), - ).to.be.undefined; - }); - - it('should retrieve value', () => { - const input = { - foo: { - bar: true, - }, - }; - expect(get(input, [])).to.eq(input); - expect(get(input, ['foo'])).to.eq(input.foo); - expect(get(input, ['foo', 'bar'])).to.eq(input.foo.bar); - }); -}); diff --git a/src/runtime/codegen-functions/get.mjs b/src/runtime/codegen-functions/get.mjs deleted file mode 100644 index 10cd483..0000000 --- a/src/runtime/codegen-functions/get.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import isObject from './is-object.mjs'; - -export default function get(input, path) { - if (path.length === 0 || !isObject(input)) return input; - - let value = input; - for (const segment of path.slice(0, path.length - 1)) { - value = value[segment]; - if (!isObject(value)) return; - } - - return value[path[path.length - 1]]; -} diff --git a/src/runtime/codegen-functions/in-bounds.mjs b/src/runtime/codegen-functions/in-bounds.mjs index be94404..f6fd9f5 100644 --- a/src/runtime/codegen-functions/in-bounds.mjs +++ b/src/runtime/codegen-functions/in-bounds.mjs @@ -1,4 +1,5 @@ -export default function (value, pos, start, end, step) { +export default function (sandbox, pos, start, end, step) { + const value = sandbox.parentAt(-2); const actualStart = start < 0 ? Math.max(0, start + value.length) diff --git a/src/runtime/codegen-functions/index.mjs b/src/runtime/codegen-functions/index.mjs index 09ed503..fa133fc 100644 --- a/src/runtime/codegen-functions/index.mjs +++ b/src/runtime/codegen-functions/index.mjs @@ -1,3 +1,2 @@ -export { default as get } from './get.mjs'; export { default as inBounds } from './in-bounds.mjs'; export { default as isObject } from './is-object.mjs'; diff --git a/src/runtime/sandbox.mjs b/src/runtime/sandbox.mjs index 556701d..483d2e8 100644 --- a/src/runtime/sandbox.mjs +++ b/src/runtime/sandbox.mjs @@ -24,10 +24,6 @@ export class Sandbox { return dumpPath(this.#path); } - get depth() { - return this.#path.length - 1; - } - get value() { if (this.#value !== void 0) { return this.#value; @@ -37,7 +33,7 @@ export class Sandbox { } get property() { - return unwrapOrNull(this.#path, this.depth); + return unwrapOrNull(this.#path, this.#path.length - 1); } get #parent() { @@ -48,6 +44,10 @@ export class Sandbox { return this.#history[this.#history.length - 3]; } + parentAt(i) { + return this.#history[this.#history.length + i][1]; + } + get parentValue() { return this.#parent?.[1]; } diff --git a/src/runtime/scope.mjs b/src/runtime/scope.mjs index c0c6b4f..2b74986 100644 --- a/src/runtime/scope.mjs +++ b/src/runtime/scope.mjs @@ -1,43 +1,71 @@ import proxyCallbacks from './proxy-callbacks.mjs'; import { Sandbox } from './sandbox.mjs'; -import { bailedTraverse, traverse, zonedTraverse } from './traverse.mjs'; +import { traverse, zonedTraverse } from './traverse.mjs'; + +class State { + #values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + #size = 0; + + initialValue = 0; + + get value() { + return this.#values[this.#size]; + } + + at(index) { + return this.#values.slice(0, this.#size).at(index); + } + + set value(value) { + this.#values[this.#size] = value; + } + + enter() { + this.#size++; + if (this.#values.length === this.#size - 1) { + this.#values.push(0); + } + + this.#values[this.#size] = this.#values[this.#size - 1]; + this.initialValue = this.value; + } + + exit(depth) { + this.#size = Math.max(0, depth - 1); + } +} export default class Scope { #parent; - #output; + #states; constructor(root, callbacks, parent = null) { this.root = root; this.#parent = parent; this.path = []; this.errors = []; - const sandbox = (this.sandbox = new Sandbox(this.path, root, null)); + this.#states = []; + this.sandbox = new Sandbox(this.path, root, null); this.callbacks = proxyCallbacks(callbacks, this.errors); - this.#output = { - path: this.path, - get value() { - return sandbox.value; - }, - }; - } - - get depth() { - return this.path.length - 1; - } - - get property() { - return this.sandbox.property; } - get value() { - return this.sandbox.value; + allocState() { + const state = new State(); + this.#states.push(state); + return state; } enter(key) { this.path.push(key); this.sandbox = this.sandbox.push(); - return this.path.length; + const length = this.path.length; + for (let i = 0; i < this.#states.length; i++) { + const state = this.#states[i]; + state.enter(key); + } + + return length; } exit(depth) { @@ -48,7 +76,12 @@ export default class Scope { this.sandbox = this.sandbox.pop(); - return this.path.length; + for (let i = 0; i < this.#states.length; i++) { + const state = this.#states[i]; + state.exit(depth); + } + + return length; } fork(path) { @@ -56,7 +89,7 @@ export default class Scope { for (const segment of path) { newScope.enter(segment); - if (newScope.value === void 0) { + if (newScope.sandbox.value === void 0) { return null; } } @@ -72,48 +105,43 @@ export default class Scope { } } - bail(id, fn, deps) { - const scope = this.fork(this.path); - bailedTraverse.call(scope, fn, deps); - } - emit(id, pos, withKeys) { const fn = this.callbacks[id]; if (pos === 0 && !withKeys) { - return void fn(this.#output); + return void fn({ + path: this.path.slice(), + value: this.sandbox.value, + }); } - if (pos !== 0 && pos > this.depth + 1) { + if (pos !== 0 && pos > this.path.length) { return; } - const output = - pos === 0 - ? this.#output - : { - path: this.#output.path.slice( - 0, - Math.max(0, this.#output.path.length - pos), - ), - value: (this.sandbox.at(-pos - 1) ?? this.sandbox.at(0)).value, - }; + let path; + let value; + if (pos > 0) { + path = this.path.slice(0, Math.max(0, this.path.length - pos)); + value = (this.sandbox.at(-pos - 1) ?? this.sandbox.at(0)).value; + } else { + path = this.path.slice(); + value = this.sandbox.value; + } if (!withKeys) { - fn(output); + fn({ path, value }); } else { fn({ - path: output.path, - value: - output.path.length === 0 - ? void 0 - : output.path[output.path.length - 1], + path, + value: path.length === 0 ? void 0 : path[path.length - 1], }); } } destroy() { this.path.length = 0; + this.#states.length = 0; this.sandbox.destroy(); this.sandbox = null; diff --git a/src/runtime/traverse.mjs b/src/runtime/traverse.mjs index 4a25ecd..9d811c6 100644 --- a/src/runtime/traverse.mjs +++ b/src/runtime/traverse.mjs @@ -1,59 +1,40 @@ import isObject from './codegen-functions/is-object.mjs'; -function _traverseBody(key, curObj, scope, cb, deps) { +function _traverseBody(key, curObj, scope, cb) { const value = curObj[key]; const pos = scope.enter(key); - const matched = deps !== null && deps.length > 0 && !deps[0].fn(scope); - if (deps === null || (deps.length === 1 && matched)) { - cb(scope); - } - - if (!isObject(value)) { - // no-op - } else if (deps === null) { - _traverse(value, scope, cb, deps); - } else if (deps.length > 0) { - if (matched) { - _traverse(value, scope, cb, deps.slice(1)); - } + cb(scope); - if (deps[0].deep) { - scope.exit(pos); - scope.enter(key); - _traverse(value, scope, cb, deps); - } + if (isObject(value)) { + _traverse(value, scope, cb); } scope.exit(pos); } -function _traverse(curObj, scope, cb, deps) { +function _traverse(curObj, scope, cb) { if (Array.isArray(curObj)) { for (let i = 0; i < curObj.length; i++) { - _traverseBody(i, curObj, scope, cb, deps); + _traverseBody(i, curObj, scope, cb); } } else { for (const key of Object.keys(curObj)) { - _traverseBody(key, curObj, scope, cb, deps); + _traverseBody(key, curObj, scope, cb); } } } export function traverse(cb) { - _traverse(this.root, this, cb, null); -} - -export function bailedTraverse(cb, deps) { - _traverse(this.value, this, cb, deps); + _traverse(this.root, this, cb); } export function zonedTraverse(cb, zones) { if (isSaneObject(this.root)) { zonesRegistry.set(this.root, zones); - _traverse(new Proxy(this.root, traps), this, cb, null); + _traverse(new Proxy(this.root, traps), this, cb); } else { - _traverse(this.root, this, cb, null); + _traverse(this.root, this, cb); } }