bare-script 0.9.0__py3-none-any.whl

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.
bare_script/parser.py ADDED
@@ -0,0 +1,680 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ The BareScript language parser
6
+ """
7
+
8
+ import re
9
+
10
+
11
+ # BareScript regex
12
+ R_SCRIPT_LINE_SPLIT = re.compile(r'\r?\n')
13
+ R_SCRIPT_CONTINUATION = re.compile(r'\\\s*$')
14
+ R_SCRIPT_COMMENT = re.compile(r'^\s*(?:#.*)?$')
15
+ R_SCRIPT_ASSIGNMENT = re.compile(r'^\s*(?P<name>[A-Za-z_]\w*)\s*=\s*(?P<expr>.+)$')
16
+ R_SCRIPT_FUNCTION_BEGIN = re.compile(
17
+ r'^(?P<async>\s*async)?\s*function\s+(?P<name>[A-Za-z_]\w*)\s*\('
18
+ r'\s*(?P<args>[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)?(?P<lastArgArray>\s*\.\.\.)?\s*\)\s*:\s*$'
19
+ )
20
+ R_SCRIPT_FUNCTION_ARG_SPLIT = re.compile(r'\s*,\s*')
21
+ R_SCRIPT_FUNCTION_END = re.compile(r'^\s*endfunction\s*$')
22
+ R_SCRIPT_LABEL = re.compile(r'^\s*(?P<name>[A-Za-z_]\w*)\s*:\s*$')
23
+ R_SCRIPT_JUMP = re.compile(r'^(?P<jump>\s*(?:jump|jumpif\s*\((?P<expr>.+)\)))\s+(?P<name>[A-Za-z_]\w*)\s*$')
24
+ R_SCRIPT_RETURN = re.compile(r'^(?P<return>\s*return(?:\s+(?P<expr>.+))?)\s*$')
25
+ R_SCRIPT_INCLUDE = re.compile(r'^\s*include\s+(?P<delim>\')(?P<url>(?:\\\'|[^\'])*)\'\s*$')
26
+ R_SCRIPT_INCLUDE_SYSTEM = re.compile(r'^\s*include\s+(?P<delim><)(?P<url>[^>]*)>\s*$')
27
+ R_SCRIPT_IF_BEGIN = re.compile(r'^\s*if\s+(?P<expr>.+)\s*:\s*$')
28
+ R_SCRIPT_IF_ELSE_IF = re.compile(r'^\s*elif\s+(?P<expr>.+)\s*:\s*$')
29
+ R_SCRIPT_IF_ELSE = re.compile(r'^\s*else\s*:\s*$')
30
+ R_SCRIPT_IF_END = re.compile(r'^\s*endif\s*$')
31
+ R_SCRIPT_FOR_BEGIN = re.compile(
32
+ r'^\s*for\s+(?P<value>[A-Za-z_]\w*)(?:\s*,\s*(?P<index>[A-Za-z_]\w*))?\s+in\s+(?P<values>.+)\s*:\s*$'
33
+ )
34
+ R_SCRIPT_FOR_END = re.compile(r'^\s*endfor\s*$')
35
+ R_SCRIPT_WHILE_BEGIN = re.compile(r'^\s*while\s+(?P<expr>.+)\s*:\s*$')
36
+ R_SCRIPT_WHILE_END = re.compile(r'^\s*endwhile\s*$')
37
+ R_SCRIPT_BREAK = re.compile(r'^\s*break\s*$')
38
+ R_SCRIPT_CONTINUE = re.compile(r'^\s*continue\s*$')
39
+
40
+
41
+ def parse_script(script_text, start_line_number=1):
42
+ """
43
+ Parse a BareScript script
44
+
45
+ :param script_text: The `script text <https://craigahobbs.github.io/bare-script/language/>`__
46
+ :type script_text: str or ~collections.abc.Iterable(str)
47
+ :param start_line_number: The script's starting line number
48
+ :type start_line_number: int, optional
49
+ :return: The `BareScript model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='BareScript'>`__
50
+ :rtype: dict
51
+ :raises BareScriptParserError: A parsing error occurred
52
+ """
53
+
54
+ script = {'statements': []}
55
+
56
+ # Line-split all script text
57
+ lines = []
58
+ if isinstance(script_text, str):
59
+ lines.extend(R_SCRIPT_LINE_SPLIT.split(script_text))
60
+ else:
61
+ for script_text_part in script_text:
62
+ lines.extend(R_SCRIPT_LINE_SPLIT.split(script_text_part))
63
+
64
+ # Process each line
65
+ line_continuation = []
66
+ function_def = None
67
+ function_label_def_depth = None
68
+ label_defs = []
69
+ label_index = 0
70
+ for ix_line_part, line_part in enumerate(lines):
71
+ statements = function_def['function']['statements'] if function_def is not None else script['statements']
72
+
73
+ # Comment?
74
+ if R_SCRIPT_COMMENT.match(line_part) is not None:
75
+ continue
76
+
77
+ # Set the line index
78
+ is_continued = (len(line_continuation) != 0)
79
+ if not is_continued:
80
+ ix_line = ix_line_part
81
+
82
+ # Line continuation?
83
+ line_part_no_continuation = R_SCRIPT_CONTINUATION.sub('', line_part)
84
+ if line_part != line_part_no_continuation:
85
+ line_continuation.append(line_part_no_continuation.strip() if is_continued else line_part_no_continuation.rstrip())
86
+ continue
87
+ elif is_continued:
88
+ line_continuation.append(line_part_no_continuation.strip())
89
+
90
+ # Join the continued script lines, if necessary
91
+ if is_continued:
92
+ line = ' '.join(line_continuation)
93
+ line_continuation.clear()
94
+ else:
95
+ line = line_part
96
+
97
+ # Assignment?
98
+ match_assignment = R_SCRIPT_ASSIGNMENT.match(line)
99
+ if match_assignment:
100
+ try:
101
+ expr_statement = {
102
+ 'expr': {
103
+ 'name': match_assignment.group('name'),
104
+ 'expr': parse_expression(match_assignment.group('expr'))
105
+ }
106
+ }
107
+ statements.append(expr_statement)
108
+ continue
109
+ except BareScriptParserError as error:
110
+ column_number = len(line) - len(match_assignment.group('expr')) + error.column_number
111
+ raise BareScriptParserError(error.error, line, column_number, start_line_number + ix_line)
112
+
113
+ # Function definition begin?
114
+ match_function_begin = R_SCRIPT_FUNCTION_BEGIN.match(line)
115
+ if match_function_begin:
116
+ # Nested function definitions are not allowed
117
+ if function_def is not None:
118
+ raise BareScriptParserError('Nested function definition', line, 1, start_line_number + ix_line)
119
+
120
+ # Add the function definition statement
121
+ function_label_def_depth = len(label_defs)
122
+ function_def = {
123
+ 'function': {
124
+ 'name': match_function_begin.group('name'),
125
+ 'statements': []
126
+ }
127
+ }
128
+ if match_function_begin.group('args') is not None:
129
+ function_def['function']['args'] = R_SCRIPT_FUNCTION_ARG_SPLIT.split(match_function_begin.group('args'))
130
+ if match_function_begin.group('async') is not None:
131
+ function_def['function']['async'] = True
132
+ if match_function_begin.group('lastArgArray') is not None:
133
+ function_def['function']['lastArgArray'] = True
134
+ statements.append(function_def)
135
+ continue
136
+
137
+ # Function definition end?
138
+ match_function_end = R_SCRIPT_FUNCTION_END.match(line)
139
+ if match_function_end:
140
+ if function_def is None:
141
+ raise BareScriptParserError('No matching function definition', line, 1, start_line_number + ix_line)
142
+
143
+ # Check for un-matched label definitions
144
+ if len(label_defs) > function_label_def_depth:
145
+ label_def = label_defs.pop()
146
+ def_key = next(iter(label_def))
147
+ def_ = label_def[def_key]
148
+ raise BareScriptParserError(f"Missing end{def_key} statement", def_['line'], 1, def_['lineNumber'])
149
+
150
+ function_def = None
151
+ function_label_def_depth = None
152
+ continue
153
+
154
+ # If-then begin?
155
+ match_if_begin = R_SCRIPT_IF_BEGIN.match(line)
156
+ if match_if_begin:
157
+ # Add the if-then label definition
158
+ ifthen = {
159
+ 'jump': {
160
+ 'label': f"__bareScriptIf{label_index}",
161
+ 'expr': {'unary': {'op': '!', 'expr': parse_expression(match_if_begin.group('expr'))}}
162
+ },
163
+ 'done': f"__bareScriptDone{label_index}",
164
+ 'hasElse': False,
165
+ 'line': line,
166
+ 'lineNumber': start_line_number + ix_line
167
+ }
168
+ label_defs.append({'if': ifthen})
169
+ label_index += 1
170
+
171
+ # Add the if-then header statement
172
+ statements.append({'jump': ifthen['jump']})
173
+ continue
174
+
175
+ # Else-if-then?
176
+ match_if_else_if = R_SCRIPT_IF_ELSE_IF.match(line)
177
+ if match_if_else_if:
178
+ # Get the if-then definition
179
+ label_def_depth = function_label_def_depth if function_def is not None else 0
180
+ ifthen = label_defs[len(label_defs) - 1].get('if') if len(label_defs) > label_def_depth else None
181
+ if ifthen is None:
182
+ raise BareScriptParserError('No matching if statement', line, 1, start_line_number + ix_line)
183
+
184
+ # Cannot come after the else-then statement
185
+ if ifthen['hasElse']:
186
+ raise BareScriptParserError('Elif statement following else statement', line, 1, start_line_number + ix_line)
187
+
188
+ # Generate the next if-then jump statement
189
+ prev_label = ifthen['jump']['label']
190
+ ifthen['jump'] = {
191
+ 'label': f"__bareScriptIf{label_index}",
192
+ 'expr': {'unary': {'op': '!', 'expr': parse_expression(match_if_else_if.group('expr'))}}
193
+ }
194
+ label_index += 1
195
+
196
+ # Add the if-then else statements
197
+ statements.extend([
198
+ {'jump': {'label': ifthen['done']}},
199
+ {'label': prev_label},
200
+ {'jump': ifthen['jump']}
201
+ ])
202
+ continue
203
+
204
+ # Else-then?
205
+ match_if_else = R_SCRIPT_IF_ELSE.match(line)
206
+ if match_if_else:
207
+ # Get the if-then definition
208
+ label_def_depth = function_label_def_depth if function_def is not None else 0
209
+ ifthen = label_defs[len(label_defs) - 1].get('if') if len(label_defs) > label_def_depth else None
210
+ if ifthen is None:
211
+ raise BareScriptParserError('No matching if statement', line, 1, start_line_number + ix_line)
212
+
213
+ # Cannot have multiple else-then statements
214
+ if ifthen['hasElse']:
215
+ raise BareScriptParserError('Multiple else statements', line, 1, start_line_number + ix_line)
216
+ ifthen['hasElse'] = True
217
+
218
+ # Add the if-then else statements
219
+ statements.extend([
220
+ {'jump': {'label': ifthen['done']}},
221
+ {'label': ifthen['jump']['label']}
222
+ ])
223
+ continue
224
+
225
+ # If-then end?
226
+ match_if_end = R_SCRIPT_IF_END.match(line)
227
+ if match_if_end:
228
+ # Pop the if-then definition
229
+ label_def_depth = function_label_def_depth if function_def is not None else 0
230
+ ifthen = label_defs.pop().get('if') if len(label_defs) > label_def_depth else None
231
+ if ifthen is None:
232
+ raise BareScriptParserError('No matching if statement', line, 1, start_line_number + ix_line)
233
+
234
+ # Update the previous jump statement's label, if necessary
235
+ if not ifthen['hasElse']:
236
+ ifthen['jump']['label'] = ifthen['done']
237
+
238
+ # Add the if-then footer statement
239
+ statements.append({'label': ifthen['done']})
240
+ continue
241
+
242
+ # While-do begin?
243
+ match_while_begin = R_SCRIPT_WHILE_BEGIN.match(line)
244
+ if match_while_begin:
245
+ # Add the while-do label
246
+ whiledo = {
247
+ 'loop': f'__bareScriptLoop{label_index}',
248
+ 'continue': f'__bareScriptLoop{label_index}',
249
+ 'done': f'__bareScriptDone{label_index}',
250
+ 'expr': parse_expression(match_while_begin.group('expr')),
251
+ 'line': line,
252
+ 'lineNumber': start_line_number + ix_line
253
+ }
254
+ label_defs.append({'while': whiledo})
255
+ label_index += 1
256
+
257
+ # Add the while-do header statements
258
+ statements.append({'jump': {'label': whiledo['done'], 'expr': {'unary': {'op': '!', 'expr': whiledo['expr']}}}})
259
+ statements.append({'label': whiledo['loop']})
260
+ continue
261
+
262
+ # While-do end?
263
+ match_while_end = R_SCRIPT_WHILE_END.match(line)
264
+ if match_while_end:
265
+ # Pop the while-do definition
266
+ label_def_depth = function_label_def_depth if function_def is not None else 0
267
+ if len(label_defs) <= label_def_depth:
268
+ raise BareScriptParserError('No matching while statement', line, 1, start_line_number + ix_line)
269
+
270
+ whiledo = label_defs.pop().get('while')
271
+ if not whiledo:
272
+ raise BareScriptParserError('No matching while statement', line, 1, start_line_number + ix_line)
273
+
274
+ # Add the while-do footer statements
275
+ statements.append({'jump': {'label': whiledo['loop'], 'expr': whiledo['expr']}})
276
+ statements.append({'label': whiledo['done']})
277
+ continue
278
+
279
+ # For-each begin?
280
+ match_for_begin = R_SCRIPT_FOR_BEGIN.match(line)
281
+ if match_for_begin:
282
+ # Add the for-each label
283
+ foreach = {
284
+ 'loop': f'__bareScriptLoop{label_index}',
285
+ 'continue': f'__bareScriptContinue{label_index}',
286
+ 'done': f'__bareScriptDone{label_index}',
287
+ 'index': match_for_begin.group('index') or f'__bareScriptIndex{label_index}',
288
+ 'values': f'__bareScriptValues{label_index}',
289
+ 'length': f'__bareScriptLength{label_index}',
290
+ 'value': match_for_begin.group('value'),
291
+ 'line': line,
292
+ 'lineNumber': start_line_number + ix_line
293
+ }
294
+ label_defs.append({'for': foreach})
295
+ label_index += 1
296
+
297
+ # Add the for-each header statements
298
+ statements.extend([
299
+ {'expr': {'name': foreach['values'], 'expr': parse_expression(match_for_begin.group('values'))}},
300
+ {'expr': {
301
+ 'name': foreach['length'],
302
+ 'expr': {'function': {'name': 'arrayLength', 'args': [{'variable': foreach['values']}]}}
303
+ }},
304
+ {'jump': {'label': foreach['done'], 'expr': {'unary': {'op': '!', 'expr': {'variable': foreach['length']}}}}},
305
+ {'expr': {'name': foreach['index'], 'expr': {'number': 0}}},
306
+ {'label': foreach['loop']},
307
+ {'expr': {
308
+ 'name': foreach['value'],
309
+ 'expr': {'function': {'name': 'arrayGet', 'args': [{'variable': foreach['values']}, {'variable': foreach['index']}]}}
310
+ }}
311
+ ])
312
+ continue
313
+
314
+ # For-each end?
315
+ match_for_end = R_SCRIPT_FOR_END.match(line)
316
+ if match_for_end:
317
+ # Pop the foreach definition
318
+ label_def_depth = function_label_def_depth if function_def is not None else 0
319
+ if len(label_defs) <= label_def_depth:
320
+ raise BareScriptParserError('No matching for statement', line, 1, start_line_number + ix_line)
321
+
322
+ foreach = label_defs.pop().get('for')
323
+ if not foreach:
324
+ raise BareScriptParserError('No matching for statement', line, 1, start_line_number + ix_line)
325
+
326
+ # Add the for-each footer statements
327
+ if foreach.get('hasContinue'):
328
+ statements.append({'label': foreach['continue']})
329
+ statements.extend([
330
+ {'expr': {
331
+ 'name': foreach['index'],
332
+ 'expr': {'binary': {'op': '+', 'left': {'variable': foreach['index']}, 'right': {'number': 1}}}
333
+ }},
334
+ {'jump': {
335
+ 'label': foreach['loop'],
336
+ 'expr': {'binary': {'op': '<', 'left': {'variable': foreach['index']}, 'right': {'variable': foreach['length']}}}
337
+ }},
338
+ {'label': foreach['done']}
339
+ ])
340
+ continue
341
+
342
+ # Break statement?
343
+ match_break = R_SCRIPT_BREAK.match(line)
344
+ if match_break:
345
+ # Get the loop definition
346
+ label_def_depth = function_label_def_depth if function_def is not None else 0
347
+ ix_label_def = next((i for i, d in reversed(list(enumerate(label_defs))) if 'if' not in d), -1)
348
+ if ix_label_def < label_def_depth:
349
+ raise BareScriptParserError('Break statement outside of loop', line, 1, start_line_number + ix_line)
350
+ label_def = label_defs[ix_label_def]
351
+ label_key = next(iter(label_def))
352
+ loop_def = label_def[label_key]
353
+
354
+ # Add the break jump statement
355
+ statements.append({'jump': {'label': loop_def['done']}})
356
+ continue
357
+
358
+ # Continue statement?
359
+ match_continue = R_SCRIPT_CONTINUE.match(line)
360
+ if match_continue:
361
+ # Get the loop definition
362
+ label_def_depth = function_label_def_depth if function_def is not None else 0
363
+ ix_label_def = next((i for i, d in reversed(list(enumerate(label_defs))) if 'if' not in d), -1)
364
+ if ix_label_def < label_def_depth:
365
+ raise BareScriptParserError('Continue statement outside of loop', line, 1, start_line_number + ix_line)
366
+ label_def = label_defs[ix_label_def]
367
+ label_key = next(iter(label_def))
368
+ loop_def = label_def[label_key]
369
+
370
+ # Add the continue jump statement
371
+ loop_def['hasContinue'] = True
372
+ statements.append({'jump': {'label': loop_def['continue']}})
373
+ continue
374
+
375
+ # Label definition?
376
+ match_label = R_SCRIPT_LABEL.match(line)
377
+ if match_label:
378
+ statements.append({'label': match_label.group('name')})
379
+ continue
380
+
381
+ # Jump definition?
382
+ match_jump = R_SCRIPT_JUMP.match(line)
383
+ if match_jump:
384
+ jump_statement = {'jump': {'label': match_jump.group('name')}}
385
+ if match_jump.group('expr'):
386
+ try:
387
+ jump_statement['jump']['expr'] = parse_expression(match_jump.group('expr'))
388
+ except BareScriptParserError as error:
389
+ column_number = len(match_jump.group('jump')) - len(match_jump.group('expr')) - 1 + error.column_number
390
+ raise BareScriptParserError(error.error, line, column_number, start_line_number + ix_line)
391
+ statements.append(jump_statement)
392
+ continue
393
+
394
+ # Return definition?
395
+ match_return = R_SCRIPT_RETURN.match(line)
396
+ if match_return:
397
+ return_statement = {'return': {}}
398
+ if match_return.group('expr'):
399
+ try:
400
+ return_statement['return']['expr'] = parse_expression(match_return.group('expr'))
401
+ except BareScriptParserError as error:
402
+ column_number = len(match_return.group('return')) - len(match_return.group('expr')) + error.column_number
403
+ raise BareScriptParserError(error.error, line, column_number, start_line_number + ix_line)
404
+ statements.append(return_statement)
405
+ continue
406
+
407
+ # Include definition?
408
+ match_include = R_SCRIPT_INCLUDE.match(line) or R_SCRIPT_INCLUDE_SYSTEM.match(line)
409
+ if match_include:
410
+ delim = match_include.group('delim')
411
+ url = match_include.group('url') if delim == '<' else R_EXPR_STRING_ESCAPE.sub('\\1', match_include.group('url'))
412
+ include_statement = statements[-1] if statements else None
413
+ if include_statement is None or 'include' not in include_statement:
414
+ include_statement = {'include': {'includes': []}}
415
+ statements.append(include_statement)
416
+ include_statement['include']['includes'].append({'url': url, 'system': True} if delim == '<' else {'url': url})
417
+ continue
418
+
419
+ # Expression
420
+ try:
421
+ expr_statement = {'expr': {'expr': parse_expression(line)}}
422
+ statements.append(expr_statement)
423
+ except BareScriptParserError as error:
424
+ raise BareScriptParserError(error.error, line, error.column_number, start_line_number + ix_line)
425
+
426
+ # Dangling label definitions?
427
+ if label_defs:
428
+ label_def = label_defs.pop()
429
+ def_key = next(iter(label_def))
430
+ def_ = label_def[def_key]
431
+ raise BareScriptParserError(f"Missing end{def_key} statement", def_['line'], 1, def_['lineNumber'])
432
+
433
+ return script
434
+
435
+
436
+ # BareScript expression regex
437
+ R_EXPR_BINARY_OP = re.compile(r'^\s*(\*\*|\*|\/|%|\+|-|<=|<|>=|>|==|!=|&&|\|\|)')
438
+ R_EXPR_UNARY_OP = re.compile(r'^\s*(!|-)')
439
+ R_EXPR_FUNCTION_OPEN = re.compile(r'^\s*([A-Za-z_]\w+)\s*\(')
440
+ R_EXPR_FUNCTION_SEPARATOR = re.compile(r'^\s*,')
441
+ R_EXPR_FUNCTION_CLOSE = re.compile(r'^\s*\)')
442
+ R_EXPR_GROUP_OPEN = re.compile(r'^\s*\(')
443
+ R_EXPR_GROUP_CLOSE = re.compile(r'^\s*\)')
444
+ R_EXPR_NUMBER = re.compile(r'^\s*([+-]?\d+(?:\.\d*)?(?:e[+-]\d+)?)')
445
+ R_EXPR_STRING = re.compile(r"^\s*'((?:\\\\|\\'|[^'])*)'")
446
+ R_EXPR_STRING_ESCAPE = re.compile(r'\\([\\\'])')
447
+ R_EXPR_STRING_DOUBLE = re.compile(r'^\s*"((?:\\\\|\\"|[^"])*)"')
448
+ R_EXPR_STRING_DOUBLE_ESCAPE = re.compile(r'\\([\\"])')
449
+ R_EXPR_VARIABLE = re.compile(r'^\s*([A-Za-z_]\w*)')
450
+ R_EXPR_VARIABLE_EX = re.compile(r'^\s*\[\s*((?:\\\]|[^\]])+)\s*\]')
451
+ R_EXPR_VARIABLE_EX_ESCAPE = re.compile(r'\\([\\\]])')
452
+
453
+
454
+ # Binary operator re-order map
455
+ BINARY_REORDER = {
456
+ '**': {'*', '/', '%', '+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||'},
457
+ '*': {'+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||'},
458
+ '/': {'+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||'},
459
+ '%': {'+', '-', '<=', '<', '>=', '>', '==', '!=', '&&', '||'},
460
+ '+': {'<=', '<', '>=', '>', '==', '!=', '&&', '||'},
461
+ '-': {'<=', '<', '>=', '>', '==', '!=', '&&', '||'},
462
+ '<=': {'==', '!=', '&&', '||'},
463
+ '<': {'==', '!=', '&&', '||'},
464
+ '>=': {'==', '!=', '&&', '||'},
465
+ '>': {'==', '!=', '&&', '||'},
466
+ '==': {'&&', '||'},
467
+ '!=': {'&&', '||'},
468
+ '&&': {'||'},
469
+ '||': set()
470
+ }
471
+
472
+
473
+ def parse_expression(expr_text):
474
+ """
475
+ Parse a BareScript expression
476
+
477
+ :param expr_text: The `expression text <https://craigahobbs.github.io/bare-script/language/#expressions>`__
478
+ :type expr_text: str or ~collections.abc.Iterable(str)
479
+ :return: The `expression model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='Expression'>`__
480
+ :rtype: dict
481
+ :raises BareScriptParserError: A parsing error occurred
482
+ """
483
+ try:
484
+ expr, next_text = _parse_binary_expression(expr_text)
485
+ if next_text.strip() != '':
486
+ raise BareScriptParserError('Syntax error', next_text)
487
+ return expr
488
+ except BareScriptParserError as error:
489
+ column_number = len(expr_text) - len(error.line) + 1
490
+ raise BareScriptParserError(error.error, expr_text, column_number)
491
+
492
+
493
+ # Helper function to parse a binary operator expression chain
494
+ def _parse_binary_expression(expr_text, bin_left_expr=None):
495
+ # Parse the binary operator's left unary expression if none was passed
496
+ if bin_left_expr is not None:
497
+ bin_text = expr_text
498
+ left_expr = bin_left_expr
499
+ else:
500
+ left_expr, bin_text = _parse_unary_expression(expr_text)
501
+
502
+ # Match a binary operator - if not found, return the left expression
503
+ match_binary_op = R_EXPR_BINARY_OP.match(bin_text)
504
+ if match_binary_op is None:
505
+ return [left_expr, bin_text]
506
+ bin_op = match_binary_op.group(1)
507
+ right_text = bin_text[len(match_binary_op.group(0)):]
508
+
509
+ # Parse the right sub-expression
510
+ right_expr, next_text = _parse_unary_expression(right_text)
511
+
512
+ # Create the binary expression - re-order for binary operators as necessary
513
+ bin_expr = None
514
+ if 'binary' in left_expr and bin_op in BINARY_REORDER and left_expr['binary']['op'] in BINARY_REORDER[bin_op]:
515
+ # Left expression has lower precedence - find where to put this expression within the left expression
516
+ bin_expr = left_expr
517
+ reorder_expr = left_expr
518
+ while 'binary' in reorder_expr['binary']['right'] and reorder_expr['binary']['right']['binary']['op'] in BINARY_REORDER[bin_op]:
519
+ reorder_expr = reorder_expr['binary']['right']
520
+ reorder_expr['binary']['right'] = {'binary': {'op': bin_op, 'left': reorder_expr['binary']['right'], 'right': right_expr}}
521
+ else:
522
+ bin_expr = {'binary': {'op': bin_op, 'left': left_expr, 'right': right_expr}}
523
+
524
+ # Parse the next binary expression in the chain
525
+ return _parse_binary_expression(next_text, bin_expr)
526
+
527
+
528
+ # Helper function to parse a unary expression
529
+ def _parse_unary_expression(expr_text):
530
+ # Group open?
531
+ match_group_open = R_EXPR_GROUP_OPEN.match(expr_text)
532
+ if match_group_open:
533
+ group_text = expr_text[len(match_group_open.group(0)):]
534
+ expr, next_text = _parse_binary_expression(group_text)
535
+ match_group_close = R_EXPR_GROUP_CLOSE.match(next_text)
536
+ if match_group_close is None:
537
+ raise BareScriptParserError('Unmatched parenthesis', expr_text)
538
+ return [{'group': expr}, next_text[len(match_group_close.group(0)):]]
539
+
540
+ # Unary operator?
541
+ match_unary = R_EXPR_UNARY_OP.match(expr_text)
542
+ if match_unary:
543
+ unary_text = expr_text[len(match_unary.group(0)):]
544
+ expr, next_text = _parse_unary_expression(unary_text)
545
+ unary_expr = {'unary': {'op': match_unary.group(1), 'expr': expr}}
546
+ return [unary_expr, next_text]
547
+
548
+ # Function?
549
+ match_function_open = R_EXPR_FUNCTION_OPEN.match(expr_text)
550
+ if match_function_open:
551
+ arg_text = expr_text[len(match_function_open.group(0)):]
552
+ args = []
553
+ while True:
554
+ # Function close?
555
+ match_function_close = R_EXPR_FUNCTION_CLOSE.match(arg_text)
556
+ if match_function_close:
557
+ arg_text = arg_text[len(match_function_close.group(0)):]
558
+ break
559
+
560
+ # Function argument separator
561
+ if args:
562
+ match_function_separator = R_EXPR_FUNCTION_SEPARATOR.match(arg_text)
563
+ if match_function_separator is None:
564
+ raise BareScriptParserError('Syntax error', arg_text)
565
+ arg_text = arg_text[len(match_function_separator.group(0)):]
566
+
567
+ # Get the argument
568
+ arg_expr, next_arg_text = _parse_binary_expression(arg_text)
569
+ args.append(arg_expr)
570
+ arg_text = next_arg_text
571
+
572
+ fn_expr = {'function': {'name': match_function_open.group(1), 'args': args}}
573
+ return [fn_expr, arg_text]
574
+
575
+ # Number?
576
+ match_number = R_EXPR_NUMBER.match(expr_text)
577
+ if match_number:
578
+ number = float(match_number.group(1))
579
+ expr = {'number': number}
580
+ return [expr, expr_text[len(match_number.group(0)):]]
581
+
582
+ # String?
583
+ match_string = R_EXPR_STRING.match(expr_text)
584
+ if match_string:
585
+ string = R_EXPR_STRING_ESCAPE.sub('\\1', match_string.group(1))
586
+ expr = {'string': string}
587
+ return [expr, expr_text[len(match_string.group(0)):]]
588
+
589
+ # String (double quotes)?
590
+ match_string_double = R_EXPR_STRING_DOUBLE.match(expr_text)
591
+ if match_string_double:
592
+ string = R_EXPR_STRING_DOUBLE_ESCAPE.sub('\\1', match_string_double.group(1))
593
+ expr = {'string': string}
594
+ return [expr, expr_text[len(match_string_double.group(0)):]]
595
+
596
+ # Variable?
597
+ match_variable = R_EXPR_VARIABLE.match(expr_text)
598
+ if match_variable:
599
+ expr = {'variable': match_variable.group(1)}
600
+ return [expr, expr_text[len(match_variable.group(0)):]]
601
+
602
+ # Variable (brackets)?
603
+ match_variable_ex = R_EXPR_VARIABLE_EX.match(expr_text)
604
+ if match_variable_ex:
605
+ variable_name = R_EXPR_VARIABLE_EX_ESCAPE.sub('\\1', match_variable_ex.group(1))
606
+ expr = {'variable': variable_name}
607
+ return [expr, expr_text[len(match_variable_ex.group(0)):]]
608
+
609
+ raise BareScriptParserError('Syntax error', expr_text)
610
+
611
+
612
+ class BareScriptParserError(Exception):
613
+ """
614
+ A BareScript parser exception
615
+
616
+ .. attribute:: error
617
+ :type: str
618
+
619
+ The error description
620
+
621
+ .. attribute:: line
622
+ :type: str
623
+
624
+ The line text
625
+
626
+ .. attribute:: column_number
627
+ :type: int
628
+
629
+ The error column number
630
+
631
+ .. attribute:: line_number
632
+ :type: int or None
633
+
634
+ The error line number
635
+
636
+ :param error: The error description
637
+ :type error: str
638
+ :param line: The line text
639
+ :type line: str
640
+ :param column_number: The error column number
641
+ :type column_number: int, optional
642
+ :param line_number: The error line number
643
+ :type line_number: int or None, optional
644
+ :param prefix: The error message prefix line
645
+ :type prefix: str or None, optional
646
+ """
647
+
648
+ def __init__(self, error, line, column_number=1, line_number=None, prefix=None):
649
+ # Parser error constants
650
+ line_length_max = 120
651
+ line_suffix = ' ...'
652
+ line_prefix = '... '
653
+
654
+ # Trim the error line, if necessary
655
+ line_error = line
656
+ line_column = column_number
657
+ if len(line) > line_length_max:
658
+ line_left = column_number - 1 - line_length_max // 2
659
+ line_right = line_left + line_length_max
660
+ if line_left < 0:
661
+ line_error = line[:line_length_max] + line_suffix
662
+ elif line_right > len(line):
663
+ line_error = line_prefix + line[-line_length_max:]
664
+ line_column -= line_left - len(line_prefix) - (line_right - len(line))
665
+ else:
666
+ line_error = line_prefix + line[int(line_left):int(line_right)] + line_suffix
667
+ line_column -= line_left - len(line_prefix)
668
+
669
+ # Format the message
670
+ newline = '\n'
671
+ message = f'''\
672
+ {(prefix + newline) if prefix is not None else ''}{error}{(', line number ' + str(line_number)) if line_number is not None else ''}:
673
+ {line_error}
674
+ {' ' * (line_column - 1)}^
675
+ '''
676
+ super().__init__(message)
677
+ self.error = error
678
+ self.line = line
679
+ self.column_number = column_number
680
+ self.line_number = line_number