qubasic 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.
Files changed (48) hide show
  1. qbasic.py +113 -0
  2. qbasic_core/__init__.py +80 -0
  3. qbasic_core/__main__.py +6 -0
  4. qbasic_core/analysis.py +179 -0
  5. qbasic_core/backend.py +76 -0
  6. qbasic_core/classic.py +419 -0
  7. qbasic_core/control_flow.py +576 -0
  8. qbasic_core/debug.py +327 -0
  9. qbasic_core/demos.py +356 -0
  10. qbasic_core/display.py +274 -0
  11. qbasic_core/engine.py +126 -0
  12. qbasic_core/engine_state.py +109 -0
  13. qbasic_core/errors.py +37 -0
  14. qbasic_core/exec_context.py +24 -0
  15. qbasic_core/executor.py +861 -0
  16. qbasic_core/expression.py +228 -0
  17. qbasic_core/file_io.py +457 -0
  18. qbasic_core/gates.py +284 -0
  19. qbasic_core/help_text.py +167 -0
  20. qbasic_core/io_protocol.py +33 -0
  21. qbasic_core/locc.py +10 -0
  22. qbasic_core/locc_commands.py +221 -0
  23. qbasic_core/locc_display.py +61 -0
  24. qbasic_core/locc_engine.py +195 -0
  25. qbasic_core/locc_execution.py +389 -0
  26. qbasic_core/memory.py +369 -0
  27. qbasic_core/mock_backend.py +66 -0
  28. qbasic_core/noise_mixin.py +96 -0
  29. qbasic_core/parser.py +564 -0
  30. qbasic_core/patterns.py +186 -0
  31. qbasic_core/profiler.py +156 -0
  32. qbasic_core/program_mgmt.py +369 -0
  33. qbasic_core/protocol.py +77 -0
  34. qbasic_core/py.typed +0 -0
  35. qbasic_core/scope.py +74 -0
  36. qbasic_core/screen.py +115 -0
  37. qbasic_core/state_display.py +60 -0
  38. qbasic_core/statements.py +387 -0
  39. qbasic_core/strings.py +107 -0
  40. qbasic_core/subs.py +261 -0
  41. qbasic_core/sweep.py +82 -0
  42. qbasic_core/terminal.py +1697 -0
  43. qubasic-0.1.0.dist-info/METADATA +736 -0
  44. qubasic-0.1.0.dist-info/RECORD +48 -0
  45. qubasic-0.1.0.dist-info/WHEEL +5 -0
  46. qubasic-0.1.0.dist-info/entry_points.txt +2 -0
  47. qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. qubasic-0.1.0.dist-info/top_level.txt +2 -0
qbasic_core/classic.py ADDED
@@ -0,0 +1,419 @@
1
+ """QBASIC classic features — DATA/READ, ON GOTO, SELECT CASE, DO/LOOP, EXIT, SWAP, DEF FN, OPTION BASE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from qbasic_core.engine import (
9
+ ExecResult, ExecOutcome,
10
+ RE_DATA, RE_READ, RE_ON_GOTO, RE_ON_GOSUB,
11
+ RE_SELECT_CASE, RE_CASE,
12
+ RE_DO, RE_LOOP_STMT, RE_EXIT,
13
+ RE_SWAP, RE_DEF_FN, RE_OPTION_BASE,
14
+ )
15
+
16
+
17
+ class ClassicMixin:
18
+ """Classic BASIC features for QBasicTerminal.
19
+
20
+ Requires: TerminalProtocol — uses self.program, self.variables,
21
+ self.arrays, self._gosub_stack, self._eval_with_vars(),
22
+ self._eval_condition(), self.eval_expr().
23
+ """
24
+
25
+ def _init_classic(self) -> None:
26
+ self._data_pool: list[Any] = []
27
+ self._data_ptr: int = 0
28
+ self._data_cache_key: tuple | None = None
29
+ self._option_base: int = 0
30
+ self._user_fns: dict[str, dict[str, Any]] = {}
31
+ self._select_stack: list[Any] = []
32
+
33
+ # Quantum state names recognized in DATA statements
34
+ _QUANTUM_STATES = {
35
+ '|0>': '0', '|1>': '1', '|+>': '+', '|->': '-',
36
+ '|0⟩': '0', '|1⟩': '1', '|+⟩': '+', '|-⟩': '-',
37
+ '|BELL>': 'BELL', '|GHZ>': 'GHZ', '|GHZ3>': 'GHZ3', '|GHZ4>': 'GHZ4',
38
+ '|W>': 'W', '|W3>': 'W3',
39
+ }
40
+
41
+ def _collect_data(self) -> None:
42
+ """Scan program for DATA statements and build the data pool.
43
+
44
+ Recognizes quantum state names like |+>, |0>, |GHZ3> as special
45
+ string tokens that can trigger state preparation when READ assigns
46
+ them to a variable.
47
+
48
+ Caches the result keyed on DATA-containing lines so repeated RUN
49
+ calls skip the rescan when the program hasn't changed.
50
+ """
51
+ # Build cache key from all DATA-containing lines
52
+ data_lines = []
53
+ for ln in sorted(self.program.keys()):
54
+ text = self.program[ln].strip()
55
+ if RE_DATA.match(text):
56
+ data_lines.append((ln, text))
57
+ cache_key = tuple(data_lines)
58
+ if cache_key == self._data_cache_key:
59
+ # Program DATA lines unchanged — just reset the read pointer
60
+ self._data_ptr = 0
61
+ return
62
+ self._data_pool = []
63
+ self._data_ptr = 0
64
+ for _ln, text in data_lines:
65
+ m = RE_DATA.match(text)
66
+ if m:
67
+ for item in m.group(1).split(','):
68
+ item = item.strip()
69
+ if (item.startswith('"') and item.endswith('"')) or \
70
+ (item.startswith("'") and item.endswith("'")):
71
+ self._data_pool.append(item[1:-1])
72
+ elif item in self._QUANTUM_STATES:
73
+ self._data_pool.append(f"QSTATE:{self._QUANTUM_STATES[item]}")
74
+ else:
75
+ try:
76
+ self._data_pool.append(float(item) if '.' in item else int(item))
77
+ except ValueError:
78
+ self._data_pool.append(item)
79
+ self._data_cache_key = cache_key
80
+
81
+ # ── DATA / READ / RESTORE ──────────────────────────────────────────
82
+
83
+ def _cf_data(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
84
+ """DATA — skip during execution (collected before run)."""
85
+ if parsed is not None or RE_DATA.match(stmt):
86
+ return True, ExecResult.ADVANCE
87
+ return None
88
+
89
+ def _cf_read(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
90
+ if parsed is not None:
91
+ var_names = [v.strip() for v in parsed.var_list.split(',')]
92
+ else:
93
+ m = RE_READ.match(stmt)
94
+ if not m:
95
+ return None
96
+ var_names = [v.strip() for v in m.group(1).split(',')]
97
+ for vname in var_names:
98
+ if self._data_ptr >= len(self._data_pool):
99
+ raise RuntimeError("READ: OUT OF DATA")
100
+ val = self._data_pool[self._data_ptr]
101
+ self._data_ptr += 1
102
+ run_vars[vname] = val
103
+ self.variables[vname] = val
104
+ return True, ExecResult.ADVANCE
105
+
106
+ def cmd_restore(self, rest: str = '') -> None:
107
+ """RESTORE — reset DATA pointer to beginning."""
108
+ self._data_ptr = 0
109
+
110
+ # ── ON expr GOTO / GOSUB ──────────────────────────────────────────
111
+
112
+ def _cf_on_goto(self, stmt: str, run_vars: dict[str, Any],
113
+ sorted_lines: list[int], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
114
+ if parsed is not None:
115
+ idx = int(self._eval_with_vars(parsed.expr, run_vars))
116
+ targets = list(parsed.targets)
117
+ else:
118
+ m = RE_ON_GOTO.match(stmt)
119
+ if not m:
120
+ return None
121
+ idx = int(self._eval_with_vars(m.group(1).strip(), run_vars))
122
+ targets = [int(t.strip()) for t in m.group(2).split(',') if t.strip()]
123
+ if 1 <= idx <= len(targets):
124
+ target = targets[idx - 1]
125
+ for i, ln in enumerate(sorted_lines):
126
+ if ln == target:
127
+ return True, i
128
+ raise RuntimeError(f"ON GOTO: LINE {target} NOT FOUND")
129
+ return True, ExecResult.ADVANCE # out of range: fall through
130
+
131
+ def _cf_on_gosub(self, stmt: str, run_vars: dict[str, Any],
132
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
133
+ if parsed is not None:
134
+ idx = int(self._eval_with_vars(parsed.expr, run_vars))
135
+ targets = list(parsed.targets)
136
+ else:
137
+ m = RE_ON_GOSUB.match(stmt)
138
+ if not m:
139
+ return None
140
+ idx = int(self._eval_with_vars(m.group(1).strip(), run_vars))
141
+ targets = [int(t.strip()) for t in m.group(2).split(',') if t.strip()]
142
+ if 1 <= idx <= len(targets):
143
+ target = targets[idx - 1]
144
+ self._gosub_stack.append(ip + 1)
145
+ for i, ln in enumerate(sorted_lines):
146
+ if ln == target:
147
+ return True, i
148
+ raise RuntimeError(f"ON GOSUB: LINE {target} NOT FOUND")
149
+ return True, ExecResult.ADVANCE
150
+
151
+ # ── SELECT CASE ───────────────────────────────────────────────────
152
+
153
+ def _cf_select_case(self, stmt: str, run_vars: dict[str, Any],
154
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
155
+ if parsed is not None:
156
+ test_val = self._eval_with_vars(parsed.expr, run_vars)
157
+ else:
158
+ m = RE_SELECT_CASE.match(stmt)
159
+ if not m:
160
+ return None
161
+ test_val = self._eval_with_vars(m.group(1).strip(), run_vars)
162
+ self._select_stack.append(test_val)
163
+ # Scan forward for matching CASE
164
+ scan = ip + 1
165
+ depth = 1
166
+ while scan < len(sorted_lines):
167
+ s = self.program[sorted_lines[scan]].strip().upper()
168
+ if s.startswith('SELECT CASE'):
169
+ depth += 1
170
+ elif s == 'END SELECT':
171
+ depth -= 1
172
+ if depth == 0:
173
+ self._select_stack.pop()
174
+ return True, scan + 1 # no matching case
175
+ elif depth == 1 and s.startswith('CASE '):
176
+ case_val = s[5:].strip()
177
+ if case_val == 'ELSE':
178
+ return True, scan + 1 # execute CASE ELSE block
179
+ try:
180
+ if float(test_val) == float(self._eval_with_vars(case_val, run_vars)):
181
+ return True, scan + 1 # execute this CASE block
182
+ except (ValueError, TypeError):
183
+ if str(test_val) == case_val:
184
+ return True, scan + 1
185
+ scan += 1
186
+ raise RuntimeError("SELECT CASE without END SELECT")
187
+
188
+ def _cf_case(self, stmt: str, sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
189
+ if parsed is None:
190
+ m = RE_CASE.match(stmt)
191
+ if not m:
192
+ return None
193
+ # If we reach a CASE during execution, skip to END SELECT (we already matched)
194
+ depth = 1
195
+ scan = ip + 1
196
+ while scan < len(sorted_lines):
197
+ s = self.program[sorted_lines[scan]].strip().upper()
198
+ if s.startswith('SELECT CASE'):
199
+ depth += 1
200
+ elif s == 'END SELECT':
201
+ depth -= 1
202
+ if depth == 0:
203
+ if self._select_stack:
204
+ self._select_stack.pop()
205
+ return True, scan + 1
206
+ scan += 1
207
+ return True, ExecResult.ADVANCE
208
+
209
+ def _cf_end_select(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
210
+ if parsed is not None or stmt.strip().upper() == 'END SELECT':
211
+ if not self._select_stack:
212
+ raise RuntimeError("END SELECT without matching SELECT CASE")
213
+ self._select_stack.pop()
214
+ return True, ExecResult.ADVANCE
215
+ return None
216
+
217
+ # ── DO / LOOP ─────────────────────────────────────────────────────
218
+
219
+ def _find_matching_loop(self, sorted_lines: list[int], ip: int) -> int:
220
+ depth = 1
221
+ scan = ip + 1
222
+ while scan < len(sorted_lines):
223
+ s = self.program[sorted_lines[scan]].strip().upper()
224
+ if s.startswith('DO'):
225
+ depth += 1
226
+ elif s.startswith('LOOP'):
227
+ depth -= 1
228
+ if depth == 0:
229
+ return scan
230
+ scan += 1
231
+ raise RuntimeError(f"DO at line {sorted_lines[ip]} has no matching LOOP")
232
+
233
+ def _cf_do(self, stmt: str, run_vars: dict[str, Any],
234
+ loop_stack: list[dict[str, Any]],
235
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
236
+ if parsed is not None:
237
+ kind = parsed.kind
238
+ cond = parsed.condition
239
+ else:
240
+ m = RE_DO.match(stmt)
241
+ if not m:
242
+ return None
243
+ kind = m.group(1) # WHILE or UNTIL or None
244
+ cond = m.group(2) # condition or None
245
+ if kind and cond:
246
+ # Pre-test
247
+ result = self._eval_condition(cond, run_vars)
248
+ if kind.upper() == 'UNTIL':
249
+ result = not result
250
+ if not result:
251
+ # Skip to after LOOP
252
+ loop_ip = self._find_matching_loop(sorted_lines, ip)
253
+ return True, loop_ip + 1
254
+ loop_stack.append({'type': 'do', 'return_ip': ip, 'kind': kind, 'cond': cond})
255
+ return True, ExecResult.ADVANCE
256
+
257
+ def _cf_loop(self, stmt: str, run_vars: dict[str, Any],
258
+ loop_stack: list[dict[str, Any]],
259
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
260
+ if parsed is not None:
261
+ loop_kind = parsed.kind
262
+ loop_cond = parsed.condition
263
+ else:
264
+ m = RE_LOOP_STMT.match(stmt)
265
+ if not m:
266
+ return None
267
+ loop_kind = m.group(1)
268
+ loop_cond = m.group(2)
269
+ # Only handle as DO/LOOP keyword if we're inside a DO block
270
+ if not loop_stack or loop_stack[-1].get('type') != 'do':
271
+ # Bare "LOOP" (no WHILE/UNTIL) that matches a SUB name should
272
+ # be handed off to subroutine expansion, not treated as a keyword.
273
+ is_bare = loop_kind is None # no WHILE/UNTIL suffix
274
+ if is_bare and hasattr(self, '_sub_defs') and 'LOOP' in self._sub_defs:
275
+ return None # let subroutine expansion handle it
276
+ # Otherwise it's either bare LOOP with no sub, or LOOP WHILE/UNTIL
277
+ # without a DO — both are errors or no-ops. Return None so the
278
+ # caller can decide (gate dispatch will surface an error if needed).
279
+ return None
280
+ loop = loop_stack[-1]
281
+ # Post-test condition on LOOP
282
+ kind = loop_kind
283
+ cond = loop_cond
284
+ if kind and cond:
285
+ result = self._eval_condition(cond, run_vars)
286
+ if kind.upper() == 'UNTIL':
287
+ result = not result
288
+ if result:
289
+ return True, loop['return_ip']
290
+ else:
291
+ loop_stack.pop()
292
+ return True, ExecResult.ADVANCE
293
+ # Pre-test condition was on DO
294
+ if loop.get('kind') and loop.get('cond'):
295
+ result = self._eval_condition(loop['cond'], run_vars)
296
+ if loop['kind'].upper() == 'UNTIL':
297
+ result = not result
298
+ if result:
299
+ return True, loop['return_ip']
300
+ else:
301
+ loop_stack.pop()
302
+ return True, ExecResult.ADVANCE
303
+ # Infinite DO/LOOP (no condition) — loop forever
304
+ return True, loop['return_ip']
305
+
306
+ # ── EXIT ──────────────────────────────────────────────────────────
307
+
308
+ def _cf_exit(self, stmt: str, loop_stack: list[dict[str, Any]],
309
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
310
+ if parsed is not None:
311
+ kind = parsed.target
312
+ else:
313
+ m = RE_EXIT.match(stmt)
314
+ if not m:
315
+ return None
316
+ kind = m.group(1).upper()
317
+ if kind == 'FOR':
318
+ # Find matching NEXT
319
+ scan = ip + 1
320
+ depth = 1
321
+ while scan < len(sorted_lines):
322
+ s = self.program[sorted_lines[scan]].strip().upper()
323
+ if s.startswith('FOR '):
324
+ depth += 1
325
+ elif s.startswith('NEXT '):
326
+ depth -= 1
327
+ if depth == 0:
328
+ if loop_stack and loop_stack[-1].get('var'):
329
+ loop_stack.pop()
330
+ return True, scan + 1
331
+ scan += 1
332
+ elif kind == 'WHILE':
333
+ scan = ip + 1
334
+ depth = 1
335
+ while scan < len(sorted_lines):
336
+ s = self.program[sorted_lines[scan]].strip().upper()
337
+ if s.startswith('WHILE '):
338
+ depth += 1
339
+ elif s == 'WEND':
340
+ depth -= 1
341
+ if depth == 0:
342
+ if loop_stack and loop_stack[-1].get('type') == 'while':
343
+ loop_stack.pop()
344
+ return True, scan + 1
345
+ scan += 1
346
+ elif kind == 'DO':
347
+ scan = ip + 1
348
+ depth = 1
349
+ while scan < len(sorted_lines):
350
+ s = self.program[sorted_lines[scan]].strip().upper()
351
+ if s.startswith('DO'):
352
+ depth += 1
353
+ elif s.startswith('LOOP'):
354
+ depth -= 1
355
+ if depth == 0:
356
+ if loop_stack and loop_stack[-1].get('type') == 'do':
357
+ loop_stack.pop()
358
+ return True, scan + 1
359
+ scan += 1
360
+ elif kind in ('SUB', 'FUNCTION'):
361
+ return True, ExecResult.END
362
+ raise RuntimeError(f"EXIT {kind}: no matching block")
363
+
364
+ # ── SWAP ──────────────────────────────────────────────────────────
365
+
366
+ def _cf_swap(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
367
+ if parsed is not None:
368
+ a, b = parsed.a, parsed.b
369
+ else:
370
+ m = RE_SWAP.match(stmt)
371
+ if not m:
372
+ return None
373
+ a, b = m.group(1), m.group(2)
374
+ va = run_vars.get(a, self.variables.get(a, 0))
375
+ vb = run_vars.get(b, self.variables.get(b, 0))
376
+ run_vars[a] = vb
377
+ run_vars[b] = va
378
+ self.variables[a] = vb
379
+ self.variables[b] = va
380
+ return True, ExecResult.ADVANCE
381
+
382
+ # ── DEF FN ────────────────────────────────────────────────────────
383
+
384
+ def _cf_def_fn(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
385
+ if parsed is not None:
386
+ name = 'FN' + parsed.name.upper()
387
+ params = [p.strip() for p in parsed.params.split(',') if p.strip()]
388
+ body = parsed.body.strip()
389
+ else:
390
+ m = RE_DEF_FN.match(stmt)
391
+ if not m:
392
+ return None
393
+ name = 'FN' + m.group(1).upper()
394
+ params = [p.strip() for p in m.group(2).split(',') if p.strip()]
395
+ body = m.group(3).strip()
396
+ self._user_fns[name] = {'params': params, 'body': body}
397
+ return True, ExecResult.ADVANCE
398
+
399
+ def _call_user_fn(self, name: str, args: list[float]) -> float:
400
+ """Call a DEF FN function."""
401
+ fn = self._user_fns.get(name.upper())
402
+ if fn is None:
403
+ raise ValueError(f"UNDEFINED FUNCTION: {name}")
404
+ ns = {}
405
+ for i, pname in enumerate(fn['params']):
406
+ ns[pname] = args[i] if i < len(args) else 0
407
+ return float(self._safe_eval(fn['body'], extra_ns=ns))
408
+
409
+ # ── OPTION BASE ───────────────────────────────────────────────────
410
+
411
+ def _cf_option_base(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
412
+ if parsed is not None:
413
+ self._option_base = parsed.base
414
+ else:
415
+ m = RE_OPTION_BASE.match(stmt)
416
+ if not m:
417
+ return None
418
+ self._option_base = int(m.group(1))
419
+ return True, ExecResult.ADVANCE