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/subs.py ADDED
@@ -0,0 +1,261 @@
1
+ """QBASIC SUB/FUNCTION — proper subroutines with local scope."""
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_SUB, RE_END_SUB, RE_FUNCTION, RE_END_FUNCTION,
11
+ RE_CALL, RE_LOCAL, RE_STATIC_DECL, RE_SHARED,
12
+ )
13
+ from qbasic_core.exec_context import ExecContext
14
+
15
+
16
+ class SubroutineMixin:
17
+ """SUB/FUNCTION subroutines with LOCAL, STATIC, SHARED scoping.
18
+
19
+ Requires: TerminalProtocol — uses self.program, self.variables,
20
+ self._eval_with_vars(), self.eval_expr().
21
+ """
22
+
23
+ def _init_subs(self) -> None:
24
+ self._sub_defs: dict[str, dict[str, Any]] = {}
25
+ self._func_defs: dict[str, dict[str, Any]] = {}
26
+ self._scope_stack: list[dict[str, Any]] = []
27
+ self._static_vars: dict[str, dict[str, Any]] = {'_GLOBAL': {}}
28
+ self._call_stack: list[dict[str, Any]] = []
29
+
30
+ def _scan_subs(self, sorted_lines: list[int]) -> None:
31
+ """Scan program for SUB and FUNCTION blocks before execution."""
32
+ self._sub_defs.clear()
33
+ self._func_defs.clear()
34
+ for i, ln in enumerate(sorted_lines):
35
+ stmt = self.program[ln].strip()
36
+ m = RE_SUB.match(stmt)
37
+ if m:
38
+ name = m.group(1).upper()
39
+ params = [p.strip() for p in m.group(2).split(',')] if m.group(2) else []
40
+ end_ip = self._find_end_block(sorted_lines, i, 'SUB')
41
+ self._sub_defs[name] = {'params': params, 'start_ip': i + 1, 'end_ip': end_ip}
42
+ continue
43
+ m = RE_FUNCTION.match(stmt)
44
+ if m:
45
+ name = m.group(1).upper()
46
+ params = [p.strip() for p in m.group(2).split(',')] if m.group(2) else []
47
+ end_ip = self._find_end_block(sorted_lines, i, 'FUNCTION')
48
+ self._func_defs[name] = {'params': params, 'start_ip': i + 1, 'end_ip': end_ip}
49
+
50
+ def _find_end_block(self, sorted_lines: list[int], start_ip: int, kind: str) -> int:
51
+ scan = start_ip + 1
52
+ end_re = RE_END_SUB if kind == 'SUB' else RE_END_FUNCTION
53
+ while scan < len(sorted_lines):
54
+ s = self.program[sorted_lines[scan]].strip()
55
+ if end_re.match(s):
56
+ return scan
57
+ scan += 1
58
+ ln = sorted_lines[start_ip]
59
+ raise RuntimeError(f"{kind} at line {ln} has no END {kind}")
60
+
61
+ # ── Control flow handlers ──────────────────────────────────────────
62
+
63
+ def _cf_sub(self, stmt: str, sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
64
+ """SUB — skip the block during normal execution."""
65
+ if parsed is not None:
66
+ name = parsed.name
67
+ else:
68
+ m = RE_SUB.match(stmt)
69
+ if not m:
70
+ return None
71
+ name = m.group(1).upper()
72
+ if name in self._sub_defs:
73
+ return True, self._sub_defs[name]['end_ip'] + 1
74
+ return True, ExecResult.ADVANCE
75
+
76
+ def _cf_end_sub(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
77
+ if parsed is None and not RE_END_SUB.match(stmt):
78
+ return None
79
+ if self._call_stack:
80
+ frame = self._call_stack.pop()
81
+ self._pop_scope(frame)
82
+ return True, frame['return_ip']
83
+ return True, ExecResult.ADVANCE
84
+
85
+ def _cf_function(self, stmt: str, sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
86
+ """FUNCTION — skip the block during normal execution."""
87
+ if parsed is not None:
88
+ name = parsed.name
89
+ else:
90
+ m = RE_FUNCTION.match(stmt)
91
+ if not m:
92
+ return None
93
+ name = m.group(1).upper()
94
+ if name in self._func_defs:
95
+ return True, self._func_defs[name]['end_ip'] + 1
96
+ return True, ExecResult.ADVANCE
97
+
98
+ def _cf_end_function(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
99
+ if parsed is None and not RE_END_FUNCTION.match(stmt):
100
+ return None
101
+ if self._call_stack:
102
+ frame = self._call_stack.pop()
103
+ func_name = frame.get('func_name', '')
104
+ ret_val = self.variables.get(func_name, 0) if func_name else 0
105
+ self._pop_scope(frame)
106
+ if func_name:
107
+ self.variables['_FUNC_RETURN'] = ret_val
108
+ return True, frame['return_ip']
109
+ return True, ExecResult.ADVANCE
110
+
111
+ def _cf_call(self, stmt: str, run_vars: dict[str, Any],
112
+ sorted_lines: list[int], ip: int, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
113
+ if parsed is not None:
114
+ name = parsed.name
115
+ arg_str = parsed.args
116
+ else:
117
+ m = RE_CALL.match(stmt)
118
+ if not m:
119
+ return None
120
+ name = m.group(1).upper()
121
+ arg_str = m.group(2) or ''
122
+ args = [self._eval_with_vars(a.strip(), run_vars)
123
+ for a in arg_str.split(',') if a.strip()] if arg_str.strip() else []
124
+ if name not in self._sub_defs:
125
+ raise RuntimeError(f"UNDEFINED SUB: {name}")
126
+ sub = self._sub_defs[name]
127
+ self._push_scope()
128
+ # Bind parameters
129
+ for i, pname in enumerate(sub['params']):
130
+ val = args[i] if i < len(args) else 0
131
+ self.variables[pname] = val
132
+ # Restore STATIC vars
133
+ if name in self._static_vars:
134
+ for k, v in self._static_vars[name].items():
135
+ self.variables[k] = v
136
+ self._call_stack.append({'return_ip': ip + 1, 'sub_name': name})
137
+ return True, sub['start_ip']
138
+
139
+ def _invoke_function(self, name: str, args: list[float],
140
+ sorted_lines: list[int]) -> Any:
141
+ """Execute a FUNCTION block and return its value.
142
+ Called from the expression evaluator.
143
+ Uses _exec_control_flow for full statement support inside function bodies."""
144
+ uname = name.upper()
145
+ if uname not in self._func_defs:
146
+ raise ValueError(f"UNDEFINED FUNCTION: {name}")
147
+ func = self._func_defs[uname]
148
+ self._push_scope()
149
+ for i, pname in enumerate(func['params']):
150
+ self.variables[pname] = args[i] if i < len(args) else 0
151
+ self.variables[uname] = 0 # return variable
152
+ if uname in self._static_vars:
153
+ for k, v in self._static_vars[uname].items():
154
+ self.variables[k] = v
155
+ ctx = ExecContext(
156
+ sorted_lines=sorted_lines,
157
+ ip=func['start_ip'],
158
+ run_vars=dict(self.variables),
159
+ loop_stack=[],
160
+ max_iterations=self._max_iterations,
161
+ )
162
+ while ctx.ip <= func['end_ip']:
163
+ ctx.iteration_count += 1
164
+ if ctx.iteration_count > ctx.max_iterations:
165
+ raise RuntimeError("FUNCTION LOOP LIMIT")
166
+ stmt = self.program[sorted_lines[ctx.ip]].strip()
167
+ if RE_END_FUNCTION.match(stmt):
168
+ break
169
+ # Pre-parse once, pass to both the main call and the recurse callback
170
+ from qbasic_core.parser import parse_stmt
171
+ stmt_parsed = parse_stmt(stmt)
172
+ # Use full control-flow dispatch for all statement types
173
+ def _fn_recurse(s, ls, sl, i, rv):
174
+ handled, result = self._exec_control_flow(s, ls, sl, i, rv, _fn_recurse)
175
+ if handled:
176
+ return result
177
+ return None
178
+ handled, result = self._exec_control_flow(
179
+ stmt, ctx.loop_stack, sorted_lines, ctx.ip, ctx.run_vars, _fn_recurse,
180
+ parsed=stmt_parsed)
181
+ if handled:
182
+ if isinstance(result, int):
183
+ ctx.ip = result
184
+ continue
185
+ elif result is ExecResult.END:
186
+ break
187
+ ctx.ip += 1
188
+ ret_val = self.variables.get(uname, ctx.run_vars.get(uname, 0))
189
+ self._pop_scope()
190
+ return ret_val
191
+
192
+ # ── LOCAL / STATIC / SHARED ────────────────────────────────────────
193
+
194
+ def _cf_local(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
195
+ if parsed is not None:
196
+ var_list = parsed.var_list
197
+ else:
198
+ m = RE_LOCAL.match(stmt)
199
+ if not m:
200
+ return None
201
+ var_list = m.group(1)
202
+ for vname in var_list.split(','):
203
+ vname = vname.strip()
204
+ if vname:
205
+ run_vars[vname] = 0
206
+ self.variables[vname] = 0
207
+ return True, ExecResult.ADVANCE
208
+
209
+ def _cf_static(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
210
+ if parsed is not None:
211
+ var_list = parsed.var_list
212
+ else:
213
+ m = RE_STATIC_DECL.match(stmt)
214
+ if not m:
215
+ return None
216
+ var_list = m.group(1)
217
+ sub_name = self._call_stack[-1]['sub_name'] if self._call_stack else '_GLOBAL'
218
+ if sub_name not in self._static_vars:
219
+ self._static_vars[sub_name] = {}
220
+ for vname in var_list.split(','):
221
+ vname = vname.strip()
222
+ if vname:
223
+ val = self._static_vars[sub_name].get(vname, 0)
224
+ run_vars[vname] = val
225
+ self.variables[vname] = val
226
+ return True, ExecResult.ADVANCE
227
+
228
+ def _cf_shared(self, stmt: str, run_vars: dict[str, Any], *, parsed=None) -> tuple[bool, ExecOutcome] | None:
229
+ if parsed is not None:
230
+ var_list = parsed.var_list
231
+ else:
232
+ m = RE_SHARED.match(stmt)
233
+ if not m:
234
+ return None
235
+ var_list = m.group(1)
236
+ for vname in var_list.split(','):
237
+ vname = vname.strip()
238
+ if vname and self._scope_stack and vname in self._scope_stack[-1]:
239
+ run_vars[vname] = self._scope_stack[-1].get(vname, 0)
240
+ self.variables[vname] = run_vars[vname]
241
+ return True, ExecResult.ADVANCE
242
+
243
+ # ── Scope management ───────────────────────────────────────────────
244
+
245
+ def _push_scope(self) -> None:
246
+ self._scope_stack.append(dict(self.variables))
247
+
248
+ def _pop_scope(self, frame: dict[str, Any] | None = None) -> None:
249
+ # Save STATIC vars for the current frame before restoring outer scope.
250
+ # *frame* should be the already-popped call frame; if not provided,
251
+ # fall back to peeking at the call stack (legacy callers).
252
+ if frame is None and self._call_stack:
253
+ frame = self._call_stack[-1]
254
+ if frame is not None:
255
+ sub_name = frame.get('sub_name') or frame.get('func_name') or ''
256
+ if sub_name in self._static_vars:
257
+ for vname in list(self._static_vars[sub_name]):
258
+ self._static_vars[sub_name][vname] = self.variables.get(vname, 0)
259
+ if self._scope_stack:
260
+ self.variables.clear()
261
+ self.variables.update(self._scope_stack.pop())
qbasic_core/sweep.py ADDED
@@ -0,0 +1,82 @@
1
+ """QBASIC sweep mixin — parameter sweep command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from qiskit import transpile
6
+ from qiskit_aer import AerSimulator
7
+
8
+ try:
9
+ import plotille as _plotille
10
+ except ImportError:
11
+ _plotille = None
12
+
13
+
14
+ class SweepMixin:
15
+ """Parameter sweep command for QBasicTerminal.
16
+
17
+ Requires: TerminalProtocol — uses self.variables, self.shots,
18
+ self.sim_method, self.sim_device, self._noise_model,
19
+ self.eval_expr(), self.build_circuit().
20
+ """
21
+
22
+ def cmd_sweep(self, rest: str) -> None:
23
+ """SWEEP var start end [steps] — run circuit for each parameter value.
24
+
25
+ When plotille is available, appends a braille line chart of the
26
+ top-state probability vs. the swept variable.
27
+ """
28
+ parts = rest.split()
29
+ if len(parts) < 3:
30
+ self.io.writeln("?USAGE: SWEEP <var> <start> <end> [steps]")
31
+ return
32
+ var = parts[0]
33
+ start = self.eval_expr(parts[1])
34
+ end = self.eval_expr(parts[2])
35
+ steps = int(parts[3]) if len(parts) > 3 else 10
36
+ if steps < 1:
37
+ self.io.writeln("?SWEEP needs at least 1 step")
38
+ return
39
+
40
+ self.io.writeln(f"\nSWEEP {var} from {start} to {end} in {steps} steps:")
41
+ if steps == 1:
42
+ values = [start]
43
+ else:
44
+ values = [start + (end - start) * i / (steps - 1) for i in range(steps)]
45
+ backend_opts = {'method': self.sim_method}
46
+ if self.sim_device == 'GPU':
47
+ backend_opts['device'] = 'GPU'
48
+ if self._noise_model:
49
+ backend_opts['noise_model'] = self._noise_model
50
+ backend = AerSimulator(**backend_opts)
51
+ sweep_xs: list[float] = []
52
+ sweep_ys: list[float] = []
53
+ for val in values:
54
+ self.variables[var] = val
55
+ try:
56
+ qc, has_measure = self.build_circuit()
57
+ if has_measure:
58
+ qc.measure_all()
59
+ result = backend.run(transpile(qc, backend), shots=self.shots).result()
60
+ counts = dict(result.get_counts())
61
+ ranked = sorted(counts.items(), key=lambda x: -x[1])
62
+ top = ranked[0]
63
+ bar_len = int(30 * top[1] / self.shots)
64
+ top2 = f" |{ranked[1][0]}\u27E9={ranked[1][1]}" if len(ranked) > 1 else ""
65
+ n_unique = len(ranked)
66
+ self.io.writeln(f" {var}={val:8.4f} |{top[0]}\u27E9 {top[1]:>5}/{self.shots} "
67
+ f"{'\u2588' * bar_len}{top2} ({n_unique} states)")
68
+ sweep_xs.append(val)
69
+ sweep_ys.append(top[1] / self.shots)
70
+ except Exception as e:
71
+ self.io.writeln(f" {var}={val:8.4f} ERROR: {e}")
72
+
73
+ # Plotille chart of P(top state) vs variable
74
+ if _plotille is not None and len(sweep_xs) >= 2:
75
+ self.io.writeln('')
76
+ self.io.writeln(_plotille.plot(
77
+ sweep_xs, sweep_ys,
78
+ width=60, height=15,
79
+ X_label=var,
80
+ Y_label='P(top)',
81
+ lc='cyan'))
82
+ self.io.writeln('')