@weborigami/language 0.6.8 → 0.6.10
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/index.ts +19 -4
- package/main.js +5 -2
- package/package.json +2 -2
- package/src/compiler/compile.js +11 -3
- package/src/compiler/origami.pegjs +4 -2
- package/src/compiler/parse.js +77 -68
- package/src/compiler/parserHelpers.js +13 -13
- package/src/handlers/getPackedPath.js +17 -0
- package/src/handlers/jpeg_handler.js +5 -0
- package/src/handlers/js_handler.js +3 -3
- package/src/handlers/json_handler.js +3 -1
- package/src/handlers/tsv_handler.js +1 -1
- package/src/handlers/yaml_handler.js +1 -1
- package/src/project/jsGlobals.js +3 -3
- package/src/protocols/package.js +3 -3
- package/src/runtime/asyncStorage.js +7 -0
- package/src/runtime/codeFragment.js +4 -3
- package/src/runtime/errors.js +82 -129
- package/src/runtime/evaluate.js +8 -77
- package/src/runtime/execute.js +82 -0
- package/src/runtime/explainReferenceError.js +248 -0
- package/src/runtime/explainTraverseError.js +77 -0
- package/src/runtime/expressionFunction.js +8 -7
- package/src/runtime/expressionObject.js +9 -6
- package/src/runtime/handleExtension.js +22 -8
- package/src/runtime/internal.js +1 -1
- package/src/runtime/interop.js +15 -0
- package/src/runtime/ops.js +24 -19
- package/src/runtime/symbols.js +0 -1
- package/src/runtime/typos.js +22 -3
- package/test/compiler/compile.test.js +7 -103
- package/test/compiler/parse.test.js +38 -31
- package/test/project/fixtures/withPackageJson/subfolder/README.md +1 -0
- package/test/runtime/errors.test.js +296 -0
- package/test/runtime/evaluate.test.js +110 -34
- package/test/runtime/execute.test.js +41 -0
- package/test/runtime/expressionObject.test.js +4 -4
- package/test/runtime/ops.test.js +36 -35
- package/test/runtime/typos.test.js +2 -0
|
@@ -12,7 +12,7 @@ export default {
|
|
|
12
12
|
const { key, parent } = options;
|
|
13
13
|
if (!(parent && "import" in parent)) {
|
|
14
14
|
throw new TypeError(
|
|
15
|
-
"The parent tree must support importing modules to unpack JavaScript files."
|
|
15
|
+
"The parent tree must support importing modules to unpack JavaScript files.",
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -35,8 +35,8 @@ export default {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
// If the value is a function, bind it to the parent so that the function can,
|
|
38
|
-
// e.g., find local files. Note:
|
|
39
|
-
// mechanism called `
|
|
38
|
+
// e.g., find local files. Note: execute() supports a related but separate
|
|
39
|
+
// mechanism called `parentAsTarget`. We want to use binding here so that, if
|
|
40
40
|
// a function is handed to another to be called later, it still has the correct
|
|
41
41
|
// `this`.
|
|
42
42
|
function bindToParent(value, parent) {
|
|
@@ -12,9 +12,11 @@ export default {
|
|
|
12
12
|
unpack(packed) {
|
|
13
13
|
const json = toString(packed);
|
|
14
14
|
if (!json) {
|
|
15
|
-
throw new Error("
|
|
15
|
+
throw new Error("JSON handler can only unpack text.");
|
|
16
16
|
}
|
|
17
|
+
|
|
17
18
|
const data = JSON.parse(json);
|
|
19
|
+
|
|
18
20
|
if (data && typeof data === "object" && Object.isExtensible(data)) {
|
|
19
21
|
Object.defineProperty(data, symbols.deep, {
|
|
20
22
|
enumerable: false,
|
|
@@ -7,7 +7,7 @@ export default {
|
|
|
7
7
|
const parent = options.parent ?? null;
|
|
8
8
|
const text = toString(packed);
|
|
9
9
|
if (text === null) {
|
|
10
|
-
throw new TypeError("
|
|
10
|
+
throw new TypeError("TSV handler can only unpack text.");
|
|
11
11
|
}
|
|
12
12
|
const data = tsvParse(text);
|
|
13
13
|
// Define `parent` as non-enumerable property
|
|
@@ -39,7 +39,7 @@ export default {
|
|
|
39
39
|
async unpack(packed, options = {}) {
|
|
40
40
|
const yaml = toString(packed);
|
|
41
41
|
if (!yaml) {
|
|
42
|
-
throw new Error("
|
|
42
|
+
throw new Error("YAML handler can only unpack text.");
|
|
43
43
|
}
|
|
44
44
|
const parent = getParent(packed, options);
|
|
45
45
|
const oriCallTag = await oriCallTagForParent(parent, options, yaml);
|
package/src/project/jsGlobals.js
CHANGED
|
@@ -162,7 +162,7 @@ Object.defineProperty(globals, "globalThis", {
|
|
|
162
162
|
|
|
163
163
|
async function fetchWrapper(resource, options) {
|
|
164
164
|
console.warn(
|
|
165
|
-
"Warning: A plain `fetch` reference will eventually call the standard JavaScript fetch() function. For Origami's fetch behavior, update your code to call Origami.fetch()."
|
|
165
|
+
"Warning: A plain `fetch` reference will eventually call the standard JavaScript fetch() function. For Origami's fetch behavior, update your code to call Origami.fetch().",
|
|
166
166
|
);
|
|
167
167
|
const response = await fetch(resource, options);
|
|
168
168
|
return response.ok ? await response.arrayBuffer() : undefined;
|
|
@@ -182,12 +182,12 @@ async function importWrapper(modulePath, options = {}) {
|
|
|
182
182
|
}
|
|
183
183
|
if (!current) {
|
|
184
184
|
throw new TypeError(
|
|
185
|
-
"Modules can only be imported from a folder or other object with a path property."
|
|
185
|
+
"Modules can only be imported from a folder or other object with a path property.",
|
|
186
186
|
);
|
|
187
187
|
}
|
|
188
188
|
const filePath = path.resolve(current.path, modulePath);
|
|
189
189
|
return import(filePath, options);
|
|
190
190
|
}
|
|
191
|
-
importWrapper.
|
|
191
|
+
importWrapper.parentAsTarget = true;
|
|
192
192
|
|
|
193
193
|
export default globals;
|
package/src/protocols/package.js
CHANGED
|
@@ -27,11 +27,11 @@ export default async function packageProtocol(...args) {
|
|
|
27
27
|
|
|
28
28
|
// Identify the main entry point
|
|
29
29
|
const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
|
|
30
|
-
if (
|
|
30
|
+
if (mainPath === undefined) {
|
|
31
31
|
throw new Error(
|
|
32
32
|
`${packageRootPath.join(
|
|
33
|
-
"/"
|
|
34
|
-
)} doesn't contain a package.json with a "main" entry
|
|
33
|
+
"/",
|
|
34
|
+
)} doesn't contain a package.json with a "main" entry.`,
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export default function codeFragment(location) {
|
|
2
2
|
const { source, start, end } = location;
|
|
3
|
+
const sourceText = source.text ?? source;
|
|
3
4
|
|
|
4
5
|
let fragment =
|
|
5
|
-
start.offset < end.offset
|
|
6
|
-
?
|
|
6
|
+
start && end && start.offset < end.offset
|
|
7
|
+
? sourceText.slice(start.offset, end.offset)
|
|
7
8
|
: // Use entire source
|
|
8
|
-
|
|
9
|
+
sourceText;
|
|
9
10
|
|
|
10
11
|
// Replace newlines and whitespace runs with a single space.
|
|
11
12
|
fragment = fragment.replace(/(\n|\s\s+)+/g, " ");
|
package/src/runtime/errors.js
CHANGED
|
@@ -1,148 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
box,
|
|
5
|
-
trailingSlash,
|
|
6
|
-
TraverseError,
|
|
7
|
-
Tree,
|
|
8
|
-
} from "@weborigami/async-tree";
|
|
1
|
+
import { TraverseError } from "@weborigami/async-tree";
|
|
9
2
|
import path from "node:path";
|
|
10
3
|
import { fileURLToPath } from "node:url";
|
|
11
4
|
import codeFragment from "./codeFragment.js";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
5
|
+
import explainReferenceError from "./explainReferenceError.js";
|
|
6
|
+
import explainTraverseError from "./explainTraverseError.js";
|
|
14
7
|
|
|
8
|
+
// Text we look for in an error stack to guess whether a given line represents a
|
|
15
9
|
// function in the Origami source code.
|
|
16
10
|
const origamiSourceSignals = [
|
|
17
11
|
"async-tree/src/",
|
|
18
12
|
"language/src/",
|
|
19
13
|
"origami/src/",
|
|
20
|
-
"at Scope.
|
|
14
|
+
"at Scope.execute",
|
|
21
15
|
];
|
|
22
16
|
|
|
23
|
-
const displayedWarnings = new Set();
|
|
24
|
-
|
|
25
|
-
export function attachWarning(value, message) {
|
|
26
|
-
if (value == null) {
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (typeof value === "object" && value?.[symbols.warningSymbol]) {
|
|
31
|
-
// Already has a warning, don't overwrite it
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const boxed = box(value);
|
|
36
|
-
boxed[symbols.warningSymbol] = message;
|
|
37
|
-
return boxed;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function builtinReferenceError(tree, builtins, key) {
|
|
41
|
-
// See if the key is in scope (but not as a builtin)
|
|
42
|
-
const scope = await Tree.scope(tree);
|
|
43
|
-
const value = await scope.get(key);
|
|
44
|
-
let message;
|
|
45
|
-
if (value === undefined) {
|
|
46
|
-
const messages = [
|
|
47
|
-
`"${key}" is being called as if it were a builtin function, but it's not.`,
|
|
48
|
-
];
|
|
49
|
-
const typos = await formatScopeTypos(builtins, key);
|
|
50
|
-
messages.push(typos);
|
|
51
|
-
message = messages.join(" ");
|
|
52
|
-
} else {
|
|
53
|
-
const messages = [
|
|
54
|
-
`To call a function like "${key}" that's not a builtin, include a slash: ${key}/( )`,
|
|
55
|
-
`Details: https://weborigami.org/language/syntax.html#shorthand-for-builtin-functions`,
|
|
56
|
-
];
|
|
57
|
-
message = messages.join("\n");
|
|
58
|
-
}
|
|
59
|
-
return new ReferenceError(message);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Display a warning message in the console, but only once for each unique
|
|
63
|
-
// message and location.
|
|
64
|
-
export function displayWarning(message, location) {
|
|
65
|
-
const warning = "Warning: " + message + lineInfo(location);
|
|
66
|
-
if (!displayedWarnings.has(warning)) {
|
|
67
|
-
displayedWarnings.add(warning);
|
|
68
|
-
console.warn(warning);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
17
|
/**
|
|
73
18
|
* Format an error for display in the console.
|
|
74
19
|
*
|
|
75
20
|
* @param {Error} error
|
|
76
21
|
*/
|
|
77
|
-
export function formatError(error) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
22
|
+
export async function formatError(error) {
|
|
23
|
+
// Get the original error message
|
|
24
|
+
let originalMessage;
|
|
25
|
+
// If the first line of the stack is just the error message, use that as the message
|
|
26
|
+
let lines = error.stack?.split("\n") ?? [];
|
|
27
|
+
if (!lines[0].startsWith(" at")) {
|
|
28
|
+
originalMessage = lines[0];
|
|
29
|
+
lines.shift();
|
|
30
|
+
} else {
|
|
31
|
+
originalMessage = error.message ?? error.toString();
|
|
32
|
+
}
|
|
33
|
+
let message = originalMessage;
|
|
34
|
+
|
|
35
|
+
// See if we can identify the Origami location that caused the error
|
|
36
|
+
let location;
|
|
37
|
+
const context = /** @type {any} */ (error).context;
|
|
38
|
+
let code = context?.code;
|
|
39
|
+
if (code) {
|
|
40
|
+
// Use the code being evaluated when the error occurred
|
|
41
|
+
let position = /** @type {any} */ (error).position;
|
|
42
|
+
const argCode =
|
|
43
|
+
position !== undefined ? context.code[position] : context.code;
|
|
44
|
+
if (argCode instanceof Array) {
|
|
45
|
+
code = argCode;
|
|
46
|
+
location = /** @type {any} */ (argCode).location;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const fragment = location ? codeFragment(location) : (code?.source ?? code);
|
|
50
|
+
|
|
51
|
+
// See if we can explain the error message
|
|
52
|
+
try {
|
|
53
|
+
if (error instanceof ReferenceError && code && context) {
|
|
54
|
+
const explanation = await explainReferenceError(code, context.state);
|
|
55
|
+
if (explanation) {
|
|
56
|
+
message += "\n" + explanation;
|
|
102
57
|
}
|
|
103
|
-
|
|
104
|
-
|
|
58
|
+
} else if (error instanceof TraverseError) {
|
|
59
|
+
const explanation = await explainTraverseError(error);
|
|
60
|
+
if (explanation) {
|
|
61
|
+
message += "\n" + explanation;
|
|
105
62
|
}
|
|
106
|
-
message += line;
|
|
107
63
|
}
|
|
108
|
-
}
|
|
109
|
-
|
|
64
|
+
} catch (internalError) {
|
|
65
|
+
// Ignore; won't modify the message
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If the error's `message` starts with a qualified method name like `Tree.map`
|
|
69
|
+
// and a colon, extract the method name and link to the docs.
|
|
70
|
+
const match = error.message?.match(/^(?<namespace>\w+).(?<method>\w+):/);
|
|
71
|
+
if (match) {
|
|
72
|
+
/** @type {any} */
|
|
73
|
+
const { namespace, method } = match.groups;
|
|
74
|
+
if (["Dev", "Origami", "Tree"].includes(namespace)) {
|
|
75
|
+
message += `\nFor documentation, see https://weborigami.org/builtins/${namespace}/${method}`;
|
|
76
|
+
}
|
|
110
77
|
}
|
|
111
78
|
|
|
79
|
+
// If the error has a stack trace, only include the portion until we reach
|
|
80
|
+
// Origami source code.
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
if (maybeOrigamiSourceCode(line)) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
message += "\n" + line;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
message += `\nevaluating: ${highlightError(fragment)}`;
|
|
90
|
+
|
|
112
91
|
// Add location
|
|
113
92
|
if (location) {
|
|
114
|
-
|
|
115
|
-
|
|
93
|
+
const lineInformation = lineInfo(location);
|
|
94
|
+
if (lineInformation) {
|
|
95
|
+
message += "\n" + lineInformation;
|
|
116
96
|
}
|
|
117
|
-
message += lineInfo(location);
|
|
118
97
|
}
|
|
119
98
|
|
|
120
99
|
return message;
|
|
121
100
|
}
|
|
122
101
|
|
|
123
|
-
export async function formatScopeTypos(scope, key) {
|
|
124
|
-
const keys = await scopeTypos(scope, key);
|
|
125
|
-
// Don't match deprecated keys
|
|
126
|
-
const filtered = keys.filter((key) => !key.startsWith("@"));
|
|
127
|
-
if (filtered.length === 0) {
|
|
128
|
-
return "";
|
|
129
|
-
}
|
|
130
|
-
const quoted = filtered.map((key) => `"${key}"`);
|
|
131
|
-
const list = quoted.join(", ");
|
|
132
|
-
return `Maybe you meant ${list}?`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
102
|
export function highlightError(text) {
|
|
136
103
|
// ANSI escape sequence to highlight text in red
|
|
137
104
|
return `\x1b[31m${text}\x1b[0m`;
|
|
138
105
|
}
|
|
139
106
|
|
|
140
|
-
export function maybeOrigamiSourceCode(text) {
|
|
141
|
-
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
107
|
// Return user-friendly line information for the error location
|
|
145
|
-
function lineInfo(location) {
|
|
108
|
+
export function lineInfo(location) {
|
|
146
109
|
let { source, start } = location;
|
|
147
110
|
|
|
148
111
|
let line;
|
|
@@ -162,7 +125,10 @@ function lineInfo(location) {
|
|
|
162
125
|
}
|
|
163
126
|
|
|
164
127
|
if (typeof source === "object" && source.url) {
|
|
165
|
-
|
|
128
|
+
let { url } = source;
|
|
129
|
+
if (typeof url === "string") {
|
|
130
|
+
url = new URL(url);
|
|
131
|
+
}
|
|
166
132
|
let fileRef;
|
|
167
133
|
// If URL is a file: URL, change to a relative path
|
|
168
134
|
if (url.protocol === "file:") {
|
|
@@ -175,28 +141,15 @@ function lineInfo(location) {
|
|
|
175
141
|
// Not a file: URL, use as is
|
|
176
142
|
fileRef = url.href;
|
|
177
143
|
}
|
|
178
|
-
return
|
|
144
|
+
return ` at ${fileRef}:${line}:${column}`;
|
|
179
145
|
} else if (source.text.includes("\n")) {
|
|
180
146
|
// Don't know the URL, but has multiple lines so add line number
|
|
181
|
-
return
|
|
147
|
+
return ` at line ${line}, column ${column}`;
|
|
182
148
|
} else {
|
|
183
|
-
return
|
|
149
|
+
return null;
|
|
184
150
|
}
|
|
185
151
|
}
|
|
186
152
|
|
|
187
|
-
export
|
|
188
|
-
|
|
189
|
-
`"${key}" is not in scope or is undefined.`,
|
|
190
|
-
await formatScopeTypos(scope, key),
|
|
191
|
-
];
|
|
192
|
-
const message = messages.join(" ");
|
|
193
|
-
return new ReferenceError(message);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Return all possible typos for `key` in scope
|
|
197
|
-
async function scopeTypos(scope, key) {
|
|
198
|
-
const scopeKeys = [...(await scope.keys())];
|
|
199
|
-
const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key));
|
|
200
|
-
const normalizedKey = trailingSlash.remove(key);
|
|
201
|
-
return typos(normalizedKey, normalizedScopeKeys);
|
|
153
|
+
export function maybeOrigamiSourceCode(text) {
|
|
154
|
+
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
202
155
|
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,83 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import codeFragment from "./codeFragment.js";
|
|
3
|
-
import { displayWarning } from "./errors.js";
|
|
4
|
-
import * as symbols from "./symbols.js";
|
|
1
|
+
import * as compile from "../compiler/compile.js";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
4
|
+
* Compile the given source code and evaluate it.
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {import("../../index.ts").RuntimeState} [state]
|
|
6
|
+
* @typedef {import("../../index.ts").Source} Source
|
|
7
|
+
* @param {Source|string} source
|
|
8
|
+
* @param {any} [options]
|
|
13
9
|
*/
|
|
14
|
-
export default async function evaluate(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return code;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let evaluated;
|
|
21
|
-
if (code[0]?.unevaluatedArgs) {
|
|
22
|
-
// Don't evaluate instructions, use as is.
|
|
23
|
-
evaluated = code;
|
|
24
|
-
} else {
|
|
25
|
-
// Evaluate each instruction in the code.
|
|
26
|
-
evaluated = await Promise.all(
|
|
27
|
-
code.map((instruction) => evaluate(instruction, state)),
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// The head of the array is a function or a tree; the rest are args or keys.
|
|
32
|
-
let [fn, ...args] = evaluated;
|
|
33
|
-
|
|
34
|
-
if (!fn) {
|
|
35
|
-
// The code wants to invoke something that's couldn't be found in scope.
|
|
36
|
-
const error = ReferenceError(
|
|
37
|
-
`${codeFragment(code[0].location)} is not defined`,
|
|
38
|
-
);
|
|
39
|
-
/** @type {any} */ (error).location = code[0].location;
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (isUnpackable(fn)) {
|
|
44
|
-
// Unpack the object and use the result as the function or tree.
|
|
45
|
-
fn = await fn.unpack();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (fn.needsState) {
|
|
49
|
-
// The function is an op that wants the runtime state
|
|
50
|
-
args.push(state);
|
|
51
|
-
} else if (fn.containerAsTarget && state.parent) {
|
|
52
|
-
// The function wants the code's container as the `this` target
|
|
53
|
-
fn = fn.bind(state.parent);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Execute the function or traverse the tree.
|
|
57
|
-
let result;
|
|
58
|
-
try {
|
|
59
|
-
result =
|
|
60
|
-
fn instanceof Function
|
|
61
|
-
? await fn(...args) // Invoke the function
|
|
62
|
-
: await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
|
|
63
|
-
} catch (/** @type {any} */ error) {
|
|
64
|
-
if (!error.location) {
|
|
65
|
-
// Attach the location of the code we tried to evaluate.
|
|
66
|
-
error.location =
|
|
67
|
-
error.position !== undefined && code[error.position + 1]?.location
|
|
68
|
-
? // Use location of the argument with the given position (need to
|
|
69
|
-
// offset by 1 to skip the function).
|
|
70
|
-
code[error.position + 1]?.location
|
|
71
|
-
: // Use overall location.
|
|
72
|
-
code.location;
|
|
73
|
-
}
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (result?.[symbols.warningSymbol]) {
|
|
78
|
-
displayWarning(result[symbols.warningSymbol], code.location);
|
|
79
|
-
delete result[symbols.warningSymbol];
|
|
80
|
-
}
|
|
81
|
-
|
|
10
|
+
export default async function evaluate(source, options = {}) {
|
|
11
|
+
const fn = compile.expression(source, options);
|
|
12
|
+
const result = await fn();
|
|
82
13
|
return result;
|
|
83
14
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isUnpackable, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import asyncStorage from "./asyncStorage.js";
|
|
3
|
+
import "./interop.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute the given code and return the result.
|
|
7
|
+
*
|
|
8
|
+
* `this` should be the map used as the context for the evaluation.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode
|
|
11
|
+
* @typedef {import("../../index.ts").RuntimeState} RuntimeState
|
|
12
|
+
*
|
|
13
|
+
* @param {AnnotatedCode} code
|
|
14
|
+
* @param {RuntimeState} [state]
|
|
15
|
+
*/
|
|
16
|
+
export default async function execute(code, state = {}) {
|
|
17
|
+
if (!(code instanceof Array)) {
|
|
18
|
+
// Simple scalar; return as is.
|
|
19
|
+
return code;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let evaluated;
|
|
23
|
+
if (code[0]?.unevaluatedArgs) {
|
|
24
|
+
// Don't evaluate instructions, use as is.
|
|
25
|
+
evaluated = code;
|
|
26
|
+
} else {
|
|
27
|
+
// Evaluate each instruction in the code.
|
|
28
|
+
evaluated = await Promise.all(
|
|
29
|
+
code.map((instruction) => execute(instruction, state)),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Add the code to the runtime state
|
|
34
|
+
/** @type {import("../../index.ts").CodeContext} */
|
|
35
|
+
const context = { state, code };
|
|
36
|
+
|
|
37
|
+
// The head of the array is a function or a map; the rest are args or keys.
|
|
38
|
+
let [fn, ...args] = evaluated;
|
|
39
|
+
|
|
40
|
+
if (!fn) {
|
|
41
|
+
// The code wants to invoke something that's couldn't be found in scope.
|
|
42
|
+
/** @type {any} */
|
|
43
|
+
const error = new ReferenceError(
|
|
44
|
+
"Couldn't find the function or map to execute.",
|
|
45
|
+
);
|
|
46
|
+
error.context = context; // For error formatting
|
|
47
|
+
error.position = 0; // Problem was at function position
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isUnpackable(fn)) {
|
|
52
|
+
// Unpack the object and use the result as the function or map.
|
|
53
|
+
fn = await fn.unpack();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (fn.needsState) {
|
|
57
|
+
// The function is an op that wants the runtime state
|
|
58
|
+
args.push(state);
|
|
59
|
+
} else if (fn.parentAsTarget && state.parent) {
|
|
60
|
+
// The function wants the code's parent as the `this` target
|
|
61
|
+
fn = fn.bind(state.parent);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Execute the function or traverse the map.
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = await asyncStorage.run(
|
|
68
|
+
context,
|
|
69
|
+
async () =>
|
|
70
|
+
fn instanceof Function
|
|
71
|
+
? await fn(...args) // Invoke the function
|
|
72
|
+
: await Tree.traverseOrThrow(fn, ...args), // Traverse the map.
|
|
73
|
+
);
|
|
74
|
+
} catch (/** @type {any} */ error) {
|
|
75
|
+
if (!error.context) {
|
|
76
|
+
error.context = context; // For error formatting
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|