@weborigami/language 0.6.9 → 0.6.11
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 +16 -17
- 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 +9 -3
- package/src/project/jsGlobals.js +3 -12
- package/src/protocols/package.js +20 -11
- package/src/runtime/asyncStorage.js +7 -0
- package/src/runtime/codeFragment.js +4 -3
- package/src/runtime/errors.js +86 -128
- package/src/runtime/evaluate.js +8 -77
- package/src/runtime/execute.js +85 -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 +4 -3
- 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 +51 -21
- 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 +42 -39
- 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 +3 -3
- package/test/runtime/ops.test.js +42 -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
castArraylike,
|
|
2
3
|
getParent,
|
|
3
4
|
isUnpackable,
|
|
4
5
|
symbols,
|
|
@@ -39,7 +40,7 @@ export default {
|
|
|
39
40
|
async unpack(packed, options = {}) {
|
|
40
41
|
const yaml = toString(packed);
|
|
41
42
|
if (!yaml) {
|
|
42
|
-
throw new Error("
|
|
43
|
+
throw new Error("YAML handler can only unpack text.");
|
|
43
44
|
}
|
|
44
45
|
const parent = getParent(packed, options);
|
|
45
46
|
const oriCallTag = await oriCallTagForParent(parent, options, yaml);
|
|
@@ -82,8 +83,13 @@ export default {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (hasOriTags) {
|
|
85
|
-
//
|
|
86
|
-
|
|
86
|
+
// Invoke any functions and resolve any promises in the deep data.
|
|
87
|
+
const tree = Tree.from(data, { deep: true });
|
|
88
|
+
data = await Tree.mapReduce(
|
|
89
|
+
tree,
|
|
90
|
+
async (value) => (value instanceof Function ? await value() : value),
|
|
91
|
+
(mapped) => castArraylike(mapped),
|
|
92
|
+
);
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
if (data && typeof data === "object" && Object.isExtensible(data)) {
|
package/src/project/jsGlobals.js
CHANGED
|
@@ -128,7 +128,7 @@ const globals = {
|
|
|
128
128
|
encodeURIComponent,
|
|
129
129
|
escape,
|
|
130
130
|
eval,
|
|
131
|
-
|
|
131
|
+
fetch,
|
|
132
132
|
globalThis,
|
|
133
133
|
isFinite,
|
|
134
134
|
isNaN,
|
|
@@ -150,7 +150,6 @@ const globals = {
|
|
|
150
150
|
true: true,
|
|
151
151
|
|
|
152
152
|
// Special cases
|
|
153
|
-
fetch: fetchWrapper,
|
|
154
153
|
import: importWrapper,
|
|
155
154
|
};
|
|
156
155
|
|
|
@@ -160,14 +159,6 @@ Object.defineProperty(globals, "globalThis", {
|
|
|
160
159
|
value: globals,
|
|
161
160
|
});
|
|
162
161
|
|
|
163
|
-
async function fetchWrapper(resource, options) {
|
|
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()."
|
|
166
|
-
);
|
|
167
|
-
const response = await fetch(resource, options);
|
|
168
|
-
return response.ok ? await response.arrayBuffer() : undefined;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
162
|
/**
|
|
172
163
|
* @typedef {import("@weborigami/async-tree").AsyncMap} AsyncMap
|
|
173
164
|
*
|
|
@@ -182,12 +173,12 @@ async function importWrapper(modulePath, options = {}) {
|
|
|
182
173
|
}
|
|
183
174
|
if (!current) {
|
|
184
175
|
throw new TypeError(
|
|
185
|
-
"Modules can only be imported from a folder or other object with a path property."
|
|
176
|
+
"Modules can only be imported from a folder or other object with a path property.",
|
|
186
177
|
);
|
|
187
178
|
}
|
|
188
179
|
const filePath = path.resolve(current.path, modulePath);
|
|
189
180
|
return import(filePath, options);
|
|
190
181
|
}
|
|
191
|
-
importWrapper.
|
|
182
|
+
importWrapper.parentAsTarget = true;
|
|
192
183
|
|
|
193
184
|
export default globals;
|
package/src/protocols/package.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tree, keysFromPath } from "@weborigami/async-tree";
|
|
1
|
+
import { Tree, keysFromPath, pathFromKeys } from "@weborigami/async-tree";
|
|
2
2
|
import projectRoot from "../project/projectRoot.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,27 +11,36 @@ export default async function packageProtocol(...args) {
|
|
|
11
11
|
const root = await projectRoot(state);
|
|
12
12
|
|
|
13
13
|
// Identify the path to the package root
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const packageRootKeys = ["node_modules"];
|
|
15
|
+
let name = args.shift();
|
|
16
|
+
packageRootKeys.push(name);
|
|
17
17
|
if (name.startsWith("@")) {
|
|
18
18
|
// First key is an npm organization, add next key as name
|
|
19
|
-
|
|
19
|
+
const nameArg = args.shift();
|
|
20
|
+
name += nameArg;
|
|
21
|
+
packageRootKeys.push(nameArg);
|
|
20
22
|
}
|
|
23
|
+
const packageRootPath = pathFromKeys(packageRootKeys);
|
|
21
24
|
|
|
22
25
|
// Get the package root (top level folder of the package)
|
|
23
|
-
|
|
26
|
+
let packageRoot = await Tree.traverse(root, ...packageRootKeys);
|
|
24
27
|
if (!packageRoot) {
|
|
25
|
-
|
|
28
|
+
// Can't find package -- are we *in* the package?
|
|
29
|
+
const packageJson = await Tree.traverse(root, "package.json");
|
|
30
|
+
const packageData = await packageJson?.unpack();
|
|
31
|
+
if (packageData?.name === name) {
|
|
32
|
+
// Yes, we're in the package itself
|
|
33
|
+
packageRoot = root;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Can't find ${packageRootPath}`);
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
// Identify the main entry point
|
|
29
40
|
const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
|
|
30
|
-
if (
|
|
41
|
+
if (mainPath === undefined) {
|
|
31
42
|
throw new Error(
|
|
32
|
-
`${packageRootPath.
|
|
33
|
-
"/"
|
|
34
|
-
)} doesn't contain a package.json with a "main" entry.`
|
|
43
|
+
`${packageRootPath} doesn't contain a package.json with a "main" entry.`,
|
|
35
44
|
);
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -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,116 @@
|
|
|
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
|
-
let fragmentInMessage = false;
|
|
22
|
+
export async function formatError(error) {
|
|
23
|
+
// We want to display information for the root cause
|
|
24
|
+
while (error.cause instanceof Error) {
|
|
25
|
+
error = error.cause;
|
|
26
|
+
}
|
|
83
27
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
28
|
+
// Get the original error message
|
|
29
|
+
let originalMessage;
|
|
30
|
+
// If the first line of the stack is just the error message, use that as the message
|
|
31
|
+
let lines = error.stack?.split("\n") ?? [];
|
|
32
|
+
if (!lines[0].startsWith(" at")) {
|
|
33
|
+
originalMessage = lines[0];
|
|
34
|
+
lines.shift();
|
|
35
|
+
} else {
|
|
36
|
+
originalMessage = error.message ?? error.toString();
|
|
37
|
+
}
|
|
38
|
+
let message = originalMessage;
|
|
39
|
+
|
|
40
|
+
// See if we can identify the Origami location that caused the error
|
|
41
|
+
let location;
|
|
42
|
+
const context = /** @type {any} */ (error).context;
|
|
43
|
+
let code = context?.code;
|
|
44
|
+
if (code) {
|
|
45
|
+
// Use the code being evaluated when the error occurred
|
|
46
|
+
let position = /** @type {any} */ (error).position;
|
|
47
|
+
const argCode =
|
|
48
|
+
position !== undefined ? context.code[position] : context.code;
|
|
49
|
+
if (argCode instanceof Array) {
|
|
50
|
+
code = argCode;
|
|
51
|
+
location = /** @type {any} */ (argCode).location;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const fragment = location ? codeFragment(location) : (code?.source ?? code);
|
|
55
|
+
|
|
56
|
+
// See if we can explain the error message
|
|
57
|
+
try {
|
|
58
|
+
if (error instanceof ReferenceError && code && context) {
|
|
59
|
+
const explanation = await explainReferenceError(code, context.state);
|
|
60
|
+
if (explanation) {
|
|
61
|
+
message += "\n" + explanation;
|
|
102
62
|
}
|
|
103
|
-
|
|
104
|
-
|
|
63
|
+
} else if (error instanceof TraverseError) {
|
|
64
|
+
const explanation = await explainTraverseError(error);
|
|
65
|
+
if (explanation) {
|
|
66
|
+
message += "\n" + explanation;
|
|
105
67
|
}
|
|
106
|
-
message += line;
|
|
107
68
|
}
|
|
108
|
-
}
|
|
109
|
-
|
|
69
|
+
} catch (internalError) {
|
|
70
|
+
// Ignore; won't modify the message
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If the error's `message` starts with a qualified method name like `Tree.map`
|
|
74
|
+
// and a colon, extract the method name and link to the docs.
|
|
75
|
+
const match = error.message?.match(/^(?<namespace>\w+).(?<method>\w+):/);
|
|
76
|
+
if (match) {
|
|
77
|
+
/** @type {any} */
|
|
78
|
+
const { namespace, method } = match.groups;
|
|
79
|
+
if (["Dev", "Origami", "Tree"].includes(namespace)) {
|
|
80
|
+
message += `\nFor documentation, see https://weborigami.org/builtins/${namespace}/${method}`;
|
|
81
|
+
}
|
|
110
82
|
}
|
|
111
83
|
|
|
84
|
+
// If the error has a stack trace, only include the portion until we reach
|
|
85
|
+
// Origami source code.
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
if (maybeOrigamiSourceCode(line)) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
message += "\n" + line;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
message += `\nevaluating: ${highlightError(fragment)}`;
|
|
95
|
+
|
|
112
96
|
// Add location
|
|
113
97
|
if (location) {
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
const lineInformation = lineInfo(location);
|
|
99
|
+
if (lineInformation) {
|
|
100
|
+
message += "\n" + lineInformation;
|
|
116
101
|
}
|
|
117
|
-
message += lineInfo(location);
|
|
118
102
|
}
|
|
119
103
|
|
|
120
104
|
return message;
|
|
121
105
|
}
|
|
122
106
|
|
|
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
107
|
export function highlightError(text) {
|
|
136
108
|
// ANSI escape sequence to highlight text in red
|
|
137
109
|
return `\x1b[31m${text}\x1b[0m`;
|
|
138
110
|
}
|
|
139
111
|
|
|
140
|
-
export function maybeOrigamiSourceCode(text) {
|
|
141
|
-
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
112
|
// Return user-friendly line information for the error location
|
|
145
|
-
function lineInfo(location) {
|
|
113
|
+
export function lineInfo(location) {
|
|
146
114
|
let { source, start } = location;
|
|
147
115
|
|
|
148
116
|
let line;
|
|
@@ -162,7 +130,10 @@ function lineInfo(location) {
|
|
|
162
130
|
}
|
|
163
131
|
|
|
164
132
|
if (typeof source === "object" && source.url) {
|
|
165
|
-
|
|
133
|
+
let { url } = source;
|
|
134
|
+
if (typeof url === "string") {
|
|
135
|
+
url = new URL(url);
|
|
136
|
+
}
|
|
166
137
|
let fileRef;
|
|
167
138
|
// If URL is a file: URL, change to a relative path
|
|
168
139
|
if (url.protocol === "file:") {
|
|
@@ -175,28 +146,15 @@ function lineInfo(location) {
|
|
|
175
146
|
// Not a file: URL, use as is
|
|
176
147
|
fileRef = url.href;
|
|
177
148
|
}
|
|
178
|
-
return
|
|
149
|
+
return ` at ${fileRef}:${line}:${column}`;
|
|
179
150
|
} else if (source.text.includes("\n")) {
|
|
180
151
|
// Don't know the URL, but has multiple lines so add line number
|
|
181
|
-
return
|
|
152
|
+
return ` at line ${line}, column ${column}`;
|
|
182
153
|
} else {
|
|
183
|
-
return
|
|
154
|
+
return null;
|
|
184
155
|
}
|
|
185
156
|
}
|
|
186
157
|
|
|
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);
|
|
158
|
+
export function maybeOrigamiSourceCode(text) {
|
|
159
|
+
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
202
160
|
}
|
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,85 @@
|
|
|
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.needsContext) {
|
|
60
|
+
// The function is an op that wants the code context
|
|
61
|
+
args.push(context);
|
|
62
|
+
} else if (fn.parentAsTarget && state.parent) {
|
|
63
|
+
// The function wants the code's parent as the `this` target
|
|
64
|
+
fn = fn.bind(state.parent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Execute the function or traverse the map.
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
result = await asyncStorage.run(
|
|
71
|
+
context,
|
|
72
|
+
async () =>
|
|
73
|
+
fn instanceof Function
|
|
74
|
+
? await fn(...args) // Invoke the function
|
|
75
|
+
: await Tree.traverseOrThrow(fn, ...args), // Traverse the map.
|
|
76
|
+
);
|
|
77
|
+
} catch (/** @type {any} */ error) {
|
|
78
|
+
if (!error.context) {
|
|
79
|
+
error.context = context; // For error formatting
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|