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/__init__.py +33 -0
- bare_script/__main__.py +12 -0
- bare_script/bare.py +109 -0
- bare_script/baredoc.py +137 -0
- bare_script/library.py +1912 -0
- bare_script/model.py +257 -0
- bare_script/options.py +108 -0
- bare_script/parser.py +680 -0
- bare_script/runtime.py +345 -0
- bare_script/value.py +199 -0
- bare_script-0.9.0.dist-info/LICENSE +21 -0
- bare_script-0.9.0.dist-info/METADATA +189 -0
- bare_script-0.9.0.dist-info/RECORD +16 -0
- bare_script-0.9.0.dist-info/WHEEL +5 -0
- bare_script-0.9.0.dist-info/entry_points.txt +3 -0
- bare_script-0.9.0.dist-info/top_level.txt +1 -0
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
|