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.
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()