@weborigami/language 0.7.0-beta.1 → 0.7.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +1 -0
- package/package.json +3 -2
- package/src/compiler/compile.js +0 -3
- package/src/compiler/optimize.js +56 -23
- package/src/compiler/parserHelpers.js +11 -8
- package/src/handlers/addExtensionKeyFn.js +18 -0
- package/src/handlers/epub_handler.js +54 -0
- package/src/handlers/getSource.js +1 -0
- package/src/handlers/handlers.js +2 -0
- package/src/handlers/ori_handler.js +2 -2
- package/src/handlers/oridocument_handler.js +5 -21
- package/src/handlers/{processOriExport.js → processOrigamiExport.js} +1 -1
- package/src/handlers/zip_handler.js +112 -0
- package/src/runtime/AsyncCacheTransform.d.ts +5 -1
- package/src/runtime/AsyncCacheTransform.js +22 -25
- package/src/runtime/OrigamiFileMap.js +3 -3
- package/src/runtime/SyncCacheTransform.d.ts +2 -1
- package/src/runtime/SyncCacheTransform.js +31 -34
- package/src/runtime/SystemCacheMap.js +28 -17
- package/src/runtime/counters.js +3 -0
- package/src/runtime/enableValueCaching.js +8 -7
- package/src/runtime/errors.js +33 -1
- package/src/runtime/execute.js +3 -0
- package/src/runtime/explainReferenceError.js +11 -24
- package/src/runtime/explainTraverseError.js +15 -6
- package/src/runtime/expressionObject.js +53 -34
- package/src/runtime/handleExtension.js +18 -9
- package/src/runtime/ops.js +4 -3
- package/test/compiler/compile.test.js +0 -1
- package/test/compiler/optimize.test.js +60 -15
- package/test/compiler/parse.test.js +38 -9
- package/test/handlers/epub_handler.test.js +27 -0
- package/test/handlers/fixtures/test.zip +0 -0
- package/test/handlers/zip_handler.test.js +45 -0
- package/test/runtime/AsyncCacheTransform.test.js +9 -4
- package/test/runtime/SyncCacheTransform.test.js +4 -0
- package/test/runtime/asyncCalcs.js +26 -4
- package/test/runtime/errors.test.js +4 -4
- package/test/runtime/expressionObject.test.js +23 -12
- package/test/runtime/handleExtension.test.js +0 -2
- package/test/runtime/ops.test.js +5 -3
- package/test/runtime/syncCalcs.js +26 -3
- package/test/runtime/systemCache.test.js +4 -0
|
@@ -45,28 +45,25 @@ export default function SyncCacheTransform(Base) {
|
|
|
45
45
|
this.cache = systemCache;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
get cachePath() {
|
|
49
|
-
// @ts-ignore
|
|
50
|
-
return this[cachePathSymbol];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
48
|
cachePathForKey(key) {
|
|
54
49
|
return key === "."
|
|
55
|
-
?
|
|
56
|
-
|
|
50
|
+
? // @ts-ignore
|
|
51
|
+
this[cachePathSymbol]
|
|
52
|
+
: // @ts-ignore
|
|
53
|
+
SystemCacheMap.joinPath(this[cachePathSymbol], key);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
delete(key) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
56
|
+
// delete(key) {
|
|
57
|
+
// const deleted = super.delete(key);
|
|
58
|
+
// if (typeof key === "string") {
|
|
59
|
+
// systemCache.delete(this.cachePathForKey(key));
|
|
60
|
+
// if (deleted) {
|
|
61
|
+
// // Deleted an existing key, need to invalidate cached keys
|
|
62
|
+
// this.invalidateKeys();
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
// return deleted;
|
|
66
|
+
// }
|
|
70
67
|
|
|
71
68
|
get(key) {
|
|
72
69
|
if (typeof key !== "string" || key.length === 0) {
|
|
@@ -113,21 +110,21 @@ export default function SyncCacheTransform(Base) {
|
|
|
113
110
|
systemCache.delete(this.cachePathForKey(key));
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
set(key, value) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
113
|
+
// set(key, value) {
|
|
114
|
+
// if (!this._self) {
|
|
115
|
+
// // Initializing in constructor
|
|
116
|
+
// super.set(key, value);
|
|
117
|
+
// return;
|
|
118
|
+
// }
|
|
119
|
+
// if (typeof key !== "string") {
|
|
120
|
+
// return super.set(key, value);
|
|
121
|
+
// }
|
|
122
|
+
// systemCache.delete(this.cachePathForKey(key));
|
|
123
|
+
// if (!this.has(key)) {
|
|
124
|
+
// // Adding a new key, need to invalidate cached keys
|
|
125
|
+
// this.invalidateKeys();
|
|
126
|
+
// }
|
|
127
|
+
// super.set(key, value);
|
|
128
|
+
// }
|
|
132
129
|
};
|
|
133
130
|
}
|
|
@@ -28,15 +28,16 @@ let nextPathId = 0;
|
|
|
28
28
|
export default class SystemCacheMap extends SyncMap {
|
|
29
29
|
delete(path) {
|
|
30
30
|
// Find all entries that depend on this path directly or indirectly
|
|
31
|
-
const
|
|
31
|
+
const dependentEntries = new Map();
|
|
32
|
+
this.visitDependentEntries(path, dependentEntries);
|
|
32
33
|
|
|
33
34
|
// Delete those dependent entries
|
|
34
|
-
for (const deletePath of
|
|
35
|
+
for (const deletePath of dependentEntries.keys()) {
|
|
35
36
|
super.delete(deletePath);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// Remove deleted entries as being downstream from still-existing entries
|
|
39
|
-
for (const [deletePath, deleteEntry] of
|
|
40
|
+
for (const [deletePath, deleteEntry] of dependentEntries.entries()) {
|
|
40
41
|
for (const upstreamPath of deleteEntry.upstreams ?? []) {
|
|
41
42
|
const upstreamEntry = this.get(upstreamPath);
|
|
42
43
|
if (upstreamEntry?.downstreams) {
|
|
@@ -44,6 +45,14 @@ export default class SystemCacheMap extends SyncMap {
|
|
|
44
45
|
if (upstreamEntry.downstreams.size === 0) {
|
|
45
46
|
// No more downstream dependencies, clean up entry
|
|
46
47
|
delete upstreamEntry.downstreams;
|
|
48
|
+
if (upstreamEntry.upstreams?.size === 0) {
|
|
49
|
+
// No more upstream dependencies either, clean up entry
|
|
50
|
+
delete upstreamEntry.upstreams;
|
|
51
|
+
if (!("value" in upstreamEntry)) {
|
|
52
|
+
// No more data in this entry, delete it
|
|
53
|
+
super.delete(upstreamPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
}
|
|
@@ -53,36 +62,38 @@ export default class SystemCacheMap extends SyncMap {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
// Return all entries that directly or indirectly depend on the given path
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
visitDependentEntries(path, dependentEntries) {
|
|
66
|
+
// The entries that will be added for the given path
|
|
67
|
+
const pathAdditions = new Map();
|
|
58
68
|
|
|
59
69
|
const entry = this.get(path);
|
|
60
70
|
if (entry) {
|
|
61
71
|
// Path itself has an entry
|
|
62
|
-
|
|
72
|
+
pathAdditions.set(path, entry);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
// Add all entries with child paths that implicitly depend on this entry
|
|
76
|
+
// REVIEW: This is expensive: it iterates through the entire cache, and is
|
|
77
|
+
// done for each node visit.
|
|
66
78
|
for (const [otherPath, otherEntry] of this.entries()) {
|
|
67
79
|
if (this.isChildPath(path, otherPath)) {
|
|
68
|
-
|
|
80
|
+
pathAdditions.set(otherPath, otherEntry);
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
//
|
|
73
|
-
for (const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
// Add all the direct dependencies
|
|
85
|
+
for (const [dependentPath, dependentEntry] of pathAdditions.entries()) {
|
|
86
|
+
if (!dependentEntries.has(dependentPath)) {
|
|
87
|
+
// Didn't already have this path in the result set
|
|
88
|
+
dependentEntries.set(dependentPath, dependentEntry);
|
|
89
|
+
// Visit all entries downstream of this one
|
|
90
|
+
for (const downstreamPath of dependentEntry.downstreams ?? []) {
|
|
91
|
+
if (!dependentEntries.has(downstreamPath)) {
|
|
92
|
+
this.visitDependentEntries(downstreamPath, dependentEntries);
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
|
-
|
|
85
|
-
return result;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// REVIEW: This doesn't have the correct signature for getOrInsertComputed,
|
|
@@ -19,14 +19,15 @@ const AsyncFunction = async function () {}.constructor;
|
|
|
19
19
|
* @param {string} cachePath
|
|
20
20
|
*/
|
|
21
21
|
export default function enableValueCaching(value, cachePath) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// Value must be a defined object or function, and not have already have a
|
|
23
|
+
// cache path, and be maplike.
|
|
24
|
+
const cachable =
|
|
25
|
+
value &&
|
|
26
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
27
|
+
value[cachePathSymbol] === undefined &&
|
|
28
|
+
Tree.isMaplike(value);
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
value[cachePathSymbol] === undefined && Tree.isMaplike(value);
|
|
29
|
-
if (cacheable) {
|
|
30
|
+
if (cachable) {
|
|
30
31
|
if (isPlainObject(value)) {
|
|
31
32
|
// Expression objects do their own caching
|
|
32
33
|
// TODO: What if it's some other kind of plain object?
|
package/src/runtime/errors.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import codeFragment from "./codeFragment.js";
|
|
5
5
|
import explainReferenceError from "./explainReferenceError.js";
|
|
6
6
|
import explainTraverseError from "./explainTraverseError.js";
|
|
7
|
+
import { ops } from "./internal.js";
|
|
7
8
|
|
|
8
9
|
// Text we look for in an error stack to guess whether a given line represents a
|
|
9
10
|
// function in the Origami source code.
|
|
@@ -61,7 +62,8 @@ export async function formatError(error) {
|
|
|
61
62
|
message += "\n" + explanation;
|
|
62
63
|
}
|
|
63
64
|
} else if (error instanceof TraverseError) {
|
|
64
|
-
|
|
65
|
+
// TraverseError explainer wants the full code
|
|
66
|
+
const explanation = await explainTraverseError(context.code, error);
|
|
65
67
|
if (explanation) {
|
|
66
68
|
message += "\n" + explanation;
|
|
67
69
|
}
|
|
@@ -158,3 +160,33 @@ export function lineInfo(location) {
|
|
|
158
160
|
export function maybeOrigamiSourceCode(text) {
|
|
159
161
|
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
160
162
|
}
|
|
163
|
+
|
|
164
|
+
// Return the keys traversed by the given code, or null if undetermined.
|
|
165
|
+
export function reconstructTraversalKeys(code) {
|
|
166
|
+
const [op, ...args] = code;
|
|
167
|
+
|
|
168
|
+
switch (op) {
|
|
169
|
+
case ops.cache:
|
|
170
|
+
return reconstructTraversalKeys(code[2]);
|
|
171
|
+
|
|
172
|
+
case ops.unpack:
|
|
173
|
+
return reconstructTraversalKeys(code[1]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (op instanceof Array) {
|
|
177
|
+
if (op[0] === ops.scope) {
|
|
178
|
+
// Simple scope reference
|
|
179
|
+
const key = args[0][1];
|
|
180
|
+
return [key];
|
|
181
|
+
} else if (op[0] === ops.cache) {
|
|
182
|
+
// Traversal starting with external scope reference
|
|
183
|
+
const head = op[2][1][1];
|
|
184
|
+
const tail = args.map((code) => code[1]);
|
|
185
|
+
const keys = [head, ...tail];
|
|
186
|
+
return keys;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Can't determine keys
|
|
191
|
+
return null;
|
|
192
|
+
}
|
package/src/runtime/execute.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUnpackable, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import counters from "../runtime/counters.js";
|
|
2
3
|
import executionContext from "./executionContext.js";
|
|
3
4
|
import "./interop.js";
|
|
4
5
|
|
|
@@ -14,6 +15,8 @@ import "./interop.js";
|
|
|
14
15
|
* @param {RuntimeState} [state]
|
|
15
16
|
*/
|
|
16
17
|
export default async function execute(code, state = {}) {
|
|
18
|
+
counters.executions++;
|
|
19
|
+
|
|
17
20
|
if (!(code instanceof Array)) {
|
|
18
21
|
// Simple scalar; return as is.
|
|
19
22
|
return code;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
trailingSlash,
|
|
5
5
|
Tree,
|
|
6
6
|
} from "@weborigami/async-tree";
|
|
7
|
+
import { reconstructTraversalKeys } from "./errors.js";
|
|
7
8
|
import { ops } from "./internal.js";
|
|
8
9
|
import { typos } from "./typos.js";
|
|
9
10
|
|
|
@@ -35,39 +36,25 @@ export default async function explainReferenceError(code, state) {
|
|
|
35
36
|
return explanation;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (code[0] === ops.cache) {
|
|
41
|
-
// External scope reference
|
|
42
|
-
let refCall = code[2];
|
|
43
|
-
if (refCall[0] === ops.unpack) {
|
|
44
|
-
// Unwrap implied unpack
|
|
45
|
-
refCall = refCall[1];
|
|
46
|
-
}
|
|
47
|
-
const scopeArgs = refCall.slice(1); // get the scope reference arguments
|
|
48
|
-
const keys = scopeArgs.map((part) => part[1]);
|
|
49
|
-
const path = pathFromKeys(keys);
|
|
50
|
-
|
|
51
|
-
if (keys.length > 1) {
|
|
52
|
-
return `This path returned undefined: ${path}`;
|
|
53
|
-
}
|
|
54
|
-
key = keys[0];
|
|
55
|
-
} else if (code[0]?.[0] === ops.scope) {
|
|
56
|
-
// Simple scope reference
|
|
57
|
-
key = code[1][1];
|
|
58
|
-
} else {
|
|
59
|
-
// Generic reference error, can't offer help
|
|
39
|
+
const keys = reconstructTraversalKeys(code);
|
|
40
|
+
if (!keys) {
|
|
60
41
|
return null;
|
|
42
|
+
} else if (keys.length > 1) {
|
|
43
|
+
const path = pathFromKeys(keys);
|
|
44
|
+
return `This path returned undefined: ${path}`;
|
|
61
45
|
}
|
|
62
46
|
|
|
63
|
-
key
|
|
47
|
+
// A single key caused the problem, try to explain it
|
|
48
|
+
let key = keys[0];
|
|
64
49
|
if (typeof key !== "string") {
|
|
65
50
|
return null;
|
|
66
51
|
}
|
|
52
|
+
key = trailingSlash.remove(key);
|
|
67
53
|
|
|
68
|
-
//
|
|
54
|
+
// Baseline message
|
|
69
55
|
let message = `"${key}" is not in scope.`;
|
|
70
56
|
|
|
57
|
+
// Various explainers can add suggestions
|
|
71
58
|
const explainers = [
|
|
72
59
|
mathExplainer,
|
|
73
60
|
qualifiedReferenceExplainer,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
TraverseError,
|
|
7
7
|
Tree,
|
|
8
8
|
} from "@weborigami/async-tree";
|
|
9
|
+
import { reconstructTraversalKeys } from "./errors.js";
|
|
9
10
|
import { typos } from "./typos.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -14,7 +15,7 @@ import { typos } from "./typos.js";
|
|
|
14
15
|
*
|
|
15
16
|
* @param {TraverseError} error
|
|
16
17
|
*/
|
|
17
|
-
export default async function explainTraverseError(error) {
|
|
18
|
+
export default async function explainTraverseError(code, error) {
|
|
18
19
|
const { lastValue, keys, position } = error;
|
|
19
20
|
if (lastValue === undefined || keys === undefined || position === undefined) {
|
|
20
21
|
// Don't have sufficient information; shouldn't happen
|
|
@@ -27,9 +28,15 @@ export default async function explainTraverseError(error) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// The key that caused the error is the one before the current position
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
let message
|
|
31
|
+
const problemKey = error.keys[position - 1];
|
|
32
|
+
const reconstructedKeys = reconstructTraversalKeys(code);
|
|
33
|
+
let message;
|
|
34
|
+
if (reconstructedKeys === null) {
|
|
35
|
+
message = `Path traversal stopped unexpectedly at: ${problemKey}`;
|
|
36
|
+
} else {
|
|
37
|
+
const path = pathFromKeys(reconstructedKeys);
|
|
38
|
+
message = `Tried to traverse path: ${path}\nStopped unexpectedly at: ${problemKey}`;
|
|
39
|
+
}
|
|
33
40
|
|
|
34
41
|
const key = trailingSlash.remove(keys[position - 1]);
|
|
35
42
|
|
|
@@ -50,10 +57,12 @@ export default async function explainTraverseError(error) {
|
|
|
50
57
|
const normalizedKeys = lastValueKeys.map(trailingSlash.remove);
|
|
51
58
|
|
|
52
59
|
const keyAsNumber = Number(key);
|
|
53
|
-
if (!isNaN(keyAsNumber)) {
|
|
60
|
+
if (reconstructedKeys && !isNaN(keyAsNumber)) {
|
|
54
61
|
// See if the string version of the key is present
|
|
55
62
|
if (lastValueKeys.includes(keyAsNumber)) {
|
|
56
|
-
const
|
|
63
|
+
const suggestedKeys = reconstructedKeys?.slice(0);
|
|
64
|
+
suggestedKeys[position] = `(${keyAsNumber})`;
|
|
65
|
+
const suggestedPath = pathFromKeys(suggestedKeys);
|
|
57
66
|
message += `\nSlash-separated keys are searched as strings. Here there's no string "${key}" key, but there is a number ${keyAsNumber} key.
|
|
58
67
|
To get the value for that number key, use parentheses: ${suggestedPath}`;
|
|
59
68
|
}
|
|
@@ -25,24 +25,34 @@ const VALUE_TYPE = {
|
|
|
25
25
|
GETTER: 2, // Calculated on demand: `a = fn()`
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const mapUnattachedObjectToIndex = new Map();
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
31
|
* Given an array of entries with string keys and Origami code values (arrays of
|
|
30
32
|
* ops and operands), return an object with the same keys defining properties
|
|
31
33
|
* whose getters evaluate the code.
|
|
32
|
-
|
|
34
|
+
*
|
|
35
|
+
* @param {string|null} cachePath
|
|
33
36
|
* @param {*} entries
|
|
34
37
|
* @param {import("../../index.ts").RuntimeState} [state]
|
|
35
38
|
*/
|
|
36
|
-
export default async function expressionObject(entries, state = {}) {
|
|
39
|
+
export default async function expressionObject(cachePath, entries, state = {}) {
|
|
37
40
|
// Create the object and set its parent
|
|
38
41
|
const object = {};
|
|
39
42
|
const parent = state?.object ?? null;
|
|
40
43
|
if (parent !== null && !Tree.isMap(parent)) {
|
|
41
44
|
throw new TypeError(`Parent must be a map or null`);
|
|
42
45
|
}
|
|
43
|
-
|
|
44
46
|
setParent(object, parent);
|
|
45
47
|
|
|
48
|
+
if (cachePath?.endsWith("/_objects/")) {
|
|
49
|
+
// Unattached object, add a unique index to the cache path
|
|
50
|
+
let index = mapUnattachedObjectToIndex.get(cachePath) ?? 0;
|
|
51
|
+
mapUnattachedObjectToIndex.set(cachePath, index + 1);
|
|
52
|
+
cachePath += index + "/";
|
|
53
|
+
}
|
|
54
|
+
object[cachePathSymbol] = cachePath;
|
|
55
|
+
|
|
46
56
|
// The object in Map form for use on the stack
|
|
47
57
|
const map = new ObjectMap(object);
|
|
48
58
|
|
|
@@ -93,8 +103,7 @@ export default async function expressionObject(entries, state = {}) {
|
|
|
93
103
|
* Define a single property on the object
|
|
94
104
|
*/
|
|
95
105
|
function defineProperty(object, propertyInfo, state, map) {
|
|
96
|
-
|
|
97
|
-
let { enumerable, hasExtension, key, value, valueType } = propertyInfo;
|
|
106
|
+
let { enumerable, key, value, valueType } = propertyInfo;
|
|
98
107
|
if (valueType == VALUE_TYPE.PRIMITIVE) {
|
|
99
108
|
// Define simple property
|
|
100
109
|
Object.defineProperty(object, key, {
|
|
@@ -109,43 +118,53 @@ function defineProperty(object, propertyInfo, state, map) {
|
|
|
109
118
|
configurable: true,
|
|
110
119
|
enumerable,
|
|
111
120
|
get: async () => {
|
|
112
|
-
|
|
113
|
-
const propertyCachePath = getPropertyCachePath(object, key);
|
|
114
|
-
|
|
115
|
-
const newState = Object.assign({}, state, { object: map });
|
|
116
|
-
let result = propertyCachePath
|
|
117
|
-
? await systemCache.getOrInsertComputedAsync(propertyCachePath, () =>
|
|
118
|
-
execute(value, newState),
|
|
119
|
-
)
|
|
120
|
-
: await execute(value, newState);
|
|
121
|
-
|
|
122
|
-
if (hasExtension) {
|
|
123
|
-
// Handle extension
|
|
124
|
-
result = handleExtension(result, key, globals, map);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (valueType === VALUE_TYPE.EAGER) {
|
|
128
|
-
// Memoize result on the object itself
|
|
129
|
-
Object.defineProperty(object, key, {
|
|
130
|
-
configurable: true,
|
|
131
|
-
enumerable,
|
|
132
|
-
value: result,
|
|
133
|
-
writable: true,
|
|
134
|
-
});
|
|
135
|
-
} else if (propertyCachePath) {
|
|
136
|
-
result = enableValueCaching(result, propertyCachePath);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
121
|
+
return executeProperty(object, propertyInfo, state, map);
|
|
140
122
|
},
|
|
141
123
|
});
|
|
142
124
|
}
|
|
143
125
|
}
|
|
144
126
|
|
|
127
|
+
// Execute the property code to get the value of the property
|
|
128
|
+
async function executeProperty(object, propertyInfo, state, map) {
|
|
129
|
+
const { globals } = state;
|
|
130
|
+
let { enumerable, hasExtension, key, value, valueType } = propertyInfo;
|
|
131
|
+
const propertyCachePath = getPropertyCachePath(object, key);
|
|
132
|
+
|
|
133
|
+
const newState = Object.assign({}, state, { object: map });
|
|
134
|
+
const cacheProperty = valueType === VALUE_TYPE.GETTER && propertyCachePath;
|
|
135
|
+
let result = cacheProperty
|
|
136
|
+
? await systemCache.getOrInsertComputedAsync(propertyCachePath, () =>
|
|
137
|
+
execute(value, newState),
|
|
138
|
+
)
|
|
139
|
+
: await execute(value, newState);
|
|
140
|
+
|
|
141
|
+
if (hasExtension) {
|
|
142
|
+
// Handle extension
|
|
143
|
+
result = handleExtension(result, key, globals, map);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (propertyCachePath) {
|
|
147
|
+
// Enable caching on value
|
|
148
|
+
result = enableValueCaching(result, propertyCachePath);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (valueType === VALUE_TYPE.EAGER) {
|
|
152
|
+
// Memoize result on the object itself
|
|
153
|
+
Object.defineProperty(object, key, {
|
|
154
|
+
configurable: true,
|
|
155
|
+
enumerable,
|
|
156
|
+
value: result,
|
|
157
|
+
writable: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
function getPropertyCachePath(object, key) {
|
|
146
165
|
// Follow parent chain looking for a parent that has caching enabled
|
|
147
166
|
let current = object;
|
|
148
|
-
while (current[cachePathSymbol]
|
|
167
|
+
while (current[cachePathSymbol] == null) {
|
|
149
168
|
current = current[symbols.parent];
|
|
150
169
|
if (!current) {
|
|
151
170
|
// Caching isn't enabled on this object tree
|
|
@@ -13,6 +13,9 @@ import { cachePathSymbol } from "./symbols.js";
|
|
|
13
13
|
import systemCache from "./systemCache.js";
|
|
14
14
|
import SystemCacheMap from "./SystemCacheMap.js";
|
|
15
15
|
|
|
16
|
+
// Base class for async functions
|
|
17
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* If the given value is packed (e.g., buffer) and the key is a string-like path
|
|
18
21
|
* that ends in an extension, search for a handler for that extension and, if
|
|
@@ -46,19 +49,27 @@ export default function handleExtension(value, key, handlers, parent = null) {
|
|
|
46
49
|
// Use `in` to look for handle so that, if the handler is a promise, we
|
|
47
50
|
// can still find it without awaiting it here.
|
|
48
51
|
if (handlerName in handlers) {
|
|
49
|
-
let handler = handlers[handlerName];
|
|
50
|
-
|
|
51
52
|
// If the value is a primitive, box it so we can attach data to it.
|
|
52
53
|
value = box(value);
|
|
53
54
|
|
|
54
|
-
if (handler.mediaType) {
|
|
55
|
-
value.mediaType = handler.mediaType;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
55
|
if (parent) {
|
|
59
56
|
setParent(value, parent);
|
|
60
57
|
}
|
|
61
58
|
|
|
59
|
+
// Avoid triggering the loading of the handler if it's async because
|
|
60
|
+
// we may be doing this work inside a sync function.
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
62
|
+
handlers,
|
|
63
|
+
handlerName,
|
|
64
|
+
);
|
|
65
|
+
const lazyHandler = descriptor?.get instanceof AsyncFunction;
|
|
66
|
+
if (!lazyHandler) {
|
|
67
|
+
const mediaType = handlers[handlerName]?.mediaType;
|
|
68
|
+
if (mediaType) {
|
|
69
|
+
value.mediaType = mediaType;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
// Wrap the unpack function so it caches the unpacked value, and so we
|
|
63
74
|
// can add the file path to any errors the unpack function throws.
|
|
64
75
|
const filePath = getPackedPath(value, { key: normalized, parent });
|
|
@@ -74,9 +85,7 @@ export default function handleExtension(value, key, handlers, parent = null) {
|
|
|
74
85
|
const unpackCachePath = trailingSlash.add(fileCachePath);
|
|
75
86
|
value.unpack = async () =>
|
|
76
87
|
systemCache.getOrInsertComputedAsync(unpackCachePath, async () => {
|
|
77
|
-
|
|
78
|
-
handler = await handler;
|
|
79
|
-
}
|
|
88
|
+
let handler = await handlers[handlerName];
|
|
80
89
|
if (isUnpackable(handler)) {
|
|
81
90
|
// The extension handler itself needs to be unpacked
|
|
82
91
|
handler = await handler.unpack();
|
package/src/runtime/ops.js
CHANGED
|
@@ -255,7 +255,7 @@ export function lambda(length, parameters, code, state = {}) {
|
|
|
255
255
|
const interimStack = stack.slice();
|
|
256
256
|
interimStack.push(args);
|
|
257
257
|
const paramState = { ...state, stack: interimStack };
|
|
258
|
-
const frame = await expressionObject(parameters, paramState);
|
|
258
|
+
const frame = await expressionObject(null, parameters, paramState);
|
|
259
259
|
// Record which code this stack frame is associated with
|
|
260
260
|
Object.defineProperty(frame, codeSymbol, {
|
|
261
261
|
value: code,
|
|
@@ -414,11 +414,12 @@ addOpLabel(nullishCoalescing, "«ops.nullishCoalescing»");
|
|
|
414
414
|
* parameter's, and the values will be the results of evaluating the
|
|
415
415
|
* corresponding code values in `obj`.
|
|
416
416
|
*
|
|
417
|
+
* @param {string} cachePath
|
|
417
418
|
* @param {any[]} entries
|
|
418
419
|
*/
|
|
419
|
-
export async function object(...entries) {
|
|
420
|
+
export async function object(cachePath, ...entries) {
|
|
420
421
|
const state = entries.pop();
|
|
421
|
-
return expressionObject(entries, state);
|
|
422
|
+
return expressionObject(cachePath, entries, state);
|
|
422
423
|
}
|
|
423
424
|
addOpLabel(object, "«ops.object»");
|
|
424
425
|
object.unevaluatedArgs = true;
|
|
@@ -16,7 +16,6 @@ const globals = {
|
|
|
16
16
|
* features, which compare the output of evaluate2() against an expected result.
|
|
17
17
|
*/
|
|
18
18
|
describe("compile", () => {
|
|
19
|
-
|
|
20
19
|
test("async object", async () => {
|
|
21
20
|
const fn = compile.expression("{ a: { b = name }}", { globals });
|
|
22
21
|
const object = await fn();
|