bare-script 2.3.2 → 3.0.1

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/model.js CHANGED
@@ -221,7 +221,7 @@ struct FunctionExpression
221
221
  /**
222
222
  * Validate a BareScript script model
223
223
  *
224
- * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
224
+ * @param {Object} script - The [BareScript model](./model/#var.vName='BareScript')
225
225
  * @returns {Object} The validated BareScript model
226
226
  * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
227
227
  */
@@ -233,7 +233,7 @@ export function validateScript(script) {
233
233
  /**
234
234
  * Validate an expression model
235
235
  *
236
- * @param {Object} expr - The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
236
+ * @param {Object} expr - The [expression model](./model/#var.vName='Expression')
237
237
  * @returns {Object} The validated expression model
238
238
  * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
239
239
  */
@@ -245,7 +245,7 @@ export function validateExpression(expr) {
245
245
  /**
246
246
  * Lint a BareScript script model
247
247
  *
248
- * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
248
+ * @param {Object} script - The [BareScript model](./model/#var.vName='BareScript')
249
249
  * @returns {string[]} The array of lint warnings
250
250
  */
251
251
  export function lintScript(script) {
package/lib/options.js ADDED
@@ -0,0 +1,72 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ /** @module lib/options */
5
+
6
+
7
+ /**
8
+ * The BareScript runtime options
9
+ *
10
+ * @typedef {Object} ExecuteScriptOptions
11
+ * @property {boolean} [debug] - If true, execute in debug mode
12
+ * @property {function} [fetchFn] - The [fetch function]{@link module:lib/options~FetchFn}
13
+ * @property {Object} [globals] - The global variables
14
+ * @property {function} [logFn] - The [log function]{@link module:lib/options~LogFn}
15
+ * @property {number} [maxStatements] - The maximum number of statements; default is 1e9; 0 for no maximum
16
+ * @property {number} [statementCount] - The current statement count
17
+ * @property {function} [urlFn] - The [URL modifier function]{@link module:lib/options~URLFn}
18
+ * @property {string} [systemPrefix] - The system include prefix
19
+ */
20
+
21
+
22
+ /**
23
+ * The fetch function
24
+ *
25
+ * @callback FetchFn
26
+ * @param {string} url - The URL to fetch
27
+ * @param {?Object} [options] - The [fetch options]{@link https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters}
28
+ * @returns {Promise} The fetch [response promise]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response}
29
+ */
30
+
31
+
32
+ /**
33
+ * The log function
34
+ *
35
+ * @callback LogFn
36
+ * @param {string} text - The log text
37
+ */
38
+
39
+
40
+ /**
41
+ * The URL modifier function
42
+ *
43
+ * @callback URLFn
44
+ * @param {string} url - The URL
45
+ * @returns {string} The modified URL
46
+ */
47
+
48
+
49
+ /**
50
+ * A [URL modifier function]{@link module:lib/options~URLFn} implementation that fixes up file-relative paths
51
+ *
52
+ * @param {string} file - The URL or path to which relative URLs are relative
53
+ * @param {string} url - The URL or POSIX path to resolve
54
+ * @returns {string} The resolved URL
55
+ */
56
+ export function urlFileRelative(file, url) {
57
+ // URL?
58
+ if (rURL.test(url)) {
59
+ return url;
60
+ }
61
+
62
+ // Absolute POSIX path?
63
+ if (url.startsWith('/')) {
64
+ return url;
65
+ }
66
+
67
+ // URL is relative POSIX path
68
+ return `${file.slice(0, file.lastIndexOf('/') + 1)}${url}`;
69
+ }
70
+
71
+
72
+ export const rURL = /^[a-z]+:/;
@@ -0,0 +1,70 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ /** @module lib/optionsNode */
5
+
6
+ import {readFile, writeFile} from 'node:fs/promises';
7
+ import {stdout} from 'node:process';
8
+
9
+
10
+ /**
11
+ * A [fetch function]{@link module:lib/options~FetchFn} implementation that fetches resources that uses HTTP GET
12
+ * and POST for URLs, otherwise read-only file system access
13
+ */
14
+ export function fetchReadOnly(url, options = null, fetchFn = fetch, readFileFn = readFile) {
15
+ // URL fetch?
16
+ if (rURL.test(url)) {
17
+ return fetchFn(url, options);
18
+ }
19
+
20
+ // File write?
21
+ if ((options ?? null) !== null && 'body' in options) {
22
+ return {'ok': false};
23
+ }
24
+
25
+ // File read
26
+ return {
27
+ 'ok': true,
28
+ 'text': () => readFileFn(url, 'utf-8')
29
+ };
30
+ }
31
+
32
+
33
+ /**
34
+ * A [fetch function]{@link module:lib/options~FetchFn} implementation that fetches resources that uses HTTP GET
35
+ * and POST for URLs, otherwise read-write file system access
36
+ */
37
+ export function fetchReadWrite(url, options, fetchFn = fetch, readFileFn = readFile, writeFileFn = writeFile) {
38
+ // URL fetch?
39
+ if (rURL.test(url)) {
40
+ return fetchFn(url, options);
41
+ }
42
+
43
+ // File write?
44
+ if ((options ?? null) !== null && 'body' in options) {
45
+ return {
46
+ 'ok': true,
47
+ 'text': async () => {
48
+ await writeFileFn(url, options.body);
49
+ return '{}';
50
+ }
51
+ };
52
+ }
53
+
54
+ // File read
55
+ return {
56
+ 'ok': true,
57
+ 'text': () => readFileFn(url, 'utf-8')
58
+ };
59
+ }
60
+
61
+
62
+ export const rURL = /^[a-z]+:/;
63
+
64
+
65
+ /**
66
+ * A [log function]{@link module:lib/options~LogFn} implementation that outputs to stdout
67
+ */
68
+ export function logStdout(text, stdoutObj = stdout) {
69
+ stdoutObj.write(`${text}\n`);
70
+ }
package/lib/parser.js CHANGED
@@ -4,40 +4,12 @@
4
4
  /** @module lib/parser */
5
5
 
6
6
 
7
- // BareScript regex
8
- const rScriptLineSplit = /\r?\n/;
9
- const rScriptContinuation = /\\\s*$/;
10
- const rScriptComment = /^\s*(?:#.*)?$/;
11
- const rScriptAssignment = /^\s*(?<name>[A-Za-z_]\w*)\s*=\s*(?<expr>.+)$/;
12
- const rScriptFunctionBegin = new RegExp(
13
- '^(?<async>\\s*async)?\\s*function\\s+(?<name>[A-Za-z_]\\w*)\\s*\\(' +
14
- '\\s*(?<args>[A-Za-z_]\\w*(?:\\s*,\\s*[A-Za-z_]\\w*)*)?(?<lastArgArray>\\s*\\.\\.\\.)?\\s*\\)\\s*:\\s*$'
15
- );
16
- const rScriptFunctionArgSplit = /\s*,\s*/;
17
- const rScriptFunctionEnd = /^\s*endfunction\s*$/;
18
- const rScriptLabel = /^\s*(?<name>[A-Za-z_]\w*)\s*:\s*$/;
19
- const rScriptJump = /^(?<jump>\s*(?:jump|jumpif\s*\((?<expr>.+)\)))\s+(?<name>[A-Za-z_]\w*)\s*$/;
20
- const rScriptReturn = /^(?<return>\s*return(?:\s+(?<expr>.+))?)\s*$/;
21
- const rScriptInclude = /^\s*include\s+(?<delim>')(?<url>(?:\\'|[^'])*)'\s*$/;
22
- const rScriptIncludeSystem = /^\s*include\s+(?<delim><)(?<url>[^>]*)>\s*$/;
23
- const rScriptIfBegin = /^\s*if\s+(?<expr>.+)\s*:\s*$/;
24
- const rScriptIfElseIf = /^\s*elif\s+(?<expr>.+)\s*:\s*$/;
25
- const rScriptIfElse = /^\s*else\s*:\s*$/;
26
- const rScriptIfEnd = /^\s*endif\s*$/;
27
- const rScriptForBegin = /^\s*for\s+(?<value>[A-Za-z_]\w*)(?:\s*,\s*(?<index>[A-Za-z_]\w*))?\s+in\s+(?<values>.+)\s*:\s*$/;
28
- const rScriptForEnd = /^\s*endfor\s*$/;
29
- const rScriptWhileBegin = /^\s*while\s+(?<expr>.+)\s*:\s*$/;
30
- const rScriptWhileEnd = /^\s*endwhile\s*$/;
31
- const rScriptBreak = /^\s*break\s*$/;
32
- const rScriptContinue = /^\s*continue\s*$/;
33
-
34
-
35
7
  /**
36
8
  * Parse a BareScript script
37
9
  *
38
- * @param {string|string[]} scriptText - The [script text]{@link https://craigahobbs.github.io/bare-script/language/}
10
+ * @param {string|string[]} scriptText - The [script text](./language/)
39
11
  * @param {number} [startLineNumber = 1] - The script's starting line number
40
- * @returns {Object} The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
12
+ * @returns {Object} The [BareScript model](./model/#var.vName='BareScript')
41
13
  * @throws [BareScriptParserError]{@link module:lib/parser.BareScriptParserError}
42
14
  */
43
15
  export function parseScript(scriptText, startLineNumber = 1) {
@@ -319,7 +291,10 @@ export function parseScript(scriptText, startLineNumber = 1) {
319
291
  // Add the for-each header statements
320
292
  statements.push(
321
293
  {'expr': {'name': foreach.values, 'expr': parseExpression(matchForBegin.groups.values)}},
322
- {'expr': {'name': foreach.length, 'expr': {'function': {'name': 'arrayLength', 'args': [{'variable': foreach.values}]}}}},
294
+ {'expr': {
295
+ 'name': foreach.length,
296
+ 'expr': {'function': {'name': 'arrayLength', 'args': [{'variable': foreach.values}]}}
297
+ }},
323
298
  {'jump': {'label': foreach.done, 'expr': {'unary': {'op': '!', 'expr': {'variable': foreach.length}}}}},
324
299
  {'expr': {'name': foreach.index, 'expr': {'number': 0}}},
325
300
  {'label': foreach.loop},
@@ -470,48 +445,39 @@ export function parseScript(scriptText, startLineNumber = 1) {
470
445
  }
471
446
 
472
447
 
473
- // BareScript expression regex
474
- const rExprBinaryOp = /^\s*(\*\*|\*|\/|%|\+|-|<=|<|>=|>|==|!=|&&|\|\|)/;
475
- const rExprUnaryOp = /^\s*(!|-)/;
476
- const rExprFunctionOpen = /^\s*([A-Za-z_]\w+)\s*\(/;
477
- const rExprFunctionSeparator = /^\s*,/;
478
- const rExprFunctionClose = /^\s*\)/;
479
- const rExprGroupOpen = /^\s*\(/;
480
- const rExprGroupClose = /^\s*\)/;
481
- const rExprNumber = /^\s*([+-]?\d+(?:\.\d*)?(?:e[+-]\d+)?)/;
482
- const rExprString = /^\s*'((?:\\\\|\\'|[^'])*)'/;
483
- const rExprStringEscape = /\\([\\'])/g;
484
- const rExprStringDouble = /^\s*"((?:\\\\|\\"|[^"])*)"/;
485
- const rExprStringDoubleEscape = /\\([\\"])/g;
486
- const rExprVariable = /^\s*([A-Za-z_]\w*)/;
487
- const rExprVariableEx = /^\s*\[\s*((?:\\\]|[^\]])+)\s*\]/;
488
- const rExprVariableExEscape = /\\([\\\]])/g;
489
-
490
-
491
- // Binary operator re-order map
492
- const binaryReorder = {
493
- '**': new Set(['*', '/', '%', '+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
494
- '*': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
495
- '/': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
496
- '%': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
497
- '+': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
498
- '-': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
499
- '<=': new Set(['==', '!=', '&&', '||']),
500
- '<': new Set(['==', '!=', '&&', '||']),
501
- '>=': new Set(['==', '!=', '&&', '||']),
502
- '>': new Set(['==', '!=', '&&', '||']),
503
- '==': new Set(['&&', '||']),
504
- '!=': new Set(['&&', '||']),
505
- '&&': new Set(['||']),
506
- '||': new Set([])
507
- };
448
+ // BareScript regex
449
+ const rScriptLineSplit = /\r?\n/;
450
+ const rScriptContinuation = /\\\s*$/;
451
+ const rScriptComment = /^\s*(?:#.*)?$/;
452
+ const rScriptAssignment = /^\s*(?<name>[A-Za-z_]\w*)\s*=\s*(?<expr>.+)$/;
453
+ const rScriptFunctionBegin = new RegExp(
454
+ '^(?<async>\\s*async)?\\s*function\\s+(?<name>[A-Za-z_]\\w*)\\s*\\(' +
455
+ '\\s*(?<args>[A-Za-z_]\\w*(?:\\s*,\\s*[A-Za-z_]\\w*)*)?(?<lastArgArray>\\s*\\.\\.\\.)?\\s*\\)\\s*:\\s*$'
456
+ );
457
+ const rScriptFunctionArgSplit = /\s*,\s*/;
458
+ const rScriptFunctionEnd = /^\s*endfunction\s*$/;
459
+ const rScriptLabel = /^\s*(?<name>[A-Za-z_]\w*)\s*:\s*$/;
460
+ const rScriptJump = /^(?<jump>\s*(?:jump|jumpif\s*\((?<expr>.+)\)))\s+(?<name>[A-Za-z_]\w*)\s*$/;
461
+ const rScriptReturn = /^(?<return>\s*return(?:\s+(?<expr>.+))?)\s*$/;
462
+ const rScriptInclude = /^\s*include\s+(?<delim>')(?<url>(?:\\'|[^'])*)'\s*$/;
463
+ const rScriptIncludeSystem = /^\s*include\s+(?<delim><)(?<url>[^>]*)>\s*$/;
464
+ const rScriptIfBegin = /^\s*if\s+(?<expr>.+)\s*:\s*$/;
465
+ const rScriptIfElseIf = /^\s*elif\s+(?<expr>.+)\s*:\s*$/;
466
+ const rScriptIfElse = /^\s*else\s*:\s*$/;
467
+ const rScriptIfEnd = /^\s*endif\s*$/;
468
+ const rScriptForBegin = /^\s*for\s+(?<value>[A-Za-z_]\w*)(?:\s*,\s*(?<index>[A-Za-z_]\w*))?\s+in\s+(?<values>.+)\s*:\s*$/;
469
+ const rScriptForEnd = /^\s*endfor\s*$/;
470
+ const rScriptWhileBegin = /^\s*while\s+(?<expr>.+)\s*:\s*$/;
471
+ const rScriptWhileEnd = /^\s*endwhile\s*$/;
472
+ const rScriptBreak = /^\s*break\s*$/;
473
+ const rScriptContinue = /^\s*continue\s*$/;
508
474
 
509
475
 
510
476
  /**
511
477
  * Parse a BareScript expression
512
478
  *
513
- * @param {string} exprText - The [expression text]{@link https://craigahobbs.github.io/bare-script/language/#expressions}
514
- * @returns {Object} The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
479
+ * @param {string} exprText - The [expression text](./language/#expressions)
480
+ * @returns {Object} The [expression model](./model/#var.vName='Expression')
515
481
  * @throws [BareScriptParserError]{@link module:lib/parser.BareScriptParserError}
516
482
  */
517
483
  export function parseExpression(exprText) {
@@ -571,6 +537,25 @@ function parseBinaryExpression(exprText, binLeftExpr = null) {
571
537
  }
572
538
 
573
539
 
540
+ // Binary operator re-order map
541
+ const binaryReorder = {
542
+ '**': new Set(['*', '/', '%', '+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
543
+ '*': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
544
+ '/': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
545
+ '%': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
546
+ '+': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
547
+ '-': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
548
+ '<=': new Set(['==', '!=', '&&', '||']),
549
+ '<': new Set(['==', '!=', '&&', '||']),
550
+ '>=': new Set(['==', '!=', '&&', '||']),
551
+ '>': new Set(['==', '!=', '&&', '||']),
552
+ '==': new Set(['&&', '||']),
553
+ '!=': new Set(['&&', '||']),
554
+ '&&': new Set(['||']),
555
+ '||': new Set([])
556
+ };
557
+
558
+
574
559
  // Helper function to parse a unary expression
575
560
  function parseUnaryExpression(exprText) {
576
561
  // Group open?
@@ -604,8 +589,7 @@ function parseUnaryExpression(exprText) {
604
589
  if (matchFunctionOpen !== null) {
605
590
  let argText = exprText.slice(matchFunctionOpen[0].length);
606
591
  const args = [];
607
- // eslint-disable-next-line no-constant-condition
608
- while (true) {
592
+ while (true) { // eslint-disable-line no-constant-condition
609
593
  // Function close?
610
594
  const matchFunctionClose = argText.match(rExprFunctionClose);
611
595
  if (matchFunctionClose !== null) {
@@ -680,6 +664,24 @@ function parseUnaryExpression(exprText) {
680
664
  }
681
665
 
682
666
 
667
+ // BareScript expression regex
668
+ const rExprBinaryOp = /^\s*(\*\*|\*|\/|%|\+|-|<=|<|>=|>|==|!=|&&|\|\|)/;
669
+ const rExprUnaryOp = /^\s*(!|-)/;
670
+ const rExprFunctionOpen = /^\s*([A-Za-z_]\w+)\s*\(/;
671
+ const rExprFunctionSeparator = /^\s*,/;
672
+ const rExprFunctionClose = /^\s*\)/;
673
+ const rExprGroupOpen = /^\s*\(/;
674
+ const rExprGroupClose = /^\s*\)/;
675
+ const rExprNumber = /^\s*([+-]?\d+(?:\.\d*)?(?:e[+-]\d+)?)/;
676
+ const rExprString = /^\s*'((?:\\\\|\\'|[^'])*)'/;
677
+ const rExprStringEscape = /\\([\\'])/g;
678
+ const rExprStringDouble = /^\s*"((?:\\\\|\\"|[^"])*)"/;
679
+ const rExprStringDoubleEscape = /\\([\\"])/g;
680
+ const rExprVariable = /^\s*([A-Za-z_]\w*)/;
681
+ const rExprVariableEx = /^\s*\[\s*((?:\\\]|[^\]])+)\s*\]/;
682
+ const rExprVariableExEscape = /\\([\\\]])/g;
683
+
684
+
683
685
  /**
684
686
  * A BareScript parser error
685
687
  *
package/lib/runtime.js CHANGED
@@ -4,52 +4,14 @@
4
4
  /** @module lib/runtime */
5
5
 
6
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
- */
7
+ import {valueBoolean, valueCompare, valueString} from './value.js';
46
8
 
47
9
 
48
10
  /**
49
11
  * Execute a BareScript model
50
12
  *
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}
13
+ * @param {Object} script - The [BareScript model](model/#var.vName='BareScript')
14
+ * @param {Object} [options = {}] - The [script execution options]{@link module:lib/options~ExecuteScriptOptions}
53
15
  * @returns The script result
54
16
  * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
55
17
  */
@@ -74,7 +36,7 @@ export function executeScript(script, options = {}) {
74
36
  }
75
37
 
76
38
 
77
- export function executeScriptHelper(statements, options, locals) {
39
+ function executeScriptHelper(statements, options, locals) {
78
40
  const {globals} = options;
79
41
 
80
42
  // Iterate each script statement
@@ -85,8 +47,9 @@ export function executeScriptHelper(statements, options, locals) {
85
47
  const [statementKey] = Object.keys(statement);
86
48
 
87
49
  // Increment the statement counter
50
+ options.statementCount += 1;
88
51
  const maxStatements = options.maxStatements ?? defaultMaxStatements;
89
- if (maxStatements > 0 && ++options.statementCount > maxStatements) {
52
+ if (maxStatements > 0 && options.statementCount > maxStatements) {
90
53
  throw new BareScriptRuntimeError(`Exceeded maximum script statements (${maxStatements})`);
91
54
  }
92
55
 
@@ -130,23 +93,7 @@ export function executeScriptHelper(statements, options, locals) {
130
93
 
131
94
  // Function?
132
95
  } 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
- const funcArgsLength = statement.function.args.length;
138
- const ixArgLast = (statement.function.lastArgArray ?? null) && (funcArgsLength - 1);
139
- for (let ixArg = 0; ixArg < funcArgsLength; ixArg++) {
140
- const argName = statement.function.args[ixArg];
141
- if (ixArg < argsLength) {
142
- funcLocals[argName] = (ixArg === ixArgLast ? args.slice(ixArg) : args[ixArg]);
143
- } else {
144
- funcLocals[argName] = (ixArg === ixArgLast ? [] : null);
145
- }
146
- }
147
- }
148
- return executeScriptHelper(statement.function.statements, fnOptions, funcLocals);
149
- };
96
+ globals[statement.function.name] = (args, fnOptions) => scriptFunction(statement.function, args, fnOptions);
150
97
 
151
98
  // Include?
152
99
  } else if (statementKey === 'include') {
@@ -158,14 +105,33 @@ export function executeScriptHelper(statements, options, locals) {
158
105
  }
159
106
 
160
107
 
108
+ // Runtime script function implementation
109
+ export function scriptFunction(function_, args, options) {
110
+ const funcLocals = {};
111
+ if ('args' in function_) {
112
+ const argsLength = args.length;
113
+ const funcArgsLength = function_.args.length;
114
+ const ixArgLast = (function_.lastArgArray ?? null) && (funcArgsLength - 1);
115
+ for (let ixArg = 0; ixArg < funcArgsLength; ixArg++) {
116
+ const argName = function_.args[ixArg];
117
+ if (ixArg < argsLength) {
118
+ funcLocals[argName] = (ixArg === ixArgLast ? args.slice(ixArg) : args[ixArg]);
119
+ } else {
120
+ funcLocals[argName] = (ixArg === ixArgLast ? [] : null);
121
+ }
122
+ }
123
+ }
124
+ return executeScriptHelper(function_.statements, options, funcLocals);
125
+ }
126
+
127
+
161
128
  /**
162
129
  * Evaluate an expression model
163
130
  *
164
- * @param {Object} expr - The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
165
- * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
131
+ * @param {Object} expr - The [expression model](./model/#var.vName='Expression')
132
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/options~ExecuteScriptOptions}
166
133
  * @param {?Object} [locals = null] - The local variables
167
- * @param {boolean} [builtins = true] - If true, include the
168
- * [built-in expression functions]{@link https://craigahobbs.github.io/bare-script/library/expression.html}
134
+ * @param {boolean} [builtins = true] - If true, include the [built-in expression functions](./library/expression.html)
169
135
  * @returns The expression result
170
136
  * @throws [BareScriptRuntimeError]{@link module:lib/runtime.BareScriptRuntimeError}
171
137
  */
@@ -257,40 +223,90 @@ export function evaluateExpression(expr, options = null, locals = null, builtins
257
223
  const binOp = expr.binary.op;
258
224
  const leftValue = evaluateExpression(expr.binary.left, options, locals, builtins);
259
225
 
260
- // Short-circuiting binary operators - evaluate right expression only if necessary
226
+ // Short-circuiting "and" binary operator
261
227
  if (binOp === '&&') {
262
- return leftValue && evaluateExpression(expr.binary.right, options, locals, builtins);
228
+ if (!valueBoolean(leftValue)) {
229
+ return leftValue;
230
+ }
231
+ return evaluateExpression(expr.binary.right, options, locals, builtins);
232
+
233
+ // Short-circuiting "or" binary operator
263
234
  } else if (binOp === '||') {
264
- return leftValue || evaluateExpression(expr.binary.right, options, locals, builtins);
235
+ if (valueBoolean(leftValue)) {
236
+ return leftValue;
237
+ }
238
+ return evaluateExpression(expr.binary.right, options, locals, builtins);
265
239
  }
266
240
 
267
241
  // Non-short-circuiting binary operators
268
242
  const rightValue = evaluateExpression(expr.binary.right, options, locals, builtins);
269
- if (binOp === '**') {
270
- return leftValue ** rightValue;
243
+ if (binOp === '+') {
244
+ // number + number
245
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
246
+ return leftValue + rightValue;
247
+
248
+ // string + string
249
+ } else if (typeof leftValue === 'string' && typeof rightValue === 'string') {
250
+ return leftValue + rightValue;
251
+
252
+ // string + <any>
253
+ } else if (typeof leftValue === 'string') {
254
+ return leftValue + valueString(rightValue);
255
+ } else if (typeof rightValue === 'string') {
256
+ return valueString(leftValue) + rightValue;
257
+
258
+ // datetime + number
259
+ } else if (leftValue instanceof Date && typeof rightValue === 'number') {
260
+ return new Date(leftValue.getTime() + rightValue);
261
+ } else if (typeof leftValue === 'number' && rightValue instanceof Date) {
262
+ return new Date(leftValue + rightValue.getTime());
263
+ }
264
+ } else if (binOp === '-') {
265
+ // number - number
266
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
267
+ return leftValue - rightValue;
268
+
269
+ // datetime - datetime
270
+ } else if (leftValue instanceof Date && rightValue instanceof Date) {
271
+ return leftValue - rightValue;
272
+ }
271
273
  } else if (binOp === '*') {
272
- return leftValue * rightValue;
274
+ // number * number
275
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
276
+ return leftValue * rightValue;
277
+ }
273
278
  } else if (binOp === '/') {
274
- return leftValue / rightValue;
275
- } else if (binOp === '%') {
276
- return leftValue % rightValue;
277
- } else if (binOp === '+') {
278
- return leftValue + rightValue;
279
- } else if (binOp === '-') {
280
- return leftValue - rightValue;
279
+ // number / number
280
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
281
+ return leftValue / rightValue;
282
+ }
283
+ } else if (binOp === '==') {
284
+ return valueCompare(leftValue, rightValue) === 0;
285
+ } else if (binOp === '!=') {
286
+ return valueCompare(leftValue, rightValue) !== 0;
281
287
  } else if (binOp === '<=') {
282
- return leftValue <= rightValue;
288
+ return valueCompare(leftValue, rightValue) <= 0;
283
289
  } else if (binOp === '<') {
284
- return leftValue < rightValue;
290
+ return valueCompare(leftValue, rightValue) < 0;
285
291
  } else if (binOp === '>=') {
286
- return leftValue >= rightValue;
292
+ return valueCompare(leftValue, rightValue) >= 0;
287
293
  } else if (binOp === '>') {
288
- return leftValue > rightValue;
289
- } else if (binOp === '==') {
290
- return leftValue === rightValue;
294
+ return valueCompare(leftValue, rightValue) > 0;
295
+ } else if (binOp === '%') {
296
+ // number % number
297
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
298
+ return leftValue % rightValue;
299
+ }
300
+ } else {
301
+ // binOp === '**'
302
+ // number ** number
303
+ if (typeof leftValue === 'number' && typeof rightValue === 'number') {
304
+ return leftValue ** rightValue;
305
+ }
291
306
  }
292
- // else if (binOp === '!=')
293
- return leftValue !== rightValue;
307
+
308
+ // Invalid operation values
309
+ return null;
294
310
  }
295
311
 
296
312
  // Unary expression
@@ -298,10 +314,13 @@ export function evaluateExpression(expr, options = null, locals = null, builtins
298
314
  const unaryOp = expr.unary.op;
299
315
  const value = evaluateExpression(expr.unary.expr, options, locals, builtins);
300
316
  if (unaryOp === '!') {
301
- return !value;
317
+ return !valueBoolean(value);
318
+ } else if (unaryOp === '-' && typeof value === 'number') {
319
+ return -value;
302
320
  }
303
- // else if (unaryOp === '-')
304
- return -value;
321
+
322
+ // Invalid operation value
323
+ return null;
305
324
  }
306
325
 
307
326
  // Expression group