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.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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('')
|