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/LICENSE +21 -0
- package/README.md +132 -0
- package/bin/bareScriptDoc.js +15 -0
- package/lib/library.js +978 -0
- package/lib/libraryDoc.js +136 -0
- package/lib/model.js +477 -0
- package/lib/parser.js +725 -0
- package/lib/runtime.js +316 -0
- package/lib/runtimeAsync.js +379 -0
- package/package.json +36 -0
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
|
+
}
|