@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.
Files changed (39) hide show
  1. package/index.ts +19 -4
  2. package/main.js +5 -2
  3. package/package.json +2 -2
  4. package/src/compiler/compile.js +11 -3
  5. package/src/compiler/origami.pegjs +4 -2
  6. package/src/compiler/parse.js +77 -68
  7. package/src/compiler/parserHelpers.js +16 -17
  8. package/src/handlers/getPackedPath.js +17 -0
  9. package/src/handlers/jpeg_handler.js +5 -0
  10. package/src/handlers/js_handler.js +3 -3
  11. package/src/handlers/json_handler.js +3 -1
  12. package/src/handlers/tsv_handler.js +1 -1
  13. package/src/handlers/yaml_handler.js +9 -3
  14. package/src/project/jsGlobals.js +3 -12
  15. package/src/protocols/package.js +20 -11
  16. package/src/runtime/asyncStorage.js +7 -0
  17. package/src/runtime/codeFragment.js +4 -3
  18. package/src/runtime/errors.js +86 -128
  19. package/src/runtime/evaluate.js +8 -77
  20. package/src/runtime/execute.js +85 -0
  21. package/src/runtime/explainReferenceError.js +248 -0
  22. package/src/runtime/explainTraverseError.js +77 -0
  23. package/src/runtime/expressionFunction.js +8 -7
  24. package/src/runtime/expressionObject.js +4 -3
  25. package/src/runtime/handleExtension.js +22 -8
  26. package/src/runtime/internal.js +1 -1
  27. package/src/runtime/interop.js +15 -0
  28. package/src/runtime/ops.js +51 -21
  29. package/src/runtime/symbols.js +0 -1
  30. package/src/runtime/typos.js +22 -3
  31. package/test/compiler/compile.test.js +7 -103
  32. package/test/compiler/parse.test.js +42 -39
  33. package/test/project/fixtures/withPackageJson/subfolder/README.md +1 -0
  34. package/test/runtime/errors.test.js +296 -0
  35. package/test/runtime/evaluate.test.js +110 -34
  36. package/test/runtime/execute.test.js +41 -0
  37. package/test/runtime/expressionObject.test.js +3 -3
  38. package/test/runtime/ops.test.js +42 -35
  39. 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: evaluate() supports a related but separate
39
- // mechanism called `containerAsTarget`. We want to use binding here so that, if
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("Tried to parse something as JSON but it wasn't text.");
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(".tsv handler can only unpack text");
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("Tried to parse something as YAML but it wasn't text.");
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
- // Resolve any promises in the data.
86
- data = await Tree.plain(data);
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)) {
@@ -128,7 +128,7 @@ const globals = {
128
128
  encodeURIComponent,
129
129
  escape,
130
130
  eval,
131
- // fetch -- special case, see below
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.containerAsTarget = true;
182
+ importWrapper.parentAsTarget = true;
192
183
 
193
184
  export default globals;
@@ -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 packageRootPath = ["node_modules"];
15
- const name = args.shift();
16
- packageRootPath.push(name);
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
- packageRootPath.push(args.shift());
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
- const packageRoot = await Tree.traverse(root, ...packageRootPath);
26
+ let packageRoot = await Tree.traverse(root, ...packageRootKeys);
24
27
  if (!packageRoot) {
25
- throw new Error(`Can't find ${packageRootPath.join("/")}`);
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 (!mainPath) {
41
+ if (mainPath === undefined) {
31
42
  throw new Error(
32
- `${packageRootPath.join(
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
 
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * Storage maintained by the execute() function and accessible to async
5
+ * functions called during evaluation.
6
+ */
7
+ export default new AsyncLocalStorage();
@@ -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
- ? source.text.slice(start.offset, end.offset)
6
+ start && end && start.offset < end.offset
7
+ ? sourceText.slice(start.offset, end.offset)
7
8
  : // Use entire source
8
- source.text;
9
+ sourceText;
9
10
 
10
11
  // Replace newlines and whitespace runs with a single space.
11
12
  fragment = fragment.replace(/(\n|\s\s+)+/g, " ");
@@ -1,148 +1,116 @@
1
- // Text we look for in an error stack to guess whether a given line represents a
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 * as symbols from "./symbols.js";
13
- import { typos } from "./typos.js";
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.evaluate",
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
- let message;
79
-
80
- let location = /** @type {any} */ (error).location;
81
- const fragment = location ? codeFragment(location) : null;
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
- if (error.stack) {
85
- // Display the stack only until we reach the Origami source code.
86
- message = "";
87
- let lines = error.stack.split("\n");
88
- for (let i = 0; i < lines.length; i++) {
89
- let line = lines[i];
90
- if (maybeOrigamiSourceCode(line)) {
91
- break;
92
- }
93
- if (
94
- error instanceof TraverseError &&
95
- error.message === "A null or undefined value can't be traversed"
96
- ) {
97
- // Provide more meaningful message for TraverseError
98
- line = `TraverseError: This part of the path is null or undefined: ${highlightError(
99
- fragment
100
- )}`;
101
- fragmentInMessage = true;
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
- if (message) {
104
- message += "\n";
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
- } else {
109
- message = error.toString();
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
- if (!fragmentInMessage) {
115
- message += `\nevaluating: ${highlightError(fragment)}`;
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
- const { url } = source;
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 `\n at ${fileRef}:${line}:${column}`;
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 `\n at line ${line}, column ${column}`;
152
+ return ` at line ${line}, column ${column}`;
182
153
  } else {
183
- return "";
154
+ return null;
184
155
  }
185
156
  }
186
157
 
187
- export async function scopeReferenceError(scope, key) {
188
- const messages = [
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
  }
@@ -1,83 +1,14 @@
1
- import { Tree, isUnpackable } from "@weborigami/async-tree";
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
- * Evaluate the given code and return the result.
4
+ * Compile the given source code and evaluate it.
8
5
  *
9
- * `this` should be the tree used as the context for the evaluation.
10
- *
11
- * @param {import("../../index.ts").AnnotatedCode} code
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(code, state = {}) {
15
- if (!(code instanceof Array)) {
16
- // Simple scalar; return as is.
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
+ }