bare-script 2.0.2

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/lib/runtime.js ADDED
@@ -0,0 +1,316 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ /** @module lib/runtime */
5
+
6
+ import {defaultMaxStatements, expressionFunctions, scriptFunctions} from './library.js';
7
+
8
+
9
+ /**
10
+ * The BareScript runtime options
11
+ *
12
+ * @typedef {Object} ExecuteScriptOptions
13
+ * @property {boolean} [debug] - If true, execute in debug mode
14
+ * @property {function} [fetchFn] - The [fetch function]{@link module:lib/runtime~FetchFn}
15
+ * @property {Object} [globals] - The global variables
16
+ * @property {function} [logFn] - The [log function]{@link module:lib/runtime~LogFn}
17
+ * @property {number} [maxStatements] - The maximum number of statements; default is 1e9; 0 for no maximum
18
+ * @property {number} [statementCount] - The current statement count
19
+ * @property {function} [urlFn] - The [URL modifier function]{@link module:lib/runtime~URLFn}
20
+ * @property {string} [systemPrefix] - The system include prefix
21
+ */
22
+
23
+ /**
24
+ * The fetch function
25
+ *
26
+ * @callback FetchFn
27
+ * @param {string} url - The URL to fetch
28
+ * @param {?Object} [options] - The [fetch options]{@link https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters}
29
+ * @returns {Promise} The fetch promise
30
+ */
31
+
32
+ /**
33
+ * The log function
34
+ *
35
+ * @callback LogFn
36
+ * @param {string} text - The log text
37
+ */
38
+
39
+ /**
40
+ * The URL modifier function
41
+ *
42
+ * @callback URLFn
43
+ * @param {string} url - The URL
44
+ * @returns {string} The modified URL
45
+ */
46
+
47
+
48
+ /**
49
+ * Execute a BareScript model
50
+ *
51
+ * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
52
+ * @param {Object} [options = {}] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
53
+ * @returns The script result
54
+ * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
55
+ */
56
+ export function executeScript(script, options = {}) {
57
+ // Create the global variable object, if necessary
58
+ let {globals = null} = options;
59
+ if (globals === null) {
60
+ globals = {};
61
+ options.globals = globals;
62
+ }
63
+
64
+ // Set the script function globals variables
65
+ for (const scriptFuncName of Object.keys(scriptFunctions)) {
66
+ if (!(scriptFuncName in globals)) {
67
+ globals[scriptFuncName] = scriptFunctions[scriptFuncName];
68
+ }
69
+ }
70
+
71
+ // Execute the script
72
+ options.statementCount = 0;
73
+ return executeScriptHelper(script.statements, options, null);
74
+ }
75
+
76
+
77
+ export function executeScriptHelper(statements, options, locals) {
78
+ const {globals} = options;
79
+
80
+ // Iterate each script statement
81
+ let labelIndexes = null;
82
+ const statementsLength = statements.length;
83
+ for (let ixStatement = 0; ixStatement < statementsLength; ixStatement++) {
84
+ const statement = statements[ixStatement];
85
+ const [statementKey] = Object.keys(statement);
86
+
87
+ // Increment the statement counter
88
+ const maxStatements = options.maxStatements ?? defaultMaxStatements;
89
+ if (maxStatements > 0 && ++options.statementCount > maxStatements) {
90
+ throw new BareScriptRuntimeError(`Exceeded maximum script statements (${maxStatements})`);
91
+ }
92
+
93
+ // Expression?
94
+ if (statementKey === 'expr') {
95
+ const exprValue = evaluateExpression(statement.expr.expr, options, locals, false);
96
+ if ('name' in statement.expr) {
97
+ if (locals !== null) {
98
+ locals[statement.expr.name] = exprValue;
99
+ } else {
100
+ globals[statement.expr.name] = exprValue;
101
+ }
102
+ }
103
+
104
+ // Jump?
105
+ } else if (statementKey === 'jump') {
106
+ // Evaluate the expression (if any)
107
+ if (!('expr' in statement.jump) || evaluateExpression(statement.jump.expr, options, locals, false)) {
108
+ // Find the label
109
+ if (labelIndexes !== null && statement.jump.label in labelIndexes) {
110
+ ixStatement = labelIndexes[statement.jump.label];
111
+ } else {
112
+ const ixLabel = statements.findIndex((stmt) => stmt.label === statement.jump.label);
113
+ if (ixLabel === -1) {
114
+ throw new BareScriptRuntimeError(`Unknown jump label "${statement.jump.label}"`);
115
+ }
116
+ if (labelIndexes === null) {
117
+ labelIndexes = {};
118
+ }
119
+ labelIndexes[statement.jump.label] = ixLabel;
120
+ ixStatement = ixLabel;
121
+ }
122
+ }
123
+
124
+ // Return?
125
+ } else if (statementKey === 'return') {
126
+ if ('expr' in statement.return) {
127
+ return evaluateExpression(statement.return.expr, options, locals, false);
128
+ }
129
+ return null;
130
+
131
+ // Function?
132
+ } else if (statementKey === 'function') {
133
+ globals[statement.function.name] = (args, fnOptions) => {
134
+ const funcLocals = {};
135
+ if ('args' in statement.function) {
136
+ const argsLength = args.length;
137
+ for (let ixArg = 0; ixArg < statement.function.args.length; ixArg++) {
138
+ funcLocals[statement.function.args[ixArg]] = (ixArg < argsLength ? args[ixArg] : null);
139
+ }
140
+ }
141
+ return executeScriptHelper(statement.function.statements, fnOptions, funcLocals);
142
+ };
143
+
144
+ // Include?
145
+ } else if (statementKey === 'include') {
146
+ throw new BareScriptRuntimeError(`Include of "${statement.include.includes[0].url}" within non-async scope`);
147
+ }
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+
154
+ /**
155
+ * Evaluate an expression model
156
+ *
157
+ * @param {Object} expr - The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
158
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
159
+ * @param {?Object} [locals = null] - The local variables
160
+ * @param {boolean} [builtins = true] - If true, include the
161
+ * [built-in expression functions]{@link https://craigahobbs.github.io/bare-script/library/expression.html}
162
+ * @returns The expression result
163
+ * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
164
+ */
165
+ export function evaluateExpression(expr, options = null, locals = null, builtins = true) {
166
+ const [exprKey] = Object.keys(expr);
167
+ const globals = (options !== null ? (options.globals ?? null) : null);
168
+
169
+ // Number
170
+ if (exprKey === 'number') {
171
+ return expr.number;
172
+
173
+ // String
174
+ } else if (exprKey === 'string') {
175
+ return expr.string;
176
+
177
+ // Variable
178
+ } else if (exprKey === 'variable') {
179
+ // Keywords
180
+ if (expr.variable === 'null') {
181
+ return null;
182
+ } else if (expr.variable === 'false') {
183
+ return false;
184
+ } else if (expr.variable === 'true') {
185
+ return true;
186
+ }
187
+
188
+ // Get the local or global variable value or null if undefined
189
+ let varValue = (locals !== null ? locals[expr.variable] : undefined);
190
+ if (typeof varValue === 'undefined') {
191
+ varValue = (globals !== null ? (globals[expr.variable] ?? null) : null);
192
+ }
193
+ return varValue;
194
+
195
+ // Function
196
+ } else if (exprKey === 'function') {
197
+ // "if" built-in function?
198
+ const funcName = expr.function.name;
199
+ if (funcName === 'if') {
200
+ const [valueExpr = null, trueExpr = null, falseExpr = null] = expr.function.args ?? [];
201
+ const value = (valueExpr !== null ? evaluateExpression(valueExpr, options, locals, builtins) : false);
202
+ const resultExpr = (value ? trueExpr : falseExpr);
203
+ return resultExpr !== null ? evaluateExpression(resultExpr, options, locals, builtins) : null;
204
+ }
205
+
206
+ // Compute the function arguments
207
+ const funcArgs = 'args' in expr.function
208
+ ? expr.function.args.map((arg) => evaluateExpression(arg, options, locals, builtins))
209
+ : null;
210
+
211
+ // Global/local function?
212
+ let funcValue = (locals !== null ? locals[funcName] : undefined);
213
+ if (typeof funcValue === 'undefined') {
214
+ funcValue = (globals !== null ? globals[funcName] : undefined);
215
+ if (typeof funcValue === 'undefined') {
216
+ funcValue = (builtins ? expressionFunctions[funcName] : null) ?? null;
217
+ }
218
+ }
219
+ if (funcValue !== null) {
220
+ // Async function called within non-async execution?
221
+ if (typeof funcValue === 'function' && funcValue.constructor.name === 'AsyncFunction') {
222
+ throw new BareScriptRuntimeError(`Async function "${funcName}" called within non-async scope`);
223
+ }
224
+
225
+ // Call the function
226
+ try {
227
+ return funcValue(funcArgs, options) ?? null;
228
+ } catch (error) {
229
+ // Propogate runtime errors
230
+ if (error instanceof BareScriptRuntimeError) {
231
+ throw error;
232
+ }
233
+
234
+ // Log and return null
235
+ if (options !== null && 'logFn' in options && options.debug) {
236
+ options.logFn(`BareScript: Function "${funcName}" failed with error: ${error.message}`);
237
+ }
238
+ return null;
239
+ }
240
+ }
241
+
242
+ throw new BareScriptRuntimeError(`Undefined function "${funcName}"`);
243
+
244
+ // Binary expression
245
+ } else if (exprKey === 'binary') {
246
+ const binOp = expr.binary.op;
247
+ const leftValue = evaluateExpression(expr.binary.left, options, locals, builtins);
248
+
249
+ // Short-circuiting binary operators - evaluate right expression only if necessary
250
+ if (binOp === '&&') {
251
+ return leftValue && evaluateExpression(expr.binary.right, options, locals, builtins);
252
+ } else if (binOp === '||') {
253
+ return leftValue || evaluateExpression(expr.binary.right, options, locals, builtins);
254
+ }
255
+
256
+ // Non-short-circuiting binary operators
257
+ const rightValue = evaluateExpression(expr.binary.right, options, locals, builtins);
258
+ if (binOp === '**') {
259
+ return leftValue ** rightValue;
260
+ } else if (binOp === '*') {
261
+ return leftValue * rightValue;
262
+ } else if (binOp === '/') {
263
+ return leftValue / rightValue;
264
+ } else if (binOp === '%') {
265
+ return leftValue % rightValue;
266
+ } else if (binOp === '+') {
267
+ return leftValue + rightValue;
268
+ } else if (binOp === '-') {
269
+ return leftValue - rightValue;
270
+ } else if (binOp === '<=') {
271
+ return leftValue <= rightValue;
272
+ } else if (binOp === '<') {
273
+ return leftValue < rightValue;
274
+ } else if (binOp === '>=') {
275
+ return leftValue >= rightValue;
276
+ } else if (binOp === '>') {
277
+ return leftValue > rightValue;
278
+ } else if (binOp === '==') {
279
+ return leftValue === rightValue;
280
+ }
281
+ // else if (binOp === '!=')
282
+ return leftValue !== rightValue;
283
+
284
+ // Unary expression
285
+ } else if (exprKey === 'unary') {
286
+ const unaryOp = expr.unary.op;
287
+ const value = evaluateExpression(expr.unary.expr, options, locals, builtins);
288
+ if (unaryOp === '!') {
289
+ return !value;
290
+ }
291
+ // else if (unaryOp === '-')
292
+ return -value;
293
+ }
294
+
295
+ // Expression group
296
+ // else if (exprKey === 'group')
297
+ return evaluateExpression(expr.group, options, locals, builtins);
298
+ }
299
+
300
+
301
+ /**
302
+ * A BareScript runtime error
303
+ *
304
+ * @extends {Error}
305
+ */
306
+ export class BareScriptRuntimeError extends Error {
307
+ /**
308
+ * Create a BareScript parser error
309
+ *
310
+ * @param {string} message - The runtime error message
311
+ */
312
+ constructor(message) {
313
+ super(message);
314
+ this.name = this.constructor.name;
315
+ }
316
+ }
@@ -0,0 +1,379 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ /** @module lib/runtimeAsync */
5
+
6
+ import {BareScriptParserError, parseScript} from './parser.js';
7
+ import {BareScriptRuntimeError, evaluateExpression, executeScriptHelper} from './runtime.js';
8
+ import {defaultMaxStatements, expressionFunctions, scriptFunctions} from './library.js';
9
+ import {lintScript} from './model.js';
10
+
11
+
12
+ /* eslint-disable no-await-in-loop, require-await */
13
+
14
+
15
+ /**
16
+ * Execute a BareScript model asynchronously.
17
+ * Use this form of the function if you have any global asynchronous functions.
18
+ *
19
+ * @async
20
+ * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
21
+ * @param {Object} [options = {}] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
22
+ * @returns The script result
23
+ * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
24
+ */
25
+ export async function executeScriptAsync(script, options = {}) {
26
+ // Create the global variable object, if necessary
27
+ let {globals = null} = options;
28
+ if (globals === null) {
29
+ globals = {};
30
+ options.globals = globals;
31
+ }
32
+
33
+ // Set the script function globals variables
34
+ for (const scriptFuncName of Object.keys(scriptFunctions)) {
35
+ if (!(scriptFuncName in globals)) {
36
+ globals[scriptFuncName] = scriptFunctions[scriptFuncName];
37
+ }
38
+ }
39
+
40
+ // Execute the script
41
+ options.statementCount = 0;
42
+ return executeScriptHelperAsync(script.statements, options, null);
43
+ }
44
+
45
+
46
+ async function executeScriptHelperAsync(statements, options, locals) {
47
+ const {globals} = options;
48
+
49
+ // Iterate each script statement
50
+ let labelIndexes = null;
51
+ const statementsLength = statements.length;
52
+ for (let ixStatement = 0; ixStatement < statementsLength; ixStatement++) {
53
+ const statement = statements[ixStatement];
54
+ const [statementKey] = Object.keys(statement);
55
+
56
+ // Increment the statement counter
57
+ const maxStatements = options.maxStatements ?? defaultMaxStatements;
58
+ if (maxStatements > 0 && ++options.statementCount > maxStatements) {
59
+ throw new BareScriptRuntimeError(`Exceeded maximum script statements (${maxStatements})`);
60
+ }
61
+
62
+ // Expression?
63
+ if (statementKey === 'expr') {
64
+ const exprValue = await evaluateExpressionAsync(statement.expr.expr, options, locals, false);
65
+ if ('name' in statement.expr) {
66
+ if (locals !== null) {
67
+ locals[statement.expr.name] = exprValue;
68
+ } else {
69
+ globals[statement.expr.name] = exprValue;
70
+ }
71
+ }
72
+
73
+ // Jump?
74
+ } else if (statementKey === 'jump') {
75
+ // Evaluate the expression (if any)
76
+ if (!('expr' in statement.jump) || await evaluateExpressionAsync(statement.jump.expr, options, locals, false)) {
77
+ // Find the label
78
+ if (labelIndexes !== null && statement.jump.label in labelIndexes) {
79
+ ixStatement = labelIndexes[statement.jump.label];
80
+ } else {
81
+ const ixLabel = statements.findIndex((stmt) => stmt.label === statement.jump.label);
82
+ if (ixLabel === -1) {
83
+ throw new BareScriptRuntimeError(`Unknown jump label "${statement.jump.label}"`);
84
+ }
85
+ if (labelIndexes === null) {
86
+ labelIndexes = {};
87
+ }
88
+ labelIndexes[statement.jump.label] = ixLabel;
89
+ ixStatement = ixLabel;
90
+ }
91
+ }
92
+
93
+ // Return?
94
+ } else if (statementKey === 'return') {
95
+ if ('expr' in statement.return) {
96
+ return evaluateExpressionAsync(statement.return.expr, options, locals, false);
97
+ }
98
+ return null;
99
+
100
+ // Function?
101
+ } else if (statementKey === 'function') {
102
+ if (statement.function.async) {
103
+ globals[statement.function.name] = async (args, fnOptions) => {
104
+ const funcLocals = {};
105
+ if ('args' in statement.function) {
106
+ const argsLength = args.length;
107
+ for (let ixArg = 0; ixArg < statement.function.args.length; ixArg++) {
108
+ funcLocals[statement.function.args[ixArg]] = (ixArg < argsLength ? args[ixArg] : null);
109
+ }
110
+ }
111
+ return executeScriptHelperAsync(statement.function.statements, fnOptions, funcLocals);
112
+ };
113
+ } else {
114
+ globals[statement.function.name] = (args, fnOptions) => {
115
+ const funcLocals = {};
116
+ if ('args' in statement.function) {
117
+ const argsLength = args.length;
118
+ for (let ixArg = 0; ixArg < statement.function.args.length; ixArg++) {
119
+ funcLocals[statement.function.args[ixArg]] = (ixArg < argsLength ? args[ixArg] : null);
120
+ }
121
+ }
122
+ return executeScriptHelper(statement.function.statements, fnOptions, funcLocals);
123
+ };
124
+ }
125
+
126
+ // Include?
127
+ } else if (statementKey === 'include') {
128
+ // Fetch the include script text
129
+ const includeURLs = statement.include.includes.map(({url, system = false}) => {
130
+ if (system && 'systemPrefix' in options && isRelativeURL(url)) {
131
+ return `${options.systemPrefix}${url}`;
132
+ }
133
+ return 'urlFn' in options ? options.urlFn(url) : url;
134
+ });
135
+ const responses = await Promise.all(includeURLs.map(async (url) => {
136
+ try {
137
+ return 'fetchFn' in options ? await options.fetchFn(url) : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }));
142
+ const scriptTexts = await Promise.all(responses.map(async (response) => {
143
+ try {
144
+ return response !== null && response.ok ? await response.text() : null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }));
149
+
150
+ // Parse and execute each script
151
+ for (const [ixScriptText, scriptText] of scriptTexts.entries()) {
152
+ const includeURL = includeURLs[ixScriptText];
153
+
154
+ // Error?
155
+ if (scriptText === null) {
156
+ throw new BareScriptRuntimeError(`Include of "${includeURL}" failed`);
157
+ }
158
+
159
+ // Parse the include script
160
+ let scriptModel = null;
161
+ try {
162
+ scriptModel = parseScript(scriptText);
163
+ } catch (error) {
164
+ throw new BareScriptParserError(
165
+ error.error, error.line, error.columnNumber, error.lineNumber, `Included from "${includeURL}"`
166
+ );
167
+ }
168
+
169
+ // Run the bare-script linter?
170
+ if ('logFn' in options && options.debug) {
171
+ const warnings = lintScript(scriptModel);
172
+ const warningPrefix = `BareScript: Include "${includeURL}" static analysis...`;
173
+ if (warnings.length) {
174
+ options.logFn(`${warningPrefix} ${warnings.length} warning${warnings.length > 1 ? 's' : ''}:`);
175
+ for (const warning of warnings) {
176
+ options.logFn(`BareScript: ${warning}`);
177
+ }
178
+ }
179
+ }
180
+
181
+ // Execute the include script
182
+ const includeOptions = {...options};
183
+ includeOptions.urlFn = (url) => (isRelativeURL(url) ? `${getBaseURL(includeURL)}${url}` : url);
184
+ await executeScriptHelperAsync(scriptModel.statements, includeOptions, null);
185
+ }
186
+ }
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+
193
+ // Test if a URL is relative
194
+ function isRelativeURL(url) {
195
+ return !rNotRelativeURL.test(url);
196
+ }
197
+
198
+ const rNotRelativeURL = /^(?:[a-z]+:|\/|\?|#)/;
199
+
200
+
201
+ // Get a URL's base URL
202
+ function getBaseURL(url) {
203
+ return url.slice(0, url.lastIndexOf('/') + 1);
204
+ }
205
+
206
+
207
+ /**
208
+ * Evaluate an expression model asynchronously.
209
+ * Use this form of the function if you have any asynchronous functions.
210
+ *
211
+ * @async
212
+ * @param {Object} expr - The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
213
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
214
+ * @param {?Object} [locals = null] - The local variables
215
+ * @param {boolean} [builtins = true] - If true, include the
216
+ * [built-in expression functions]{@link https://craigahobbs.github.io/bare-script/library/expression.html}
217
+ * @returns The expression result
218
+ * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
219
+ */
220
+ export async function evaluateExpressionAsync(expr, options = null, locals = null, builtins = true) {
221
+ const [exprKey] = Object.keys(expr);
222
+ const globals = (options !== null ? (options.globals ?? null) : null);
223
+
224
+ // If this expression does not require async then evaluate non-async
225
+ const hasSubExpr = (exprKey !== 'number' && exprKey !== 'string' && exprKey !== 'variable');
226
+ if (hasSubExpr && !isAsyncExpr(expr, globals, locals)) {
227
+ return evaluateExpression(expr, options, locals, builtins);
228
+ }
229
+
230
+ // Number
231
+ if (exprKey === 'number') {
232
+ return expr.number;
233
+
234
+ // String
235
+ } else if (exprKey === 'string') {
236
+ return expr.string;
237
+
238
+ // Variable
239
+ } else if (exprKey === 'variable') {
240
+ // Keywords
241
+ if (expr.variable === 'null') {
242
+ return null;
243
+ } else if (expr.variable === 'false') {
244
+ return false;
245
+ } else if (expr.variable === 'true') {
246
+ return true;
247
+ }
248
+
249
+ // Get the local or global variable value or null if undefined
250
+ let varValue = (locals !== null ? locals[expr.variable] : undefined);
251
+ if (typeof varValue === 'undefined') {
252
+ varValue = (globals !== null ? (globals[expr.variable] ?? null) : null);
253
+ }
254
+ return varValue;
255
+
256
+ // Function
257
+ } else if (exprKey === 'function') {
258
+ // "if" built-in function?
259
+ const funcName = expr.function.name;
260
+ if (funcName === 'if') {
261
+ const [valueExpr, trueExpr = null, falseExpr = null] = expr.function.args;
262
+ const value = await evaluateExpressionAsync(valueExpr, options, locals, builtins);
263
+ const resultExpr = (value ? trueExpr : falseExpr);
264
+ return resultExpr !== null ? evaluateExpressionAsync(resultExpr, options, locals, builtins) : null;
265
+ }
266
+
267
+ // Compute the function arguments
268
+ const funcArgs = 'args' in expr.function
269
+ ? await Promise.all(expr.function.args.map((arg) => evaluateExpressionAsync(arg, options, locals, builtins)))
270
+ : null;
271
+
272
+ // Global/local function?
273
+ let funcValue = (locals !== null ? locals[funcName] : undefined);
274
+ if (typeof funcValue === 'undefined') {
275
+ /* c8 ignore next */
276
+ funcValue = (globals !== null ? globals[funcName] : undefined);
277
+ if (typeof funcValue === 'undefined') {
278
+ funcValue = (builtins ? expressionFunctions[funcName] : null) ?? null;
279
+ }
280
+ }
281
+ if (funcValue !== null) {
282
+ // Call the function
283
+ try {
284
+ return await funcValue(funcArgs, options) ?? null;
285
+ } catch (error) {
286
+ // Propogate runtime errors
287
+ if (error instanceof BareScriptRuntimeError) {
288
+ throw error;
289
+ }
290
+
291
+ // Log and return null
292
+ if (options !== null && 'logFn' in options && options.debug) {
293
+ options.logFn(`BareScript: Function "${funcName}" failed with error: ${error.message}`);
294
+ }
295
+ return null;
296
+ }
297
+ }
298
+
299
+ throw new BareScriptRuntimeError(`Undefined function "${funcName}"`);
300
+
301
+ // Binary expression
302
+ } else if (exprKey === 'binary') {
303
+ const binOp = expr.binary.op;
304
+ const leftValue = await evaluateExpressionAsync(expr.binary.left, options, locals, builtins);
305
+
306
+ // Short-circuiting binary operators - evaluate right expression only if necessary
307
+ if (binOp === '&&') {
308
+ return leftValue && evaluateExpressionAsync(expr.binary.right, options, locals, builtins);
309
+ } else if (binOp === '||') {
310
+ return leftValue || evaluateExpressionAsync(expr.binary.right, options, locals, builtins);
311
+ }
312
+
313
+ // Non-short-circuiting binary operators
314
+ const rightValue = await evaluateExpressionAsync(expr.binary.right, options, locals, builtins);
315
+ if (binOp === '**') {
316
+ return leftValue ** rightValue;
317
+ } else if (binOp === '*') {
318
+ return leftValue * rightValue;
319
+ } else if (binOp === '/') {
320
+ return leftValue / rightValue;
321
+ } else if (binOp === '%') {
322
+ return leftValue % rightValue;
323
+ } else if (binOp === '+') {
324
+ return leftValue + rightValue;
325
+ } else if (binOp === '-') {
326
+ return leftValue - rightValue;
327
+ } else if (binOp === '<=') {
328
+ return leftValue <= rightValue;
329
+ } else if (binOp === '<') {
330
+ return leftValue < rightValue;
331
+ } else if (binOp === '>=') {
332
+ return leftValue >= rightValue;
333
+ } else if (binOp === '>') {
334
+ return leftValue > rightValue;
335
+ } else if (binOp === '==') {
336
+ return leftValue === rightValue;
337
+ }
338
+ // else if (binOp === '!=')
339
+ return leftValue !== rightValue;
340
+
341
+ // Unary expression
342
+ } else if (exprKey === 'unary') {
343
+ const unaryOp = expr.unary.op;
344
+ const value = await evaluateExpressionAsync(expr.unary.expr, options, locals, builtins);
345
+ if (unaryOp === '!') {
346
+ return !value;
347
+ }
348
+ // else if (unaryOp === '-')
349
+ return -value;
350
+ }
351
+
352
+ // Expression group
353
+ // else if (exprKey === 'group')
354
+ return evaluateExpressionAsync(expr.group, options, locals, builtins);
355
+ }
356
+
357
+
358
+ function isAsyncExpr(expr, globals, locals) {
359
+ const [exprKey] = Object.keys(expr);
360
+ if (exprKey === 'function') {
361
+ // Is the global/local function async?
362
+ const funcName = expr.function.name;
363
+ const localFuncValue = (locals !== null ? locals[funcName] : undefined);
364
+ const funcValue = (typeof localFuncValue !== 'undefined' ? localFuncValue : (globals !== null ? globals[funcName] : undefined));
365
+ if (typeof funcValue === 'function' && funcValue.constructor.name === 'AsyncFunction') {
366
+ return true;
367
+ }
368
+
369
+ // Are any of the function argument expressions async?
370
+ return 'args' in expr.function && expr.function.args.some((exprArg) => isAsyncExpr(exprArg, globals, locals));
371
+ } else if (exprKey === 'binary') {
372
+ return isAsyncExpr(expr.binary.left, globals, locals) || isAsyncExpr(expr.binary.right, globals, locals);
373
+ } else if (exprKey === 'unary') {
374
+ return isAsyncExpr(expr.unary.expr, globals, locals);
375
+ } else if (exprKey === 'group') {
376
+ return isAsyncExpr(expr.group, globals, locals);
377
+ }
378
+ return false;
379
+ }