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/parser.js
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
// Licensed under the MIT License
|
|
2
|
+
// https://github.com/craigahobbs/bare-script/blob/main/LICENSE
|
|
3
|
+
|
|
4
|
+
/** @module lib/parser */
|
|
5
|
+
|
|
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 =
|
|
13
|
+
/^\s*(?:(?<async>async)\s+)?function\s+(?<name>[A-Za-z_]\w*)\s*\(\s*(?<args>[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)?\s*\)\s*$/;
|
|
14
|
+
const rScriptFunctionArgSplit = /\s*,\s*/;
|
|
15
|
+
const rScriptFunctionEnd = /^\s*endfunction\s*$/;
|
|
16
|
+
const rScriptLabel = /^\s*(?<name>[A-Za-z_]\w*)\s*:\s*$/;
|
|
17
|
+
const rScriptJump = /^(?<jump>\s*(?:jump|jumpif\s*\((?<expr>.+)\)))\s+(?<name>[A-Za-z_]\w*)\s*$/;
|
|
18
|
+
const rScriptReturn = /^(?<return>\s*return(?:\s+(?<expr>.+))?)\s*$/;
|
|
19
|
+
const rScriptInclude = /^\s*include\s+(?<delim>')(?<url>(?:\\'|[^'])*)'\s*$/;
|
|
20
|
+
const rScriptIncludeSystem = /^\s*include\s+(?<delim><)(?<url>[^>]*)>\s*$/;
|
|
21
|
+
const rScriptIfBegin = /^\s*if\s+(?<expr>.+)\s*:\s*$/;
|
|
22
|
+
const rScriptIfElseIf = /^\s*elif\s+(?<expr>.+)\s*:\s*$/;
|
|
23
|
+
const rScriptIfElse = /^\s*else\s*:\s*$/;
|
|
24
|
+
const rScriptIfEnd = /^\s*endif\s*$/;
|
|
25
|
+
const rScriptForBegin = /^\s*for\s+(?<value>[A-Za-z_]\w*)(?:\s*,\s*(?<index>[A-Za-z_]\w*))?\s+in\s+(?<values>.+)\s*:\s*$/;
|
|
26
|
+
const rScriptForEnd = /^\s*endfor\s*$/;
|
|
27
|
+
const rScriptWhileBegin = /^\s*while\s+(?<expr>.+)\s*:\s*$/;
|
|
28
|
+
const rScriptWhileEnd = /^\s*endwhile\s*$/;
|
|
29
|
+
const rScriptBreak = /^\s*break\s*$/;
|
|
30
|
+
const rScriptContinue = /^\s*continue\s*$/;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a BareScript script
|
|
35
|
+
*
|
|
36
|
+
* @param {string|string[]} scriptText - The [script text]{@link https://craigahobbs.github.io/bare-script/language/}
|
|
37
|
+
* @param {number} [startLineNumber = 1] - The script's starting line number
|
|
38
|
+
* @returns {Object} The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
|
|
39
|
+
* @throws [BareScriptParserError]{@link module:lib/parser.BareScriptParserError}
|
|
40
|
+
*/
|
|
41
|
+
export function parseScript(scriptText, startLineNumber = 1) {
|
|
42
|
+
const script = {'statements': []};
|
|
43
|
+
|
|
44
|
+
// Line-split all script text
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (typeof scriptText === 'string') {
|
|
47
|
+
lines.push(...scriptText.split(rScriptLineSplit));
|
|
48
|
+
} else {
|
|
49
|
+
for (const scriptTextPart of scriptText) {
|
|
50
|
+
lines.push(...scriptTextPart.split(rScriptLineSplit));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Process each line
|
|
55
|
+
const lineContinuation = [];
|
|
56
|
+
let functionDef = null;
|
|
57
|
+
const labelDefs = [];
|
|
58
|
+
let labelIndex = 0;
|
|
59
|
+
let ixLine;
|
|
60
|
+
for (const [ixLinePart, linePart] of lines.entries()) {
|
|
61
|
+
const statements = (functionDef !== null ? functionDef.function.statements : script.statements);
|
|
62
|
+
|
|
63
|
+
// Set the line index
|
|
64
|
+
const isContinued = (lineContinuation.length !== 0);
|
|
65
|
+
if (!isContinued) {
|
|
66
|
+
ixLine = ixLinePart;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Line continuation?
|
|
70
|
+
const linePartNoContinuation = linePart.replace(rScriptContinuation, '');
|
|
71
|
+
if (linePart !== linePartNoContinuation) {
|
|
72
|
+
lineContinuation.push(lineContinuation.length === 0 ? linePartNoContinuation.trimEnd() : linePartNoContinuation.trim());
|
|
73
|
+
continue;
|
|
74
|
+
} else if (isContinued) {
|
|
75
|
+
lineContinuation.push(linePartNoContinuation.trim());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Join the continued script lines, if necessary
|
|
79
|
+
let line;
|
|
80
|
+
if (isContinued) {
|
|
81
|
+
line = lineContinuation.join(' ');
|
|
82
|
+
lineContinuation.length = 0;
|
|
83
|
+
} else {
|
|
84
|
+
line = linePart;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Comment?
|
|
88
|
+
if (line.match(rScriptComment) !== null) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Assignment?
|
|
93
|
+
const matchAssignment = line.match(rScriptAssignment);
|
|
94
|
+
if (matchAssignment !== null) {
|
|
95
|
+
try {
|
|
96
|
+
const exprStatement = {
|
|
97
|
+
'expr': {
|
|
98
|
+
'name': matchAssignment.groups.name,
|
|
99
|
+
'expr': parseExpression(matchAssignment.groups.expr)
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
statements.push(exprStatement);
|
|
103
|
+
continue;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const columnNumber = line.length - matchAssignment.groups.expr.length + error.columnNumber;
|
|
106
|
+
throw new BareScriptParserError(error.error, line, columnNumber, startLineNumber + ixLine);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Function definition begin?
|
|
111
|
+
const matchFunctionBegin = line.match(rScriptFunctionBegin);
|
|
112
|
+
if (matchFunctionBegin !== null) {
|
|
113
|
+
// Nested function definitions are not allowed
|
|
114
|
+
if (functionDef !== null) {
|
|
115
|
+
throw new BareScriptParserError('Nested function definition', line, 1, startLineNumber + ixLine);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add the function definition statement
|
|
119
|
+
functionDef = {
|
|
120
|
+
'function': {
|
|
121
|
+
'name': matchFunctionBegin.groups.name,
|
|
122
|
+
'args': typeof matchFunctionBegin.groups.args !== 'undefined'
|
|
123
|
+
? matchFunctionBegin.groups.args.split(rScriptFunctionArgSplit) : [],
|
|
124
|
+
'statements': []
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (matchFunctionBegin.groups.async === 'async') {
|
|
128
|
+
functionDef.function.async = true;
|
|
129
|
+
}
|
|
130
|
+
statements.push(functionDef);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Function definition end?
|
|
135
|
+
const matchFunctionEnd = line.match(rScriptFunctionEnd);
|
|
136
|
+
if (matchFunctionEnd !== null) {
|
|
137
|
+
if (functionDef === null) {
|
|
138
|
+
throw new BareScriptParserError('No matching function definition', line, 1, startLineNumber + ixLine);
|
|
139
|
+
}
|
|
140
|
+
functionDef = null;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If-then begin?
|
|
145
|
+
const matchIfBegin = line.match(rScriptIfBegin);
|
|
146
|
+
if (matchIfBegin !== null) {
|
|
147
|
+
// Add the if-then label definition
|
|
148
|
+
const ifthen = {
|
|
149
|
+
'jump': {
|
|
150
|
+
'label': `__bareScriptIf${labelIndex}`,
|
|
151
|
+
'expr': {'unary': {'op': '!', 'expr': parseExpression(matchIfBegin.groups.expr)}}
|
|
152
|
+
},
|
|
153
|
+
'done': `__bareScriptDone${labelIndex}`,
|
|
154
|
+
'hasElse': false,
|
|
155
|
+
line,
|
|
156
|
+
'lineNumber': startLineNumber + ixLine
|
|
157
|
+
};
|
|
158
|
+
labelDefs.push({'if': ifthen});
|
|
159
|
+
labelIndex += 1;
|
|
160
|
+
|
|
161
|
+
// Add the if-then header statement
|
|
162
|
+
statements.push({'jump': ifthen.jump});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Else-if-then?
|
|
167
|
+
const matchIfElseIf = line.match(rScriptIfElseIf);
|
|
168
|
+
if (matchIfElseIf !== null) {
|
|
169
|
+
// Get the if-then definition
|
|
170
|
+
const ifthen = (labelDefs.length > 0 ? (labelDefs[labelDefs.length - 1].if ?? null) : null);
|
|
171
|
+
if (ifthen === null) {
|
|
172
|
+
throw new BareScriptParserError('No matching if statement', line, 1, startLineNumber + ixLine);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Cannot come after the else-then statement
|
|
176
|
+
if (ifthen.hasElse) {
|
|
177
|
+
throw new BareScriptParserError('Elif statement following else statement', line, 1, startLineNumber + ixLine);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Generate the next if-then jump statement
|
|
181
|
+
const prevLabel = ifthen.jump.label;
|
|
182
|
+
ifthen.jump = {
|
|
183
|
+
'label': `__bareScriptIf${labelIndex}`,
|
|
184
|
+
'expr': {'unary': {'op': '!', 'expr': parseExpression(matchIfElseIf.groups.expr)}}
|
|
185
|
+
};
|
|
186
|
+
labelIndex += 1;
|
|
187
|
+
|
|
188
|
+
// Add the if-then else statements
|
|
189
|
+
statements.push(
|
|
190
|
+
{'jump': {'label': ifthen.done}},
|
|
191
|
+
{'label': prevLabel},
|
|
192
|
+
{'jump': ifthen.jump}
|
|
193
|
+
);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Else-then?
|
|
198
|
+
const matchIfElse = line.match(rScriptIfElse);
|
|
199
|
+
if (matchIfElse !== null) {
|
|
200
|
+
// Get the if-then definition
|
|
201
|
+
const ifthen = (labelDefs.length > 0 ? (labelDefs[labelDefs.length - 1].if ?? null) : null);
|
|
202
|
+
if (ifthen === null) {
|
|
203
|
+
throw new BareScriptParserError('No matching if statement', line, 1, startLineNumber + ixLine);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Cannot have multiple else-then statements
|
|
207
|
+
if (ifthen.hasElse) {
|
|
208
|
+
throw new BareScriptParserError('Multiple else statements', line, 1, startLineNumber + ixLine);
|
|
209
|
+
}
|
|
210
|
+
ifthen.hasElse = true;
|
|
211
|
+
|
|
212
|
+
// Add the if-then else statements
|
|
213
|
+
statements.push(
|
|
214
|
+
{'jump': {'label': ifthen.done}},
|
|
215
|
+
{'label': ifthen.jump.label}
|
|
216
|
+
);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If-then end?
|
|
221
|
+
const matchIfEnd = line.match(rScriptIfEnd);
|
|
222
|
+
if (matchIfEnd !== null) {
|
|
223
|
+
// Pop the if-then definition
|
|
224
|
+
const ifthen = (labelDefs.length > 0 ? (labelDefs.pop().if ?? null) : null);
|
|
225
|
+
if (ifthen === null) {
|
|
226
|
+
throw new BareScriptParserError('No matching if statement', line, 1, startLineNumber + ixLine);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Update the previous jump statement's label, if necessary
|
|
230
|
+
if (!ifthen.hasElse) {
|
|
231
|
+
ifthen.jump.label = ifthen.done;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Add the if-then footer statement
|
|
235
|
+
statements.push({'label': ifthen.done});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// While-do begin?
|
|
240
|
+
const matchWhileBegin = line.match(rScriptWhileBegin);
|
|
241
|
+
if (matchWhileBegin !== null) {
|
|
242
|
+
// Add the while-do label
|
|
243
|
+
const whiledo = {
|
|
244
|
+
'loop': `__bareScriptLoop${labelIndex}`,
|
|
245
|
+
'continue': `__bareScriptLoop${labelIndex}`,
|
|
246
|
+
'done': `__bareScriptDone${labelIndex}`,
|
|
247
|
+
'expr': parseExpression(matchWhileBegin.groups.expr),
|
|
248
|
+
line,
|
|
249
|
+
'lineNumber': startLineNumber + ixLine
|
|
250
|
+
};
|
|
251
|
+
labelDefs.push({'while': whiledo});
|
|
252
|
+
labelIndex += 1;
|
|
253
|
+
|
|
254
|
+
// Add the while-do header statements
|
|
255
|
+
statements.push(
|
|
256
|
+
{'jump': {'label': whiledo.done, 'expr': {'unary': {'op': '!', 'expr': whiledo.expr}}}},
|
|
257
|
+
{'label': whiledo.loop}
|
|
258
|
+
);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// While-do end?
|
|
263
|
+
const matchWhileEnd = line.match(rScriptWhileEnd);
|
|
264
|
+
if (matchWhileEnd !== null) {
|
|
265
|
+
// Pop the while-do definition
|
|
266
|
+
const whiledo = (labelDefs.length > 0 ? (labelDefs.pop().while ?? null) : null);
|
|
267
|
+
if (whiledo === null) {
|
|
268
|
+
throw new BareScriptParserError('No matching while statement', line, 1, startLineNumber + ixLine);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Add the while-do footer statements
|
|
272
|
+
statements.push(
|
|
273
|
+
{'jump': {'label': whiledo.loop, 'expr': whiledo.expr}},
|
|
274
|
+
{'label': whiledo.done}
|
|
275
|
+
);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// For-each begin?
|
|
280
|
+
const matchForBegin = line.match(rScriptForBegin);
|
|
281
|
+
if (matchForBegin !== null) {
|
|
282
|
+
// Add the for-each label
|
|
283
|
+
const foreach = {
|
|
284
|
+
'loop': `__bareScriptLoop${labelIndex}`,
|
|
285
|
+
'continue': `__bareScriptContinue${labelIndex}`,
|
|
286
|
+
'done': `__bareScriptDone${labelIndex}`,
|
|
287
|
+
'index': matchForBegin.groups.index ?? `__bareScriptIndex${labelIndex}`,
|
|
288
|
+
'values': `__bareScriptValues${labelIndex}`,
|
|
289
|
+
'length': `__bareScriptLength${labelIndex}`,
|
|
290
|
+
'value': matchForBegin.groups.value,
|
|
291
|
+
line,
|
|
292
|
+
'lineNumber': startLineNumber + ixLine
|
|
293
|
+
};
|
|
294
|
+
labelDefs.push({'for': foreach});
|
|
295
|
+
labelIndex += 1;
|
|
296
|
+
|
|
297
|
+
// Add the for-each header statements
|
|
298
|
+
statements.push(
|
|
299
|
+
{'expr': {'name': foreach.values, 'expr': parseExpression(matchForBegin.groups.values)}},
|
|
300
|
+
{'expr': {'name': foreach.length, 'expr': {'function': {'name': 'arrayLength', 'args': [{'variable': foreach.values}]}}}},
|
|
301
|
+
{'jump': {'label': foreach.done, 'expr': {'unary': {'op': '!', 'expr': {'variable': foreach.length}}}}},
|
|
302
|
+
{'expr': {'name': foreach.index, 'expr': {'number': 0}}},
|
|
303
|
+
{'label': foreach.loop},
|
|
304
|
+
{'expr': {
|
|
305
|
+
'name': foreach.value,
|
|
306
|
+
'expr': {'function': {'name': 'arrayGet', 'args': [{'variable': foreach.values}, {'variable': foreach.index}]}}
|
|
307
|
+
}}
|
|
308
|
+
);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// For-each end?
|
|
313
|
+
const matchForEnd = line.match(rScriptForEnd);
|
|
314
|
+
if (matchForEnd !== null) {
|
|
315
|
+
// Pop the foreach definition
|
|
316
|
+
const foreach = (labelDefs.length > 0 ? (labelDefs.pop().for ?? null) : null);
|
|
317
|
+
if (foreach === null) {
|
|
318
|
+
throw new BareScriptParserError('No matching for statement', line, 1, startLineNumber + ixLine);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Add the for-each footer statements
|
|
322
|
+
if (foreach.hasContinue) {
|
|
323
|
+
statements.push({'label': foreach.continue});
|
|
324
|
+
}
|
|
325
|
+
statements.push(
|
|
326
|
+
{'expr': {
|
|
327
|
+
'name': foreach.index,
|
|
328
|
+
'expr': {'binary': {'op': '+', 'left': {'variable': foreach.index}, 'right': {'number': 1}}}
|
|
329
|
+
}},
|
|
330
|
+
{'jump': {
|
|
331
|
+
'label': foreach.loop,
|
|
332
|
+
'expr': {'binary': {'op': '<', 'left': {'variable': foreach.index}, 'right': {'variable': foreach.length}}}
|
|
333
|
+
}},
|
|
334
|
+
{'label': foreach.done}
|
|
335
|
+
);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Break statement?
|
|
340
|
+
const matchBreak = line.match(rScriptBreak);
|
|
341
|
+
if (matchBreak !== null) {
|
|
342
|
+
// Get the loop definition
|
|
343
|
+
const labelDef = (labelDefs.length > 0 ? arrayFindLast(labelDefs, (def) => !('if' in def)) : null);
|
|
344
|
+
if (labelDef === null) {
|
|
345
|
+
throw new BareScriptParserError('Break statement outside of loop', line, 1, startLineNumber + ixLine);
|
|
346
|
+
}
|
|
347
|
+
const [labelKey] = Object.keys(labelDef);
|
|
348
|
+
const loopDef = labelDef[labelKey];
|
|
349
|
+
|
|
350
|
+
// Add the break jump statement
|
|
351
|
+
statements.push({'jump': {'label': loopDef.done}});
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Continue statement?
|
|
356
|
+
const matchContinue = line.match(rScriptContinue);
|
|
357
|
+
if (matchContinue !== null) {
|
|
358
|
+
// Get the loop definition
|
|
359
|
+
const labelDef = (labelDefs.length > 0 ? arrayFindLast(labelDefs, (def) => !('if' in def)) : null);
|
|
360
|
+
if (labelDef === null) {
|
|
361
|
+
throw new BareScriptParserError('Continue statement outside of loop', line, 1, startLineNumber + ixLine);
|
|
362
|
+
}
|
|
363
|
+
const [labelKey] = Object.keys(labelDef);
|
|
364
|
+
const loopDef = labelDef[labelKey];
|
|
365
|
+
|
|
366
|
+
// Add the continue jump statement
|
|
367
|
+
loopDef.hasContinue = true;
|
|
368
|
+
statements.push({'jump': {'label': loopDef.continue}});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Label definition?
|
|
373
|
+
const matchLabel = line.match(rScriptLabel);
|
|
374
|
+
if (matchLabel !== null) {
|
|
375
|
+
statements.push({'label': matchLabel.groups.name});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Jump definition?
|
|
380
|
+
const matchJump = line.match(rScriptJump);
|
|
381
|
+
if (matchJump !== null) {
|
|
382
|
+
const jumpStatement = {'jump': {'label': matchJump.groups.name}};
|
|
383
|
+
if (typeof matchJump.groups.expr !== 'undefined') {
|
|
384
|
+
try {
|
|
385
|
+
jumpStatement.jump.expr = parseExpression(matchJump.groups.expr);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const columnNumber = matchJump.groups.jump.length - matchJump.groups.expr.length - 1 + error.columnNumber;
|
|
388
|
+
throw new BareScriptParserError(error.error, line, columnNumber, startLineNumber + ixLine);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
statements.push(jumpStatement);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Return definition?
|
|
396
|
+
const matchReturn = line.match(rScriptReturn);
|
|
397
|
+
if (matchReturn !== null) {
|
|
398
|
+
const returnStatement = {'return': {}};
|
|
399
|
+
if (typeof matchReturn.groups.expr !== 'undefined') {
|
|
400
|
+
try {
|
|
401
|
+
returnStatement.return.expr = parseExpression(matchReturn.groups.expr);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const columnNumber = matchReturn.groups.return.length - matchReturn.groups.expr.length + error.columnNumber;
|
|
404
|
+
throw new BareScriptParserError(error.error, line, columnNumber, startLineNumber + ixLine);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
statements.push(returnStatement);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Include definition?
|
|
412
|
+
const matchInclude = line.match(rScriptInclude) || line.match(rScriptIncludeSystem);
|
|
413
|
+
if (matchInclude !== null) {
|
|
414
|
+
const {delim} = matchInclude.groups;
|
|
415
|
+
const url = (delim === '<' ? matchInclude.groups.url : matchInclude.groups.url.replace(rExprStringEscape, '$1'));
|
|
416
|
+
let includeStatement = (statements.length ? statements[statements.length - 1] : null);
|
|
417
|
+
if (includeStatement === null || !('include' in includeStatement)) {
|
|
418
|
+
includeStatement = {'include': {'includes': []}};
|
|
419
|
+
statements.push(includeStatement);
|
|
420
|
+
}
|
|
421
|
+
includeStatement.include.includes.push(delim === '<' ? {url, 'system': true} : {url});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Expression
|
|
426
|
+
try {
|
|
427
|
+
const exprStatement = {'expr': {'expr': parseExpression(line)}};
|
|
428
|
+
statements.push(exprStatement);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
throw new BareScriptParserError(error.error, line, error.columnNumber, startLineNumber + ixLine);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Dangling label definitions?
|
|
435
|
+
if (labelDefs.length > 0) {
|
|
436
|
+
const labelDef = labelDefs.pop();
|
|
437
|
+
const [defKey] = Object.keys(labelDef);
|
|
438
|
+
const def = labelDef[defKey];
|
|
439
|
+
throw new BareScriptParserError(`Missing end${defKey} statement`, def.line, 1, def.lineNumber);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return script;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
// Firefox versions prior to 103 are missing Array.findLast - Debian Bookworm has Firefox 102
|
|
447
|
+
function arrayFindLast(array, findFn) {
|
|
448
|
+
let ixValue = array.length - 1;
|
|
449
|
+
while (ixValue >= 0) {
|
|
450
|
+
const value = array[ixValue];
|
|
451
|
+
if (findFn(value)) {
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
ixValue -= 1;
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
// BareScript expression regex
|
|
461
|
+
const rExprBinaryOp = /^\s*(\*\*|\*|\/|%|\+|-|<=|<|>=|>|==|!=|&&|\|\|)/;
|
|
462
|
+
const rExprUnaryOp = /^\s*(!|-)/;
|
|
463
|
+
const rExprFunctionOpen = /^\s*([A-Za-z_]\w+)\s*\(/;
|
|
464
|
+
const rExprFunctionSeparator = /^\s*,/;
|
|
465
|
+
const rExprFunctionClose = /^\s*\)/;
|
|
466
|
+
const rExprGroupOpen = /^\s*\(/;
|
|
467
|
+
const rExprGroupClose = /^\s*\)/;
|
|
468
|
+
const rExprNumber = /^\s*([+-]?\d+(?:\.\d*)?(?:e[+-]\d+)?)/;
|
|
469
|
+
const rExprString = /^\s*'((?:\\\\|\\'|[^'])*)'/;
|
|
470
|
+
const rExprStringEscape = /\\([\\'])/g;
|
|
471
|
+
const rExprStringDouble = /^\s*"((?:\\\\|\\"|[^"])*)"/;
|
|
472
|
+
const rExprStringDoubleEscape = /\\([\\"])/g;
|
|
473
|
+
const rExprVariable = /^\s*([A-Za-z_]\w*)/;
|
|
474
|
+
const rExprVariableEx = /^\s*\[\s*((?:\\\]|[^\]])+)\s*\]/;
|
|
475
|
+
const rExprVariableExEscape = /\\([\\\]])/g;
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
// Binary operator re-order map
|
|
479
|
+
const binaryReorder = {
|
|
480
|
+
'**': new Set(['*', '/', '%', '+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
481
|
+
'*': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
482
|
+
'/': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
483
|
+
'%': new Set(['+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
484
|
+
'+': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
485
|
+
'-': new Set(['<=', '<', '>=', '>', '==', '!=', '&&', '||']),
|
|
486
|
+
'<=': new Set(['==', '!=', '&&', '||']),
|
|
487
|
+
'<': new Set(['==', '!=', '&&', '||']),
|
|
488
|
+
'>=': new Set(['==', '!=', '&&', '||']),
|
|
489
|
+
'>': new Set(['==', '!=', '&&', '||']),
|
|
490
|
+
'==': new Set(['&&', '||']),
|
|
491
|
+
'!=': new Set(['&&', '||']),
|
|
492
|
+
'&&': new Set(['||']),
|
|
493
|
+
'||': new Set([])
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Parse a BareScript expression
|
|
499
|
+
*
|
|
500
|
+
* @param {string} exprText - The [expression text]{@link https://craigahobbs.github.io/bare-script/language/#Expressions}
|
|
501
|
+
* @returns {Object} The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
|
|
502
|
+
* @throws [BareScriptParserError]{@link module:lib/parser.BareScriptParserError}
|
|
503
|
+
*/
|
|
504
|
+
export function parseExpression(exprText) {
|
|
505
|
+
try {
|
|
506
|
+
const [expr, nextText] = parseBinaryExpression(exprText);
|
|
507
|
+
if (nextText.trim() !== '') {
|
|
508
|
+
throw new BareScriptParserError('Syntax error', nextText);
|
|
509
|
+
}
|
|
510
|
+
return expr;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
const columnNumber = exprText.length - error.line.length + 1;
|
|
513
|
+
throw new BareScriptParserError(error.error, exprText, columnNumber);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
// Helper function to parse a binary operator expression chain
|
|
519
|
+
function parseBinaryExpression(exprText, binLeftExpr = null) {
|
|
520
|
+
// Parse the binary operator's left unary expression if none was passed
|
|
521
|
+
let leftExpr;
|
|
522
|
+
let binText;
|
|
523
|
+
if (binLeftExpr !== null) {
|
|
524
|
+
binText = exprText;
|
|
525
|
+
leftExpr = binLeftExpr;
|
|
526
|
+
} else {
|
|
527
|
+
[leftExpr, binText] = parseUnaryExpression(exprText);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Match a binary operator - if not found, return the left expression
|
|
531
|
+
const matchBinaryOp = binText.match(rExprBinaryOp);
|
|
532
|
+
if (matchBinaryOp === null) {
|
|
533
|
+
return [leftExpr, binText];
|
|
534
|
+
}
|
|
535
|
+
const [, binOp] = matchBinaryOp;
|
|
536
|
+
const rightText = binText.slice(matchBinaryOp[0].length);
|
|
537
|
+
|
|
538
|
+
// Parse the right sub-expression
|
|
539
|
+
const [rightExpr, nextText] = parseUnaryExpression(rightText);
|
|
540
|
+
|
|
541
|
+
// Create the binary expression - re-order for binary operators as necessary
|
|
542
|
+
let binExpr;
|
|
543
|
+
if (Object.keys(leftExpr)[0] === 'binary' && binaryReorder[binOp].has(leftExpr.binary.op)) {
|
|
544
|
+
// Left expression has lower precendence - find where to put this expression within the left expression
|
|
545
|
+
binExpr = leftExpr;
|
|
546
|
+
let reorderExpr = leftExpr;
|
|
547
|
+
while (Object.keys(reorderExpr.binary.right)[0] === 'binary' &&
|
|
548
|
+
binaryReorder[binOp].has(reorderExpr.binary.right.binary.op)) {
|
|
549
|
+
reorderExpr = reorderExpr.binary.right;
|
|
550
|
+
}
|
|
551
|
+
reorderExpr.binary.right = {'binary': {'op': binOp, 'left': reorderExpr.binary.right, 'right': rightExpr}};
|
|
552
|
+
} else {
|
|
553
|
+
binExpr = {'binary': {'op': binOp, 'left': leftExpr, 'right': rightExpr}};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Parse the next binary expression in the chain
|
|
557
|
+
return parseBinaryExpression(nextText, binExpr);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
// Helper function to parse a unary expression
|
|
562
|
+
function parseUnaryExpression(exprText) {
|
|
563
|
+
// Group open?
|
|
564
|
+
const matchGroupOpen = exprText.match(rExprGroupOpen);
|
|
565
|
+
if (matchGroupOpen !== null) {
|
|
566
|
+
const groupText = exprText.slice(matchGroupOpen[0].length);
|
|
567
|
+
const [expr, nextText] = parseBinaryExpression(groupText);
|
|
568
|
+
const matchGroupClose = nextText.match(rExprGroupClose);
|
|
569
|
+
if (matchGroupClose === null) {
|
|
570
|
+
throw new BareScriptParserError('Unmatched parenthesis', exprText);
|
|
571
|
+
}
|
|
572
|
+
return [{'group': expr}, nextText.slice(matchGroupClose[0].length)];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Unary operator?
|
|
576
|
+
const matchUnary = exprText.match(rExprUnaryOp);
|
|
577
|
+
if (matchUnary !== null) {
|
|
578
|
+
const unaryText = exprText.slice(matchUnary[0].length);
|
|
579
|
+
const [expr, nextText] = parseUnaryExpression(unaryText);
|
|
580
|
+
const unaryExpr = {
|
|
581
|
+
'unary': {
|
|
582
|
+
'op': matchUnary[1],
|
|
583
|
+
expr
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
return [unaryExpr, nextText];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Function?
|
|
590
|
+
const matchFunctionOpen = exprText.match(rExprFunctionOpen);
|
|
591
|
+
if (matchFunctionOpen !== null) {
|
|
592
|
+
let argText = exprText.slice(matchFunctionOpen[0].length);
|
|
593
|
+
const args = [];
|
|
594
|
+
// eslint-disable-next-line no-constant-condition
|
|
595
|
+
while (true) {
|
|
596
|
+
// Function close?
|
|
597
|
+
const matchFunctionClose = argText.match(rExprFunctionClose);
|
|
598
|
+
if (matchFunctionClose !== null) {
|
|
599
|
+
argText = argText.slice(matchFunctionClose[0].length);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Function argument separator
|
|
604
|
+
if (args.length !== 0) {
|
|
605
|
+
const matchFunctionSeparator = argText.match(rExprFunctionSeparator);
|
|
606
|
+
if (matchFunctionSeparator === null) {
|
|
607
|
+
throw new BareScriptParserError('Syntax error', argText);
|
|
608
|
+
}
|
|
609
|
+
argText = argText.slice(matchFunctionSeparator[0].length);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Get the argument
|
|
613
|
+
const [argExpr, nextArgText] = parseBinaryExpression(argText);
|
|
614
|
+
args.push(argExpr);
|
|
615
|
+
argText = nextArgText;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const fnExpr = {
|
|
619
|
+
'function': {
|
|
620
|
+
'name': matchFunctionOpen[1],
|
|
621
|
+
'args': args
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
return [fnExpr, argText];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Number?
|
|
628
|
+
const matchNumber = exprText.match(rExprNumber);
|
|
629
|
+
if (matchNumber !== null) {
|
|
630
|
+
const number = parseFloat(matchNumber[1]);
|
|
631
|
+
const expr = {'number': number};
|
|
632
|
+
return [expr, exprText.slice(matchNumber[0].length)];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// String?
|
|
636
|
+
const matchString = exprText.match(rExprString);
|
|
637
|
+
if (matchString !== null) {
|
|
638
|
+
const string = matchString[1].replace(rExprStringEscape, '$1');
|
|
639
|
+
const expr = {'string': string};
|
|
640
|
+
return [expr, exprText.slice(matchString[0].length)];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// String (double quotes)?
|
|
644
|
+
const matchStringDouble = exprText.match(rExprStringDouble);
|
|
645
|
+
if (matchStringDouble !== null) {
|
|
646
|
+
const string = matchStringDouble[1].replace(rExprStringDoubleEscape, '$1');
|
|
647
|
+
const expr = {'string': string};
|
|
648
|
+
return [expr, exprText.slice(matchStringDouble[0].length)];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Variable?
|
|
652
|
+
const matchVariable = exprText.match(rExprVariable);
|
|
653
|
+
if (matchVariable !== null) {
|
|
654
|
+
const expr = {'variable': matchVariable[1]};
|
|
655
|
+
return [expr, exprText.slice(matchVariable[0].length)];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Variable (brackets)?
|
|
659
|
+
const matchVariableEx = exprText.match(rExprVariableEx);
|
|
660
|
+
if (matchVariableEx !== null) {
|
|
661
|
+
const variableName = matchVariableEx[1].replace(rExprVariableExEscape, '$1');
|
|
662
|
+
const expr = {'variable': variableName};
|
|
663
|
+
return [expr, exprText.slice(matchVariableEx[0].length)];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
throw new BareScriptParserError('Syntax error', exprText);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* A BareScript parser error
|
|
672
|
+
*
|
|
673
|
+
* @extends {Error}
|
|
674
|
+
* @property {string} error - The error description
|
|
675
|
+
* @property {string} line - The line text
|
|
676
|
+
* @property {number} columnNumber - The error column number
|
|
677
|
+
* @property {?number} lineNumber - The error line number
|
|
678
|
+
*/
|
|
679
|
+
export class BareScriptParserError extends Error {
|
|
680
|
+
/**
|
|
681
|
+
* Create a BareScript parser error
|
|
682
|
+
*
|
|
683
|
+
* @param {string} error - The error description
|
|
684
|
+
* @param {string} line - The line text
|
|
685
|
+
* @param {number} [columnNumber=1] - The error column number
|
|
686
|
+
* @param {?number} [lineNumber=null] - The error line number
|
|
687
|
+
* @param {?string} [prefix=null] - The error message prefix line
|
|
688
|
+
*/
|
|
689
|
+
constructor(error, line, columnNumber = 1, lineNumber = null, prefix = null) {
|
|
690
|
+
// Parser error constants
|
|
691
|
+
const lineLengthMax = 120;
|
|
692
|
+
const lineSuffix = ' ...';
|
|
693
|
+
const linePrefix = '... ';
|
|
694
|
+
|
|
695
|
+
// Trim the error line, if necessary
|
|
696
|
+
let lineError = line;
|
|
697
|
+
let lineColumn = columnNumber;
|
|
698
|
+
if (line.length > lineLengthMax) {
|
|
699
|
+
const lineLeft = columnNumber - 1 - 0.5 * lineLengthMax;
|
|
700
|
+
const lineRight = lineLeft + lineLengthMax;
|
|
701
|
+
if (lineLeft < 0) {
|
|
702
|
+
lineError = line.slice(0, lineLengthMax) + lineSuffix;
|
|
703
|
+
} else if (lineRight > line.length) {
|
|
704
|
+
lineError = linePrefix + line.slice(line.length - lineLengthMax);
|
|
705
|
+
lineColumn -= lineLeft - linePrefix.length - (lineRight - line.length);
|
|
706
|
+
} else {
|
|
707
|
+
lineError = linePrefix + line.slice(lineLeft, lineRight) + lineSuffix;
|
|
708
|
+
lineColumn -= lineLeft - linePrefix.length;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Format the message
|
|
713
|
+
const message = `\
|
|
714
|
+
${prefix !== null ? `${prefix}\n` : ''}${error}${lineNumber !== null ? `, line number ${lineNumber}` : ''}:
|
|
715
|
+
${lineError}
|
|
716
|
+
${' '.repeat(lineColumn - 1)}^
|
|
717
|
+
`;
|
|
718
|
+
super(message);
|
|
719
|
+
this.name = this.constructor.name;
|
|
720
|
+
this.error = error;
|
|
721
|
+
this.line = line;
|
|
722
|
+
this.columnNumber = columnNumber;
|
|
723
|
+
this.lineNumber = lineNumber;
|
|
724
|
+
}
|
|
725
|
+
}
|