brittainscript 0.1.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.
- brittainscript-0.1.0.dist-info/METADATA +276 -0
- brittainscript-0.1.0.dist-info/RECORD +14 -0
- brittainscript-0.1.0.dist-info/WHEEL +5 -0
- brittainscript-0.1.0.dist-info/entry_points.txt +2 -0
- brittainscript-0.1.0.dist-info/licenses/LICENSE +21 -0
- brittainscript-0.1.0.dist-info/top_level.txt +2 -0
- core/__init__.py +0 -0
- core/lexer.py +132 -0
- core/main.py +364 -0
- core/parser.py +384 -0
- core/parsetab.py +71 -0
- libs/__init__.py +0 -0
- libs/convert.bs +57 -0
- libs/math.bs +47 -0
core/main.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import sys, os
|
|
2
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
3
|
+
import re
|
|
4
|
+
import lexer as lexer_module
|
|
5
|
+
import parser as parser_module
|
|
6
|
+
|
|
7
|
+
functions = {}
|
|
8
|
+
|
|
9
|
+
class BreakSignal(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class ContinueSignal(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class ReturnSignal(Exception):
|
|
16
|
+
def __init__(self, value):
|
|
17
|
+
self.value = value
|
|
18
|
+
|
|
19
|
+
def strip_inline_comment(line):
|
|
20
|
+
in_string = False
|
|
21
|
+
escaped = False
|
|
22
|
+
result = []
|
|
23
|
+
for char in line:
|
|
24
|
+
if escaped:
|
|
25
|
+
result.append(char)
|
|
26
|
+
escaped = False
|
|
27
|
+
continue
|
|
28
|
+
if char == '\\' and in_string:
|
|
29
|
+
result.append(char)
|
|
30
|
+
escaped = True
|
|
31
|
+
continue
|
|
32
|
+
if char == '"':
|
|
33
|
+
in_string = not in_string
|
|
34
|
+
result.append(char)
|
|
35
|
+
continue
|
|
36
|
+
if char == '#' and not in_string:
|
|
37
|
+
break
|
|
38
|
+
result.append(char)
|
|
39
|
+
return ''.join(result).strip()
|
|
40
|
+
|
|
41
|
+
def indentation(line):
|
|
42
|
+
return len(line) - len(line.lstrip(' \t'))
|
|
43
|
+
|
|
44
|
+
def split_assignment(line):
|
|
45
|
+
in_string = False
|
|
46
|
+
escaped = False
|
|
47
|
+
depth = 0
|
|
48
|
+
for index, char in enumerate(line):
|
|
49
|
+
if escaped:
|
|
50
|
+
escaped = False
|
|
51
|
+
continue
|
|
52
|
+
if char == '\\' and in_string:
|
|
53
|
+
escaped = True
|
|
54
|
+
continue
|
|
55
|
+
if char == '"':
|
|
56
|
+
in_string = not in_string
|
|
57
|
+
continue
|
|
58
|
+
if in_string:
|
|
59
|
+
continue
|
|
60
|
+
if char in '([':
|
|
61
|
+
depth += 1
|
|
62
|
+
continue
|
|
63
|
+
if char in ')]':
|
|
64
|
+
depth -= 1
|
|
65
|
+
continue
|
|
66
|
+
if char == '=' and depth == 0:
|
|
67
|
+
previous_char = line[index - 1] if index > 0 else ''
|
|
68
|
+
next_char = line[index + 1] if index + 1 < len(line) else ''
|
|
69
|
+
if previous_char in ('=', '!', '<', '>') or next_char == '=':
|
|
70
|
+
continue
|
|
71
|
+
return line[:index].strip(), line[index + 1:].strip()
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def parse_expression(text):
|
|
75
|
+
return parser_module.parser.parse(text, lexer=lexer_module.lexer.clone())
|
|
76
|
+
|
|
77
|
+
def execute_line(line):
|
|
78
|
+
assignment = split_assignment(line)
|
|
79
|
+
if assignment:
|
|
80
|
+
target, expression = assignment
|
|
81
|
+
parser_module.assign_target(target, parse_expression(expression))
|
|
82
|
+
return
|
|
83
|
+
result = parse_expression(line)
|
|
84
|
+
if result is not None:
|
|
85
|
+
print(result)
|
|
86
|
+
|
|
87
|
+
def block_keyword(line):
|
|
88
|
+
for keyword in ('cond', 'while', 'for', 'func'):
|
|
89
|
+
if line == keyword or line.startswith(keyword + ' ') or line.startswith(keyword + '('):
|
|
90
|
+
return keyword
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def is_block_start(line):
|
|
94
|
+
return block_keyword(line) is not None
|
|
95
|
+
|
|
96
|
+
def collect_block(lines, start_index, parent_indent=0):
|
|
97
|
+
body = []
|
|
98
|
+
i = start_index
|
|
99
|
+
block_indents = [parent_indent]
|
|
100
|
+
while i < len(lines):
|
|
101
|
+
line = strip_inline_comment(lines[i])
|
|
102
|
+
if not line:
|
|
103
|
+
body.append(lines[i])
|
|
104
|
+
i += 1
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
current_indent = indentation(lines[i])
|
|
108
|
+
while len(block_indents) > 1 and current_indent <= block_indents[-1] and line != 'end':
|
|
109
|
+
block_indents.pop()
|
|
110
|
+
if len(block_indents) == 1 and current_indent <= parent_indent and line != 'end':
|
|
111
|
+
return body, i
|
|
112
|
+
|
|
113
|
+
if is_block_start(line):
|
|
114
|
+
block_indents.append(current_indent)
|
|
115
|
+
elif line == 'end':
|
|
116
|
+
if len(block_indents) == 1:
|
|
117
|
+
return body, i + 1
|
|
118
|
+
block_indents.pop()
|
|
119
|
+
body.append(lines[i])
|
|
120
|
+
i += 1
|
|
121
|
+
print("Syntax error: missing end")
|
|
122
|
+
return body, i
|
|
123
|
+
|
|
124
|
+
def parse_colon_expression(line, keyword):
|
|
125
|
+
expression = line[len(keyword):].strip()
|
|
126
|
+
if expression.endswith(':'):
|
|
127
|
+
expression = expression[:-1].strip()
|
|
128
|
+
if expression.startswith('(') and expression.endswith(')'):
|
|
129
|
+
expression = expression[1:-1].strip()
|
|
130
|
+
return expression
|
|
131
|
+
|
|
132
|
+
def execute_cond(line, body):
|
|
133
|
+
condition_text = parse_colon_expression(line, 'cond')
|
|
134
|
+
if parse_expression(condition_text):
|
|
135
|
+
execute_lines(body)
|
|
136
|
+
|
|
137
|
+
def execute_while(line, body):
|
|
138
|
+
condition_text = parse_colon_expression(line, 'while')
|
|
139
|
+
while parse_expression(condition_text):
|
|
140
|
+
try:
|
|
141
|
+
execute_lines(body)
|
|
142
|
+
except ContinueSignal:
|
|
143
|
+
continue
|
|
144
|
+
except BreakSignal:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
def execute_for(line, body):
|
|
148
|
+
match = re.fullmatch(r'for\s+([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+?):?', line)
|
|
149
|
+
if not match:
|
|
150
|
+
print("Syntax error: expected for name in expression:")
|
|
151
|
+
return
|
|
152
|
+
name, iterable_text = match.groups()
|
|
153
|
+
iterable = parse_expression(iterable_text)
|
|
154
|
+
if iterable is None:
|
|
155
|
+
return
|
|
156
|
+
try:
|
|
157
|
+
iterator = iter(iterable)
|
|
158
|
+
except TypeError:
|
|
159
|
+
print("Error: for loop target is not iterable")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
for value in iterator:
|
|
163
|
+
parser_module.set_name(name, value)
|
|
164
|
+
try:
|
|
165
|
+
execute_lines(body)
|
|
166
|
+
except ContinueSignal:
|
|
167
|
+
continue
|
|
168
|
+
except BreakSignal:
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
def execute_func_definition(line, body):
|
|
172
|
+
match = re.fullmatch(r'func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*?)\)\s*:?', line)
|
|
173
|
+
if not match:
|
|
174
|
+
print("Syntax error: expected func name(arg1, arg2):")
|
|
175
|
+
return
|
|
176
|
+
name, raw_params = match.groups()
|
|
177
|
+
params = [param.strip() for param in raw_params.split(',') if param.strip()]
|
|
178
|
+
invalid = [param for param in params if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', param)]
|
|
179
|
+
if invalid:
|
|
180
|
+
print("Syntax error: invalid function parameter")
|
|
181
|
+
return
|
|
182
|
+
functions[name] = (params, body)
|
|
183
|
+
|
|
184
|
+
def call_user_function(name, args):
|
|
185
|
+
if name not in functions:
|
|
186
|
+
print(f"Undefined function: {name}")
|
|
187
|
+
return None
|
|
188
|
+
params, body = functions[name]
|
|
189
|
+
if len(args) != len(params):
|
|
190
|
+
print(f"Error: {name}() expects {len(params)} arguments")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
parser_module.push_scope(dict(zip(params, args)))
|
|
194
|
+
try:
|
|
195
|
+
execute_lines(body)
|
|
196
|
+
except ReturnSignal as signal:
|
|
197
|
+
return signal.value
|
|
198
|
+
except BreakSignal:
|
|
199
|
+
print("Error: break used outside a loop")
|
|
200
|
+
return None
|
|
201
|
+
except ContinueSignal:
|
|
202
|
+
print("Error: continue used outside a loop")
|
|
203
|
+
return None
|
|
204
|
+
finally:
|
|
205
|
+
parser_module.pop_scope()
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def call_module_function(module, func_name, args):
|
|
209
|
+
funcs = module['funcs']
|
|
210
|
+
if func_name not in funcs:
|
|
211
|
+
print(f"Error: '{module['name']}' has no function '{func_name}'")
|
|
212
|
+
return None
|
|
213
|
+
params, body = funcs[func_name]
|
|
214
|
+
if len(args) != len(params):
|
|
215
|
+
print(f"Error: {func_name}() expects {len(params)} arguments")
|
|
216
|
+
return None
|
|
217
|
+
# Register module functions globally so they can call each other recursively
|
|
218
|
+
for name, defn in funcs.items():
|
|
219
|
+
functions[name] = defn
|
|
220
|
+
parser_module.push_scope(dict(zip(params, args)))
|
|
221
|
+
try:
|
|
222
|
+
execute_lines(body)
|
|
223
|
+
except ReturnSignal as s:
|
|
224
|
+
return s.value
|
|
225
|
+
except BreakSignal:
|
|
226
|
+
print("Error: break used outside a loop")
|
|
227
|
+
return None
|
|
228
|
+
except ContinueSignal:
|
|
229
|
+
print("Error: continue used outside a loop")
|
|
230
|
+
return None
|
|
231
|
+
finally:
|
|
232
|
+
parser_module.pop_scope()
|
|
233
|
+
for name in funcs:
|
|
234
|
+
functions.pop(name, None)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def import_module(lib_name):
|
|
238
|
+
libs_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'libs'))
|
|
239
|
+
lib_path = os.path.join(libs_dir, lib_name + '.bs')
|
|
240
|
+
if not os.path.exists(lib_path):
|
|
241
|
+
print(f"Error: library '{lib_name}' not found")
|
|
242
|
+
return None
|
|
243
|
+
before = set(functions.keys())
|
|
244
|
+
with open(lib_path, 'r') as f:
|
|
245
|
+
lines = f.readlines()
|
|
246
|
+
try:
|
|
247
|
+
execute_lines(lines)
|
|
248
|
+
except (BreakSignal, ContinueSignal, ReturnSignal):
|
|
249
|
+
pass
|
|
250
|
+
new_names = set(functions.keys()) - before
|
|
251
|
+
module_funcs = {name: functions.pop(name) for name in new_names}
|
|
252
|
+
return {'__bs_module__': True, 'name': lib_name, 'funcs': module_funcs}
|
|
253
|
+
|
|
254
|
+
parser_module.set_function_caller(call_user_function)
|
|
255
|
+
parser_module.set_module_caller(call_module_function)
|
|
256
|
+
|
|
257
|
+
def execute_lines(lines):
|
|
258
|
+
i = 0
|
|
259
|
+
while i < len(lines):
|
|
260
|
+
line = strip_inline_comment(lines[i])
|
|
261
|
+
if not line:
|
|
262
|
+
i += 1
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
first_word = block_keyword(line) or line.split()[0]
|
|
266
|
+
if first_word == 'cond':
|
|
267
|
+
body, i = collect_block(lines, i + 1, indentation(lines[i]))
|
|
268
|
+
execute_cond(line, body)
|
|
269
|
+
elif first_word == 'while':
|
|
270
|
+
body, i = collect_block(lines, i + 1, indentation(lines[i]))
|
|
271
|
+
execute_while(line, body)
|
|
272
|
+
elif first_word == 'for':
|
|
273
|
+
body, i = collect_block(lines, i + 1, indentation(lines[i]))
|
|
274
|
+
execute_for(line, body)
|
|
275
|
+
elif first_word == 'func':
|
|
276
|
+
body, i = collect_block(lines, i + 1, indentation(lines[i]))
|
|
277
|
+
execute_func_definition(line, body)
|
|
278
|
+
elif first_word == 'break':
|
|
279
|
+
raise BreakSignal()
|
|
280
|
+
elif first_word == 'continue':
|
|
281
|
+
raise ContinueSignal()
|
|
282
|
+
elif first_word == 'return':
|
|
283
|
+
return_text = line[len('return'):].strip()
|
|
284
|
+
raise ReturnSignal(parse_expression(return_text) if return_text else None)
|
|
285
|
+
elif first_word == 'add':
|
|
286
|
+
parts = line.split(None, 1)
|
|
287
|
+
if len(parts) < 2:
|
|
288
|
+
print("Syntax error: expected import name")
|
|
289
|
+
else:
|
|
290
|
+
lib_name = parts[1].strip()
|
|
291
|
+
module = import_module(lib_name)
|
|
292
|
+
if module:
|
|
293
|
+
parser_module.set_name(lib_name, module)
|
|
294
|
+
i += 1
|
|
295
|
+
elif first_word == 'end':
|
|
296
|
+
print("Syntax error: unexpected end")
|
|
297
|
+
i += 1
|
|
298
|
+
else:
|
|
299
|
+
execute_line(line)
|
|
300
|
+
i += 1
|
|
301
|
+
|
|
302
|
+
def run_file(file_path):
|
|
303
|
+
with open(file_path, 'r') as f:
|
|
304
|
+
lines = f.readlines()
|
|
305
|
+
try:
|
|
306
|
+
execute_lines(lines)
|
|
307
|
+
except BreakSignal:
|
|
308
|
+
print("Error: break used outside a loop")
|
|
309
|
+
except ContinueSignal:
|
|
310
|
+
print("Error: continue used outside a loop")
|
|
311
|
+
except ReturnSignal:
|
|
312
|
+
print("Error: return used outside a function")
|
|
313
|
+
|
|
314
|
+
def run_repl():
|
|
315
|
+
print("BrittainScript — type 'exit' to quit")
|
|
316
|
+
while True:
|
|
317
|
+
try:
|
|
318
|
+
text = input('bs> ')
|
|
319
|
+
except (EOFError, KeyboardInterrupt):
|
|
320
|
+
print()
|
|
321
|
+
break
|
|
322
|
+
if text.strip() in ('exit', 'quit'):
|
|
323
|
+
break
|
|
324
|
+
stripped_text = strip_inline_comment(text)
|
|
325
|
+
if not stripped_text:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
first_word = stripped_text.split()[0]
|
|
329
|
+
if first_word in ('cond', 'while', 'for', 'func'):
|
|
330
|
+
body = []
|
|
331
|
+
while True:
|
|
332
|
+
try:
|
|
333
|
+
line = input('...> ')
|
|
334
|
+
except (EOFError, KeyboardInterrupt):
|
|
335
|
+
break
|
|
336
|
+
if line.strip() == 'end':
|
|
337
|
+
break
|
|
338
|
+
body.append(line)
|
|
339
|
+
try:
|
|
340
|
+
execute_lines([text] + body + ['end'])
|
|
341
|
+
except BreakSignal:
|
|
342
|
+
print("Error: break used outside a loop")
|
|
343
|
+
except ContinueSignal:
|
|
344
|
+
print("Error: continue used outside a loop")
|
|
345
|
+
except ReturnSignal:
|
|
346
|
+
print("Error: return used outside a function")
|
|
347
|
+
else:
|
|
348
|
+
try:
|
|
349
|
+
execute_lines([stripped_text])
|
|
350
|
+
except BreakSignal:
|
|
351
|
+
print("Error: break used outside a loop")
|
|
352
|
+
except ContinueSignal:
|
|
353
|
+
print("Error: continue used outside a loop")
|
|
354
|
+
except ReturnSignal:
|
|
355
|
+
print("Error: return used outside a function")
|
|
356
|
+
|
|
357
|
+
def cli_entry():
|
|
358
|
+
if len(sys.argv) > 1:
|
|
359
|
+
run_file(sys.argv[1])
|
|
360
|
+
else:
|
|
361
|
+
run_repl()
|
|
362
|
+
|
|
363
|
+
if __name__ == '__main__':
|
|
364
|
+
cli_entry()
|