qubasic 0.11.1__tar.gz → 0.13.0__tar.gz
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.
- {qubasic-0.11.1 → qubasic-0.13.0}/CHANGELOG.md +35 -0
- {qubasic-0.11.1/qubasic.egg-info → qubasic-0.13.0}/PKG-INFO +7 -2
- {qubasic-0.11.1 → qubasic-0.13.0}/README.md +6 -1
- {qubasic-0.11.1 → qubasic-0.13.0}/pyproject.toml +1 -1
- {qubasic-0.11.1 → qubasic-0.13.0/qubasic.egg-info}/PKG-INFO +7 -2
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/analysis.py +22 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/cli.py +43 -6
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/control_flow.py +22 -3
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/engine_state.py +7 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/executor.py +3 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/expression.py +25 -11
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/help_text.py +5 -4
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/patterns.py +2 -2
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/strings.py +4 -1
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/subs.py +14 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/terminal.py +80 -17
- {qubasic-0.11.1 → qubasic-0.13.0}/tests/test_features.py +22 -15
- {qubasic-0.11.1 → qubasic-0.13.0}/tests/test_qubasic.py +108 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/LICENSE +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/MANIFEST.in +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/examples/bell.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/examples/grover3.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/algos2.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/demos.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/memory.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qec.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qudits.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/resources.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.11.1 → qubasic-0.13.0}/setup.cfg +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Closes the remaining audit gaps in the classic-BASIC layer (conventions,
|
|
6
|
+
inspection, and an agent contract). The quantum engine is unchanged.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `qubasic --spec` emits a machine-readable JSON contract (version, commands with arg flag and one-line help, gates, functions, constants, bit order) so an agent can load the exact surface of the installed version.
|
|
10
|
+
- `REDIM PRESERVE name(n)` keeps existing elements; plain `REDIM` clears to zeros (QBASIC semantics).
|
|
11
|
+
- `DENSITY` shows the density matrix after a measured `SET_DENSITY` run, not only a no-MEASURE one.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `DIM` uses inclusive sizing: `DIM a(n)` spans indices base..n (the declared top index is valid), matching QBASIC instead of the previous C-style element count.
|
|
15
|
+
- `round()` rounds half away from zero (`round(2.5)` = 3), not Python's banker's rounding.
|
|
16
|
+
- `STR$(n)` reserves a leading space for non-negative numbers (`STR$(42)` = " 42").
|
|
17
|
+
- A chained comparison such as `a < b < c` now raises a clear "ambiguous" error instead of silently using Python chaining; write `(a < b) AND (b < c)`.
|
|
18
|
+
- `PRINT`ing a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) shows "mid-circuit bit, resolved per shot" rather than the placeholder 0.
|
|
19
|
+
|
|
20
|
+
## 0.12.0 (2026-06-19)
|
|
21
|
+
|
|
22
|
+
Correctness and robustness pass on the classic-BASIC layer, from an extended
|
|
23
|
+
adversarial audit. The quantum engine is unchanged.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `SHARED` variables in a `SUB` now propagate their modifications back to the caller instead of being discarded when the call's scope is restored (an accumulator across two calls now sums correctly: `8`, not `3`).
|
|
27
|
+
- `RESTORE` resets the `DATA` read pointer inside a program; it was a no-op, so a `READ` after `RESTORE` raised `OUT OF DATA`.
|
|
28
|
+
- Writes to an explicitly `DIM`med array are bounds-checked like reads instead of silently auto-extending past the declared size. Implicit (undimensioned) arrays still grow on first assignment.
|
|
29
|
+
- String arrays work: `LET s$(i) = ...` stores and reads string elements.
|
|
30
|
+
- `SET_DENSITY` followed by inspection without a `MEASURE` no longer raises a raw "unable to translate" error from Aer; the density matrix is captured and shown by `DENSITY`.
|
|
31
|
+
- A script that issues an explicit `RUN` and also contains a `MEASURE` no longer auto-runs (and prints) the program a second time.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `STATUS` reports the user-variable count only, excluding the internal `_DEPTH`/`_GATES`/`_TIME` set after a run.
|
|
35
|
+
- `DEF FN name(x) = expr` now works at the prompt (immediate mode), matching the in-program form.
|
|
36
|
+
- `HELP` refreshed (`STATUS`, `INT` floors / `FIX` truncates, implicit `LET`, `<>`/`XOR`, corrected `MEAS`); `--help` notes the `-q`/`-v`/`-h` short flags and `python -m qubasic_core`.
|
|
37
|
+
|
|
3
38
|
## 0.11.1 (2026-06-19)
|
|
4
39
|
|
|
5
40
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -78,6 +78,7 @@ python -m qubasic_core Same, without installing
|
|
|
78
78
|
qubasic script.qb Run a script file
|
|
79
79
|
qubasic --quiet script Suppress banner, output results only
|
|
80
80
|
qubasic --json script Machine-readable JSON output
|
|
81
|
+
qubasic --spec Print a JSON contract (commands, gates, functions)
|
|
81
82
|
qubasic --help Show CLI help
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -255,11 +256,15 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
255
256
|
DIM data(10) 1D array
|
|
256
257
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
257
258
|
LET data(0) = PI
|
|
258
|
-
|
|
259
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
260
|
+
REDIM data(20) Resize, clearing to zeros
|
|
261
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
259
262
|
ERASE data Delete array
|
|
260
263
|
OPTION BASE 1 Set array index base
|
|
261
264
|
```
|
|
262
265
|
|
|
266
|
+
`DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
267
|
+
|
|
263
268
|
## Control flow
|
|
264
269
|
|
|
265
270
|
```
|
|
@@ -45,6 +45,7 @@ python -m qubasic_core Same, without installing
|
|
|
45
45
|
qubasic script.qb Run a script file
|
|
46
46
|
qubasic --quiet script Suppress banner, output results only
|
|
47
47
|
qubasic --json script Machine-readable JSON output
|
|
48
|
+
qubasic --spec Print a JSON contract (commands, gates, functions)
|
|
48
49
|
qubasic --help Show CLI help
|
|
49
50
|
```
|
|
50
51
|
|
|
@@ -222,11 +223,15 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
222
223
|
DIM data(10) 1D array
|
|
223
224
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
224
225
|
LET data(0) = PI
|
|
225
|
-
|
|
226
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
227
|
+
REDIM data(20) Resize, clearing to zeros
|
|
228
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
226
229
|
ERASE data Delete array
|
|
227
230
|
OPTION BASE 1 Set array index base
|
|
228
231
|
```
|
|
229
232
|
|
|
233
|
+
`DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
234
|
+
|
|
230
235
|
## Control flow
|
|
231
236
|
|
|
232
237
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -78,6 +78,7 @@ python -m qubasic_core Same, without installing
|
|
|
78
78
|
qubasic script.qb Run a script file
|
|
79
79
|
qubasic --quiet script Suppress banner, output results only
|
|
80
80
|
qubasic --json script Machine-readable JSON output
|
|
81
|
+
qubasic --spec Print a JSON contract (commands, gates, functions)
|
|
81
82
|
qubasic --help Show CLI help
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -255,11 +256,15 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
255
256
|
DIM data(10) 1D array
|
|
256
257
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
257
258
|
LET data(0) = PI
|
|
258
|
-
|
|
259
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
260
|
+
REDIM data(20) Resize, clearing to zeros
|
|
261
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
259
262
|
ERASE data Delete array
|
|
260
263
|
OPTION BASE 1 Set array index base
|
|
261
264
|
```
|
|
262
265
|
|
|
266
|
+
`DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
267
|
+
|
|
263
268
|
## Control flow
|
|
264
269
|
|
|
265
270
|
```
|
|
@@ -109,6 +109,28 @@ class AnalysisMixin:
|
|
|
109
109
|
|
|
110
110
|
def cmd_density(self, rest: str = '') -> None:
|
|
111
111
|
"""Show density matrix (DENSITY [reg]); summarizes for large systems."""
|
|
112
|
+
# A mixed state set via SET_DENSITY has a stored density matrix and no
|
|
113
|
+
# statevector; display it directly.
|
|
114
|
+
dm = getattr(self, '_last_density', None)
|
|
115
|
+
if dm is not None and not rest.strip():
|
|
116
|
+
rho = np.ascontiguousarray(dm)
|
|
117
|
+
dim = rho.shape[0]
|
|
118
|
+
self.io.writeln(f"\n Density matrix ({dim}x{dim}):\n")
|
|
119
|
+
if dim <= 16:
|
|
120
|
+
for i in range(dim):
|
|
121
|
+
row = []
|
|
122
|
+
for j in range(dim):
|
|
123
|
+
v = complex(rho[i, j])
|
|
124
|
+
if abs(v.imag) < 1e-6:
|
|
125
|
+
row.append(f"{v.real:7.3f}")
|
|
126
|
+
else:
|
|
127
|
+
row.append(f"{v.real:+.2f}{v.imag:+.2f}j")
|
|
128
|
+
self.io.writeln(f" {' '.join(row)}")
|
|
129
|
+
else:
|
|
130
|
+
self.io.writeln(f" ({dim}x{dim}, too large to display)")
|
|
131
|
+
self.io.writeln(f"\n Purity: {float(np.real(np.trace(rho @ rho))):.6f}")
|
|
132
|
+
self.io.writeln('')
|
|
133
|
+
return
|
|
112
134
|
sv, n, rest = self._resolve_analysis_target(rest)
|
|
113
135
|
if sv is None:
|
|
114
136
|
if self.locc_mode and self.locc and not self.locc.joint:
|
|
@@ -38,8 +38,12 @@ def run_script(path: str, terminal: 'QBasicTerminal') -> None:
|
|
|
38
38
|
ProgramMgmtMixin._load_lines_with_defs(
|
|
39
39
|
lines, lambda line: terminal.process(line, track_undo=False))
|
|
40
40
|
|
|
41
|
-
# Auto-run if the program has a reachable MEASURE (incl. in subs / IF
|
|
42
|
-
|
|
41
|
+
# Auto-run if the program has a reachable MEASURE (incl. in subs / IF
|
|
42
|
+
# clauses), unless the script already issued an explicit RUN (which would
|
|
43
|
+
# otherwise execute and print the program twice).
|
|
44
|
+
explicit_run = any(l.strip().upper() == 'RUN' for l in lines)
|
|
45
|
+
if (terminal.program and not explicit_run
|
|
46
|
+
and terminal._program_has_measure(sorted(terminal.program))):
|
|
43
47
|
terminal.cmd_run()
|
|
44
48
|
|
|
45
49
|
|
|
@@ -53,8 +57,9 @@ def main():
|
|
|
53
57
|
quiet = '--quiet' in args or '-q' in args
|
|
54
58
|
json_mode = '--json' in args
|
|
55
59
|
agent_mode = '--agent' in args
|
|
60
|
+
spec_mode = '--spec' in args
|
|
56
61
|
seed_val = None
|
|
57
|
-
for flag in ('--quiet', '-q', '--json', '--agent'):
|
|
62
|
+
for flag in ('--quiet', '-q', '--json', '--agent', '--spec'):
|
|
58
63
|
args = [a for a in args if a != flag]
|
|
59
64
|
# Parse --seed N
|
|
60
65
|
filtered = []
|
|
@@ -78,16 +83,48 @@ def main():
|
|
|
78
83
|
print("Usage:")
|
|
79
84
|
print(" qubasic Interactive REPL")
|
|
80
85
|
print(" qubasic script.qb Run a script file")
|
|
81
|
-
print(" qubasic --quiet script Suppress banner and progress")
|
|
86
|
+
print(" qubasic --quiet script Suppress banner and progress (also -q)")
|
|
82
87
|
print(" qubasic --json script Output results as JSON")
|
|
83
88
|
print(" qubasic --agent script Confine file writes to the working dir")
|
|
84
89
|
print(" qubasic --seed N script Set random seed for reproducibility")
|
|
85
|
-
print(" qubasic --version Show version")
|
|
86
|
-
print(" qubasic --help Show this help")
|
|
90
|
+
print(" qubasic --version Show version (also -v)")
|
|
91
|
+
print(" qubasic --help Show this help (also -h)")
|
|
92
|
+
print(" python -m qubasic_core Run without the installed console script")
|
|
87
93
|
print()
|
|
88
94
|
print("Type HELP inside the REPL for full command reference.")
|
|
89
95
|
sys.exit(0)
|
|
90
96
|
|
|
97
|
+
if spec_mode:
|
|
98
|
+
# Machine-readable contract for agents: commands, gates, functions,
|
|
99
|
+
# constants, and conventions for the installed version.
|
|
100
|
+
from qubasic_core.engine import GATE_TABLE
|
|
101
|
+
from qubasic_core.expression import ExpressionMixin
|
|
102
|
+
|
|
103
|
+
def _help1(mname):
|
|
104
|
+
doc = (getattr(getattr(QBasicTerminal, mname, None), '__doc__', '') or '').strip()
|
|
105
|
+
return doc.split('\n')[0].strip()
|
|
106
|
+
|
|
107
|
+
cmds = []
|
|
108
|
+
for tbl, takes in ((QBasicTerminal._CMD_WITH_ARG, True),
|
|
109
|
+
(QBasicTerminal._CMD_NO_ARG, False)):
|
|
110
|
+
for cname, mname in tbl.items():
|
|
111
|
+
cmds.append({'name': cname, 'takes_arg': takes, 'help': _help1(mname)})
|
|
112
|
+
spec = {
|
|
113
|
+
'name': 'qubasic',
|
|
114
|
+
'version': __version__,
|
|
115
|
+
'bit_order': 'little-endian (qubit 0 = rightmost bit)',
|
|
116
|
+
'commands': sorted(cmds, key=lambda c: c['name']),
|
|
117
|
+
'gates': sorted(GATE_TABLE.keys()),
|
|
118
|
+
'functions': sorted(
|
|
119
|
+
set(ExpressionMixin._SAFE_FUNCS)
|
|
120
|
+
| {'RND', 'TIMER', 'POS', 'PEEK', 'USR', 'EOF', 'FRE',
|
|
121
|
+
'LEFT$', 'RIGHT$', 'MID$', 'CHR$', 'STR$', 'HEX$', 'BIN$',
|
|
122
|
+
'ASC', 'VAL', 'INSTR', 'LEN'}),
|
|
123
|
+
'constants': sorted(ExpressionMixin._SAFE_CONSTS.keys()),
|
|
124
|
+
}
|
|
125
|
+
print(_json.dumps(spec, indent=2))
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
91
128
|
term = QBasicTerminal()
|
|
92
129
|
# JSON mode implies agent use, so confine file writes to the working dir.
|
|
93
130
|
term.agent_mode = agent_mode or json_mode
|
|
@@ -75,7 +75,11 @@ class ControlFlowMixin:
|
|
|
75
75
|
name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
|
|
76
76
|
self._assert_assignable(name)
|
|
77
77
|
base = getattr(self, '_option_base', 0)
|
|
78
|
-
|
|
78
|
+
# String arrays (name$) hold string values; numeric arrays hold floats.
|
|
79
|
+
if name.endswith('$'):
|
|
80
|
+
val = self._eval_string_expr(val_expr, run_vars)
|
|
81
|
+
else:
|
|
82
|
+
val = self._eval_with_vars(val_expr, run_vars)
|
|
79
83
|
parts = self._split_arg_list(idx_expr)
|
|
80
84
|
if len(parts) > 1:
|
|
81
85
|
# Multi-dimensional write: flatten with the same stride convention
|
|
@@ -97,10 +101,19 @@ class ControlFlowMixin:
|
|
|
97
101
|
idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
|
|
98
102
|
if idx < 0:
|
|
99
103
|
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
|
|
104
|
+
dimmed = getattr(self, '_dimmed_arrays', set())
|
|
100
105
|
if name not in self.arrays:
|
|
106
|
+
# Implicit array: created (and allowed to grow) on first assignment.
|
|
101
107
|
self.arrays[name] = [0.0] * (idx + 1)
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
elif idx >= len(self.arrays[name]):
|
|
109
|
+
if name in dimmed:
|
|
110
|
+
# Explicitly DIMmed: writes are bounds-checked like reads,
|
|
111
|
+
# instead of silently auto-extending past the declared size.
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
f"ARRAY INDEX OUT OF RANGE: {name}({idx + base}), "
|
|
114
|
+
f"size {len(self.arrays[name])}")
|
|
115
|
+
while idx >= len(self.arrays[name]):
|
|
116
|
+
self.arrays[name].append(0.0)
|
|
104
117
|
self.arrays[name][idx] = val
|
|
105
118
|
return True, ExecResult.ADVANCE
|
|
106
119
|
|
|
@@ -171,6 +184,12 @@ class ControlFlowMixin:
|
|
|
171
184
|
# Quoted literal: emit verbatim (no substitution, no SPC/TAB).
|
|
172
185
|
if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
|
|
173
186
|
return item[1:-1]
|
|
187
|
+
# A mid-circuit measurement bit (MEAS/SYNDROME -> var) has no single
|
|
188
|
+
# value in standard mode; it is resolved per shot inside the if_test.
|
|
189
|
+
# Show that instead of the placeholder 0.
|
|
190
|
+
cb = getattr(self, '_classical_bits', None)
|
|
191
|
+
if cb and item in cb:
|
|
192
|
+
return f"<{item}: mid-circuit bit, resolved per shot>"
|
|
174
193
|
text = self._substitute_vars(item, run_vars)
|
|
175
194
|
|
|
176
195
|
def _spaces(m):
|
|
@@ -54,6 +54,11 @@ class Engine:
|
|
|
54
54
|
self.variables: dict[str, Any] = {}
|
|
55
55
|
self.arrays: dict[str, Any] = {}
|
|
56
56
|
self._array_dims: dict[str, list[int]] = {}
|
|
57
|
+
# Arrays declared with DIM/REDIM enforce their bounds on write;
|
|
58
|
+
# arrays created implicitly by first assignment keep auto-growing.
|
|
59
|
+
self._dimmed_arrays: set[str] = set()
|
|
60
|
+
# Density matrix captured by a no-MEASURE run with SET_DENSITY.
|
|
61
|
+
self._last_density: Any = None
|
|
57
62
|
|
|
58
63
|
# Subroutines and registers
|
|
59
64
|
self.subroutines: dict[str, Any] = {}
|
|
@@ -112,6 +117,8 @@ class Engine:
|
|
|
112
117
|
self.variables.clear()
|
|
113
118
|
self.arrays.clear()
|
|
114
119
|
self._array_dims.clear()
|
|
120
|
+
self._dimmed_arrays.clear()
|
|
121
|
+
self._last_density = None
|
|
115
122
|
self.last_counts = None
|
|
116
123
|
self.last_sv = None
|
|
117
124
|
self.last_circuit = None
|
|
@@ -113,6 +113,8 @@ class ExecutorMixin:
|
|
|
113
113
|
self._classical_bits = {}
|
|
114
114
|
# Partial-measurement subset (None = measure all at the end).
|
|
115
115
|
self._measure_subset = None
|
|
116
|
+
# Density matrix captured by a no-MEASURE run (None unless set).
|
|
117
|
+
self._last_density = None
|
|
116
118
|
# Apply any qubit state preparation requested via POKE to $0100.
|
|
117
119
|
if getattr(self, '_poke_state_prep', None):
|
|
118
120
|
self._emit_poke_state_prep(qc)
|
|
@@ -289,6 +291,7 @@ class ExecutorMixin:
|
|
|
289
291
|
# 3. Remaining statement handlers (not in control-flow dispatch)
|
|
290
292
|
from qubasic_core.statements import RestoreStmt
|
|
291
293
|
if isinstance(parsed, RestoreStmt):
|
|
294
|
+
self._data_ptr = 0 # reset the DATA read pointer (was a no-op)
|
|
292
295
|
return ExecResult.ADVANCE
|
|
293
296
|
|
|
294
297
|
# Multi-line IF block markers — no-ops during execution
|
|
@@ -124,6 +124,18 @@ def _basic_xor(a: Any, b: Any) -> int:
|
|
|
124
124
|
return _as_int(a) ^ _as_int(b)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def _basic_round(x: Any, ndigits: Any = None) -> float:
|
|
128
|
+
"""Round half away from zero (BASIC convention), not Python's half-to-even.
|
|
129
|
+
|
|
130
|
+
round(2.5) == 3, round(-2.5) == -3, round(2.345, 2) == 2.35.
|
|
131
|
+
"""
|
|
132
|
+
nd = int(ndigits) if ndigits is not None else 0
|
|
133
|
+
f = 10 ** nd
|
|
134
|
+
y = float(x) * f
|
|
135
|
+
r = math.floor(y + 0.5) if y >= 0 else math.ceil(y - 0.5)
|
|
136
|
+
return (r / f) if nd else float(r)
|
|
137
|
+
|
|
138
|
+
|
|
127
139
|
class ExpressionMixin:
|
|
128
140
|
"""AST-based safe expression evaluation. No eval().
|
|
129
141
|
|
|
@@ -138,7 +150,7 @@ class ExpressionMixin:
|
|
|
138
150
|
# INT floors toward negative infinity, as in QBASIC (INT(-3.2) = -4).
|
|
139
151
|
# FIX truncates toward zero (FIX(-3.2) = -3) for the other convention.
|
|
140
152
|
'abs': abs, 'int': math.floor, 'fix': math.trunc, 'float': float,
|
|
141
|
-
'min': min, 'max': max, 'round':
|
|
153
|
+
'min': min, 'max': max, 'round': _basic_round, 'len': len,
|
|
142
154
|
'ceil': math.ceil, 'floor': math.floor,
|
|
143
155
|
}
|
|
144
156
|
_SAFE_CONSTS = {
|
|
@@ -203,16 +215,18 @@ class ExpressionMixin:
|
|
|
203
215
|
result = op(result, self._ast_eval(val, ns))
|
|
204
216
|
return result
|
|
205
217
|
if isinstance(node, ast.Compare):
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
# A chained comparison (a < b < c) is ambiguous: Python reads it as
|
|
219
|
+
# (a<b) and (b<c), BASIC as the left-to-right (a<b)<c. Rather than
|
|
220
|
+
# pick one silently, require it to be written explicitly.
|
|
221
|
+
if len(node.ops) > 1:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"AMBIGUOUS CHAINED COMPARISON: write it explicitly, "
|
|
224
|
+
"e.g. (a < b) AND (b < c)")
|
|
225
|
+
op = self._AST_OPS.get(type(node.ops[0]))
|
|
226
|
+
if op is None:
|
|
227
|
+
raise ValueError(f"UNSUPPORTED OP: {type(node.ops[0]).__name__}")
|
|
228
|
+
return op(self._ast_eval(node.left, ns),
|
|
229
|
+
self._ast_eval(node.comparators[0], ns))
|
|
216
230
|
if isinstance(node, ast.Call):
|
|
217
231
|
if not isinstance(node.func, ast.Name):
|
|
218
232
|
raise ValueError("ONLY SIMPLE FUNCTION CALLS ALLOWED")
|
|
@@ -42,7 +42,7 @@ REGISTERS & SUBROUTINES
|
|
|
42
42
|
DEFS List subroutines
|
|
43
43
|
|
|
44
44
|
VARIABLES & LOOPS
|
|
45
|
-
LET angle = PI/4 Set a variable
|
|
45
|
+
LET angle = PI/4 Set a variable (LET optional: x = 5 also works)
|
|
46
46
|
10 RX angle, 0 Use in gate parameters
|
|
47
47
|
10 FOR I = 0 TO 3 Loop (variable substitution in body)
|
|
48
48
|
20 H I
|
|
@@ -62,6 +62,7 @@ CONFIGURATION
|
|
|
62
62
|
SHOTS n Set number of shots (default: 1024)
|
|
63
63
|
METHOD name Set simulation method (automatic, statevector,
|
|
64
64
|
matrix_product_state, stabilizer, ...)
|
|
65
|
+
STATUS [JSON] Show every active mode (qubits, method, LOCC, noise, ...)
|
|
65
66
|
|
|
66
67
|
DEMOS
|
|
67
68
|
DEMO LIST List available demos
|
|
@@ -141,7 +142,7 @@ INLINE CIRCUIT INSTRUCTIONS (in programs, results available after RUN)
|
|
|
141
142
|
SAVE_EXPECT ZZ 0,1 -> v Expectation value -> variable
|
|
142
143
|
SAVE_PROBS 0,1 -> p Probability snapshot -> array
|
|
143
144
|
SAVE_AMPS 0,3 -> a Specific amplitudes -> array
|
|
144
|
-
MEAS qubit -> var Mid-circuit measurement (
|
|
145
|
+
MEAS qubit -> var Mid-circuit measurement + IF feedforward (any mode)
|
|
145
146
|
MEASURE_X/Y/Z qubit Basis measurement
|
|
146
147
|
|
|
147
148
|
FLOW CONTROL (in programs)
|
|
@@ -162,8 +163,8 @@ FLOW CONTROL (in programs)
|
|
|
162
163
|
LET arr[i] = val Array assignment
|
|
163
164
|
|
|
164
165
|
EXPRESSIONS
|
|
165
|
-
PI, TAU, E, SQRT2, sin(), cos(), sqrt(), log(),
|
|
166
|
-
Comparisons: ==, !=, <, >, <=,
|
|
166
|
+
PI, TAU, E, SQRT2, sin(), cos(), sqrt(), log(), int() floors, fix() truncates
|
|
167
|
+
Comparisons: ==, !=, <>, <, >, <=, >= Logical/bitwise: AND, OR, NOT, XOR
|
|
167
168
|
Arrays: arr(i) or arr[i]
|
|
168
169
|
Example: LET theta = PI/4 + asin(0.5)
|
|
169
170
|
"""
|
|
@@ -18,13 +18,13 @@ RE_MEAS = re.compile(r'MEAS\s+(\S+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
|
18
18
|
RE_RESET = re.compile(r'RESET\s+(\S+)', re.IGNORECASE)
|
|
19
19
|
RE_UNITARY = re.compile(r'UNITARY\s+(\w+)\s*=\s*(\[.+\])', re.IGNORECASE)
|
|
20
20
|
RE_DIM = re.compile(r'DIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
|
|
21
|
-
RE_REDIM = re.compile(r'REDIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
|
|
21
|
+
RE_REDIM = re.compile(r'REDIM\s+(PRESERVE\s+)?(\w+)\((\d+)\)', re.IGNORECASE)
|
|
22
22
|
RE_ERASE = re.compile(r'ERASE\s+(\w+)', re.IGNORECASE)
|
|
23
23
|
RE_GET = re.compile(r'GET\s+(\w+\$?)', re.IGNORECASE)
|
|
24
24
|
RE_INPUT = re.compile(r'INPUT\s+(?:"([^"]*)"\s*,\s*)?(\w+)', re.IGNORECASE)
|
|
25
25
|
RE_CTRL = re.compile(r'CTRL\s+(\w+)\s+(.*)', re.IGNORECASE)
|
|
26
26
|
RE_INV = re.compile(r'INV\s+(\w+)\s+(.*)', re.IGNORECASE)
|
|
27
|
-
RE_LET_ARRAY = re.compile(r'LET\s+(\w
|
|
27
|
+
RE_LET_ARRAY = re.compile(r'LET\s+(\w+\$?)\((.+?)\)\s*=\s*(.*)', re.IGNORECASE)
|
|
28
28
|
RE_LET_VAR = re.compile(r'LET\s+(\w+(?:\.\w+)?)\s*=\s*(.*)', re.IGNORECASE)
|
|
29
29
|
RE_PRINT = re.compile(r'PRINT\s+(.*)', re.IGNORECASE)
|
|
30
30
|
RE_GOTO = re.compile(r'GOTO\s+(\d+)\s*$', re.IGNORECASE)
|
|
@@ -29,8 +29,11 @@ def _chr_fn(n: float) -> str:
|
|
|
29
29
|
return chr(int(n))
|
|
30
30
|
|
|
31
31
|
def _str_fn(n: float) -> str:
|
|
32
|
+
# BASIC reserves a leading space for the sign of a non-negative number,
|
|
33
|
+
# so STR$(42) is " 42" and STR$(-5) is "-5".
|
|
32
34
|
v = float(n)
|
|
33
|
-
|
|
35
|
+
s = str(int(v)) if v == int(v) else str(v)
|
|
36
|
+
return s if v < 0 else ' ' + s
|
|
34
37
|
|
|
35
38
|
def _val_fn(s: str) -> float:
|
|
36
39
|
try:
|
|
@@ -27,6 +27,9 @@ class SubroutineMixin:
|
|
|
27
27
|
self._static_vars: dict[str, dict[str, Any]] = {'_GLOBAL': {}}
|
|
28
28
|
self._call_stack: list[dict[str, Any]] = []
|
|
29
29
|
self._func_call_depth: int = 0
|
|
30
|
+
# Names declared SHARED in the current scope level, so their
|
|
31
|
+
# modifications survive _pop_scope instead of being discarded.
|
|
32
|
+
self._shared_stack: list[set[str]] = []
|
|
30
33
|
|
|
31
34
|
def _scan_subs(self, sorted_lines: list[int]) -> None:
|
|
32
35
|
"""Scan program for SUB/FUNCTION blocks and build jump table."""
|
|
@@ -284,12 +287,15 @@ class SubroutineMixin:
|
|
|
284
287
|
if vname in self._scope_stack[-1]:
|
|
285
288
|
run_vars[vname] = self._scope_stack[-1].get(vname, 0)
|
|
286
289
|
self.variables[vname] = run_vars[vname]
|
|
290
|
+
if self._shared_stack:
|
|
291
|
+
self._shared_stack[-1].add(vname)
|
|
287
292
|
return True, ExecResult.ADVANCE
|
|
288
293
|
|
|
289
294
|
# ── Scope management ───────────────────────────────────────────────
|
|
290
295
|
|
|
291
296
|
def _push_scope(self) -> None:
|
|
292
297
|
self._scope_stack.append(dict(self.variables))
|
|
298
|
+
self._shared_stack.append(set())
|
|
293
299
|
|
|
294
300
|
def _pop_scope(self, frame: dict[str, Any] | None = None) -> None:
|
|
295
301
|
# Save STATIC vars for the current frame before restoring outer scope.
|
|
@@ -303,5 +309,13 @@ class SubroutineMixin:
|
|
|
303
309
|
for vname in list(self._static_vars[sub_name]):
|
|
304
310
|
self._static_vars[sub_name][vname] = self.variables.get(vname, 0)
|
|
305
311
|
if self._scope_stack:
|
|
312
|
+
# SHARED variables must keep the modifications made inside the
|
|
313
|
+
# call; capture them before restoring the caller snapshot and
|
|
314
|
+
# write them back, instead of discarding them with the snapshot.
|
|
315
|
+
shared = self._shared_stack.pop() if self._shared_stack else set()
|
|
316
|
+
shared_vals = {v: self.variables.get(v) for v in shared}
|
|
306
317
|
self.variables.clear()
|
|
307
318
|
self.variables.update(self._scope_stack.pop())
|
|
319
|
+
for v, val in shared_vals.items():
|
|
320
|
+
if val is not None:
|
|
321
|
+
self.variables[v] = val
|
|
@@ -915,6 +915,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
915
915
|
if upper.startswith('BEGIN'):
|
|
916
916
|
return self._def_multiline(rest[5:].strip())
|
|
917
917
|
|
|
918
|
+
# DEF FN name(params) = expr — a user expression function, same as the
|
|
919
|
+
# in-program form, so it works at the prompt too.
|
|
920
|
+
if upper.startswith('FN'):
|
|
921
|
+
from qubasic_core.patterns import RE_DEF_FN
|
|
922
|
+
fm = RE_DEF_FN.match(f"DEF {rest}")
|
|
923
|
+
if fm:
|
|
924
|
+
fparams = [p.strip() for p in fm.group(2).split(',') if p.strip()]
|
|
925
|
+
self._user_fns['FN' + fm.group(1).upper()] = {
|
|
926
|
+
'params': fparams, 'body': fm.group(3).strip()}
|
|
927
|
+
self.io.writeln(f"DEF FN {fm.group(1).upper()}({', '.join(fparams)})")
|
|
928
|
+
return
|
|
929
|
+
|
|
918
930
|
m = RE_DEF_SINGLE.match(rest)
|
|
919
931
|
if not m:
|
|
920
932
|
self.io.writeln("?USAGE: DEF NAME[(params)] = GATE : GATE : ...")
|
|
@@ -1067,7 +1079,11 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1067
1079
|
parsed = LetArrayStmt(raw=f"LET {rest}", name=name,
|
|
1068
1080
|
index_expr=idx_expr, value_expr=val_expr)
|
|
1069
1081
|
self._cf_let_array(f"LET {rest}", self.variables, parsed)
|
|
1070
|
-
self.
|
|
1082
|
+
shown = (self._eval_string_expr(val_expr) if name.endswith('$')
|
|
1083
|
+
else self.eval_expr(val_expr))
|
|
1084
|
+
self.io.writeln(f"{name}({idx_expr.strip()}) = {shown!r}"
|
|
1085
|
+
if isinstance(shown, str)
|
|
1086
|
+
else f"{name}({idx_expr.strip()}) = {shown}")
|
|
1071
1087
|
return
|
|
1072
1088
|
sm = RE_LET_STR.match(f"LET {rest}")
|
|
1073
1089
|
if sm:
|
|
@@ -1122,7 +1138,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1122
1138
|
'trace_mode': bool(getattr(self, '_trace_mode', False)),
|
|
1123
1139
|
'bank': getattr(self, '_current_slot', 0),
|
|
1124
1140
|
'program_lines': len(self.program),
|
|
1125
|
-
'variables':
|
|
1141
|
+
'variables': sum(1 for k in self.variables if not k.startswith('_')),
|
|
1126
1142
|
'arrays': len(self.arrays),
|
|
1127
1143
|
'subroutines': len(self.subroutines),
|
|
1128
1144
|
'custom_gates': sorted(self._custom_gates.keys()),
|
|
@@ -1239,7 +1255,25 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1239
1255
|
2^n statevector that cannot be displayed anyway.
|
|
1240
1256
|
"""
|
|
1241
1257
|
if getattr(self, '_pending_set_density', None) is not None:
|
|
1242
|
-
|
|
1258
|
+
# Mixed state: no pure statevector. Capture the density matrix so
|
|
1259
|
+
# DENSITY works after a measured run, not just a no-MEASURE one.
|
|
1260
|
+
self.last_sv = None
|
|
1261
|
+
if self.num_qubits <= self._SV_EXTRACT_MAX_QUBITS:
|
|
1262
|
+
try:
|
|
1263
|
+
qc_sv.save_density_matrix()
|
|
1264
|
+
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1265
|
+
_kw = {}
|
|
1266
|
+
if self._seed is not None:
|
|
1267
|
+
_kw['seed_simulator'] = self._seed
|
|
1268
|
+
dm_result = dm_backend.run(
|
|
1269
|
+
transpile(qc_sv, dm_backend,
|
|
1270
|
+
optimization_level=self._transpile_opt_level),
|
|
1271
|
+
**_kw).result()
|
|
1272
|
+
data = dm_result.data(0)
|
|
1273
|
+
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1274
|
+
self._last_density = np.array(dm) if dm is not None else None
|
|
1275
|
+
except Exception:
|
|
1276
|
+
self._last_density = None
|
|
1243
1277
|
return
|
|
1244
1278
|
if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
|
|
1245
1279
|
self.last_sv = None
|
|
@@ -1281,9 +1315,24 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1281
1315
|
}
|
|
1282
1316
|
|
|
1283
1317
|
def _run_no_measure(self, qc, qc_sv, t0: float) -> None:
|
|
1284
|
-
"""Execute the no-MEASURE path: statevector
|
|
1318
|
+
"""Execute the no-MEASURE path: statevector (or density matrix), no shots."""
|
|
1285
1319
|
too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
|
|
1286
|
-
if too_large:
|
|
1320
|
+
if getattr(self, '_pending_set_density', None) is not None and not too_large:
|
|
1321
|
+
# A mixed state has no statevector, so capture the density matrix
|
|
1322
|
+
# for DENSITY instead. Saving a statevector here used to raise an
|
|
1323
|
+
# untranslatable-circuit error from Aer.
|
|
1324
|
+
self.last_sv = None
|
|
1325
|
+
try:
|
|
1326
|
+
qc_sv.save_density_matrix()
|
|
1327
|
+
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1328
|
+
dm_result = dm_backend.run(
|
|
1329
|
+
transpile(qc_sv, dm_backend, optimization_level=self._transpile_opt_level)).result()
|
|
1330
|
+
data = dm_result.data(0)
|
|
1331
|
+
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1332
|
+
self._last_density = np.array(dm) if dm is not None else None
|
|
1333
|
+
except Exception:
|
|
1334
|
+
self._last_density = None
|
|
1335
|
+
elif too_large:
|
|
1287
1336
|
self.last_sv = None
|
|
1288
1337
|
else:
|
|
1289
1338
|
try:
|
|
@@ -1912,13 +1961,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1912
1961
|
if not m:
|
|
1913
1962
|
return False
|
|
1914
1963
|
name = m.group(1)
|
|
1964
|
+
base = getattr(self, '_option_base', 0)
|
|
1915
1965
|
dims = [int(d.strip()) for d in m.group(2).split(',')]
|
|
1966
|
+
# Inclusive sizing (QBASIC): DIM a(n) spans indices base..n, so the
|
|
1967
|
+
# declared top index n is valid (n - base + 1 slots per dimension).
|
|
1968
|
+
sizes = [max(0, d - base + 1) for d in dims]
|
|
1916
1969
|
total = 1
|
|
1917
|
-
for
|
|
1918
|
-
total *=
|
|
1970
|
+
for s in sizes:
|
|
1971
|
+
total *= s
|
|
1919
1972
|
self.arrays[name] = [0.0] * total
|
|
1920
|
-
if len(
|
|
1921
|
-
self._array_dims[name] =
|
|
1973
|
+
if len(sizes) > 1:
|
|
1974
|
+
self._array_dims[name] = sizes
|
|
1975
|
+
self._dimmed_arrays.add(name)
|
|
1922
1976
|
return True
|
|
1923
1977
|
|
|
1924
1978
|
def _try_exec_dim_type(self, stmt: str) -> bool:
|
|
@@ -1943,20 +1997,27 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1943
1997
|
return True
|
|
1944
1998
|
|
|
1945
1999
|
def _try_exec_redim(self, stmt: str) -> bool:
|
|
1946
|
-
"""Handle REDIM name(size) — resize an
|
|
2000
|
+
"""Handle REDIM [PRESERVE] name(size) — resize an array.
|
|
2001
|
+
|
|
2002
|
+
Plain REDIM clears to zeros (QBASIC semantics); REDIM PRESERVE keeps the
|
|
2003
|
+
existing elements. Sizing is inclusive (REDIM a(n) spans indices base..n).
|
|
2004
|
+
"""
|
|
1947
2005
|
m = RE_REDIM.match(stmt)
|
|
1948
2006
|
if not m:
|
|
1949
2007
|
return False
|
|
1950
|
-
|
|
1951
|
-
|
|
2008
|
+
preserve = bool(m.group(1))
|
|
2009
|
+
name = m.group(2)
|
|
2010
|
+
base = getattr(self, '_option_base', 0)
|
|
2011
|
+
new_len = max(0, int(m.group(3)) - base + 1)
|
|
1952
2012
|
old = self.arrays.get(name, [])
|
|
1953
|
-
if isinstance(old, list):
|
|
1954
|
-
if
|
|
1955
|
-
self.arrays[name] = old + [0.0] * (
|
|
2013
|
+
if preserve and isinstance(old, list):
|
|
2014
|
+
if new_len > len(old):
|
|
2015
|
+
self.arrays[name] = old + [0.0] * (new_len - len(old))
|
|
1956
2016
|
else:
|
|
1957
|
-
self.arrays[name] = old[:
|
|
2017
|
+
self.arrays[name] = old[:new_len]
|
|
1958
2018
|
else:
|
|
1959
|
-
self.arrays[name] = [0.0] *
|
|
2019
|
+
self.arrays[name] = [0.0] * new_len
|
|
2020
|
+
self._dimmed_arrays.add(name)
|
|
1960
2021
|
return True
|
|
1961
2022
|
|
|
1962
2023
|
def _try_exec_erase(self, stmt: str) -> bool:
|
|
@@ -1967,6 +2028,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1967
2028
|
name = m.group(1)
|
|
1968
2029
|
if name in self.arrays:
|
|
1969
2030
|
del self.arrays[name]
|
|
2031
|
+
self._array_dims.pop(name, None)
|
|
2032
|
+
self._dimmed_arrays.discard(name)
|
|
1970
2033
|
return True
|
|
1971
2034
|
|
|
1972
2035
|
def _try_exec_get(self, stmt: str, run_vars: dict) -> bool:
|
|
@@ -487,8 +487,9 @@ class TestStrings(unittest.TestCase):
|
|
|
487
487
|
self.assertEqual(_instr("HELLO", "XYZ"), 0.0)
|
|
488
488
|
self.assertEqual(_hex_fn(255), "FF")
|
|
489
489
|
self.assertEqual(_bin_fn(5), "101")
|
|
490
|
-
self.assertEqual(_str_fn(42.0), "42")
|
|
491
|
-
self.assertEqual(_str_fn(3.14), "3.14")
|
|
490
|
+
self.assertEqual(_str_fn(42.0), " 42") # leading space for non-negative
|
|
491
|
+
self.assertEqual(_str_fn(3.14), " 3.14")
|
|
492
|
+
self.assertEqual(_str_fn(-5.0), "-5")
|
|
492
493
|
self.assertEqual(_val_fn("42"), 42.0)
|
|
493
494
|
self.assertEqual(_val_fn("abc"), 0.0)
|
|
494
495
|
self.assertEqual(_len_fn("HELLO"), 5.0)
|
|
@@ -1048,7 +1049,7 @@ class TestProgramManagement(unittest.TestCase):
|
|
|
1048
1049
|
_, out = capture(t_dim.cmd_run)
|
|
1049
1050
|
self.assertIn('matrix', t_dim.arrays)
|
|
1050
1051
|
self.assertIsInstance(t_dim.arrays['matrix'], list)
|
|
1051
|
-
self.assertEqual(len(t_dim.arrays['matrix']),
|
|
1052
|
+
self.assertEqual(len(t_dim.arrays['matrix']), 16) # inclusive: (0..3)^2
|
|
1052
1053
|
|
|
1053
1054
|
# DIM single
|
|
1054
1055
|
t_dim2 = QBasicTerminal()
|
|
@@ -1056,7 +1057,7 @@ class TestProgramManagement(unittest.TestCase):
|
|
|
1056
1057
|
t_dim2.process('20 END')
|
|
1057
1058
|
capture(t_dim2.cmd_run)
|
|
1058
1059
|
self.assertIn('arr', t_dim2.arrays)
|
|
1059
|
-
self.assertEqual(len(t_dim2.arrays['arr']), 5
|
|
1060
|
+
self.assertEqual(len(t_dim2.arrays['arr']), 6) # inclusive: indices 0..5
|
|
1060
1061
|
|
|
1061
1062
|
# IMPORT namespace
|
|
1062
1063
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.qb', dir='.', delete=False) as f:
|
|
@@ -2305,20 +2306,26 @@ class TestGapCoverage(unittest.TestCase):
|
|
|
2305
2306
|
|
|
2306
2307
|
# 16 REDIM
|
|
2307
2308
|
def test_redim(self):
|
|
2308
|
-
"""REDIM resizes
|
|
2309
|
-
# DIM
|
|
2309
|
+
"""REDIM resizes (inclusive sizing); plain clears, PRESERVE keeps."""
|
|
2310
|
+
# DIM a(5) spans indices 0..5 -> 6 slots (inclusive sizing).
|
|
2310
2311
|
self.t._try_exec_dim('DIM arr(5)')
|
|
2311
|
-
self.assertEqual(len(self.t.arrays['arr']),
|
|
2312
|
-
|
|
2312
|
+
self.assertEqual(len(self.t.arrays['arr']), 6)
|
|
2313
|
+
self.t.arrays['arr'][2] = 99.0
|
|
2314
|
+
# Plain REDIM clears to zeros.
|
|
2313
2315
|
self.t._try_exec_redim('REDIM arr(8)')
|
|
2314
|
-
self.assertEqual(len(self.t.arrays['arr']),
|
|
2315
|
-
self.assertEqual(self.t.arrays['arr'][
|
|
2316
|
-
# REDIM
|
|
2317
|
-
self.t.
|
|
2318
|
-
self.
|
|
2319
|
-
|
|
2316
|
+
self.assertEqual(len(self.t.arrays['arr']), 9)
|
|
2317
|
+
self.assertEqual(self.t.arrays['arr'][2], 0.0)
|
|
2318
|
+
# REDIM PRESERVE keeps existing data.
|
|
2319
|
+
self.t.arrays['arr'][1] = 7.0
|
|
2320
|
+
self.t._try_exec_redim('REDIM PRESERVE arr(10)')
|
|
2321
|
+
self.assertEqual(len(self.t.arrays['arr']), 11)
|
|
2322
|
+
self.assertEqual(self.t.arrays['arr'][1], 7.0)
|
|
2323
|
+
# PRESERVE smaller truncates.
|
|
2324
|
+
self.t._try_exec_redim('REDIM PRESERVE arr(3)')
|
|
2325
|
+
self.assertEqual(len(self.t.arrays['arr']), 4)
|
|
2326
|
+
# REDIM on a non-existent array creates it.
|
|
2320
2327
|
self.t._try_exec_redim('REDIM newary(4)')
|
|
2321
|
-
self.assertEqual(len(self.t.arrays['newary']),
|
|
2328
|
+
self.assertEqual(len(self.t.arrays['newary']), 5)
|
|
2322
2329
|
|
|
2323
2330
|
|
|
2324
2331
|
# =====================================================================
|
|
@@ -2096,6 +2096,114 @@ class TestConventionAndStateRegressions(unittest.TestCase):
|
|
|
2096
2096
|
self.assertIsNotNone(t.last_counts) # ran; not blocked as a "command"
|
|
2097
2097
|
|
|
2098
2098
|
|
|
2099
|
+
class TestDeepFixRegressions(unittest.TestCase):
|
|
2100
|
+
"""SHARED writeback, RESTORE, array write bounds, string arrays,
|
|
2101
|
+
SET_DENSITY inspection, STATUS var count, immediate DEF FN."""
|
|
2102
|
+
|
|
2103
|
+
def _runp(self, lines, nq=1):
|
|
2104
|
+
t = QBasicTerminal(); t.num_qubits = nq
|
|
2105
|
+
for l in lines:
|
|
2106
|
+
t.process(l, track_undo=False)
|
|
2107
|
+
capture(t.cmd_run)
|
|
2108
|
+
return t
|
|
2109
|
+
|
|
2110
|
+
def test_shared_writeback(self):
|
|
2111
|
+
t = self._runp(['10 LET acc=0', '20 SUB ADD(n)', '30 SHARED acc',
|
|
2112
|
+
'40 LET acc=acc+n', '50 END SUB', '60 CALL ADD(5)',
|
|
2113
|
+
'70 CALL ADD(3)'])
|
|
2114
|
+
self.assertEqual(t.variables.get('acc'), 8.0)
|
|
2115
|
+
|
|
2116
|
+
def test_restore_resets_data_pointer(self):
|
|
2117
|
+
t = self._runp(['10 DATA 5', '20 READ a', '30 RESTORE', '40 READ b',
|
|
2118
|
+
'50 LET s=a+b'])
|
|
2119
|
+
self.assertEqual(t.variables.get('s'), 10.0)
|
|
2120
|
+
|
|
2121
|
+
def test_dimmed_array_write_is_bounds_checked(self):
|
|
2122
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2123
|
+
for l in ['10 DIM a(3)', '20 LET a(99)=1']:
|
|
2124
|
+
t.process(l, track_undo=False)
|
|
2125
|
+
_, out = capture(t.cmd_run)
|
|
2126
|
+
self.assertIn('OUT OF RANGE', out)
|
|
2127
|
+
# An implicit (undimensioned) array still auto-grows on write.
|
|
2128
|
+
t2 = self._runp(['10 LET b(50)=7', '20 LET v=b(50)'])
|
|
2129
|
+
self.assertEqual(t2.variables.get('v'), 7.0)
|
|
2130
|
+
|
|
2131
|
+
def test_string_array(self):
|
|
2132
|
+
t = self._runp(['10 LET s$(0)="he"+"llo"', '20 LET t$=s$(0)'])
|
|
2133
|
+
self.assertEqual(t.variables.get('t$'), 'hello')
|
|
2134
|
+
|
|
2135
|
+
def test_set_density_no_measure_does_not_crash(self):
|
|
2136
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2137
|
+
t.process('SET_DENSITY [[0.5,0],[0,0.5]]', track_undo=False)
|
|
2138
|
+
t.process('10 ID 0', track_undo=False)
|
|
2139
|
+
_, out = capture(t.cmd_run)
|
|
2140
|
+
self.assertNotIn('Unable to translate', out)
|
|
2141
|
+
self.assertIsNotNone(getattr(t, '_last_density', None))
|
|
2142
|
+
_, dout = capture(t.cmd_density, '')
|
|
2143
|
+
self.assertIn('Density matrix', dout)
|
|
2144
|
+
|
|
2145
|
+
def test_status_excludes_internal_vars(self):
|
|
2146
|
+
t = self._runp(['10 H 0', '20 MEASURE'])
|
|
2147
|
+
self.assertEqual(t._status_dict()['variables'], 0)
|
|
2148
|
+
|
|
2149
|
+
def test_immediate_def_fn(self):
|
|
2150
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2151
|
+
capture(t.cmd_def, 'FN sq(x) = x*x')
|
|
2152
|
+
self.assertEqual(t.eval_expr('sq(5)'), 25.0)
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
class TestConventionFixesV2(unittest.TestCase):
|
|
2156
|
+
"""round half-away, chained-comparison guard, inclusive DIM, REDIM
|
|
2157
|
+
PRESERVE, MEAS print hint, DENSITY after MEASURE."""
|
|
2158
|
+
|
|
2159
|
+
def setUp(self):
|
|
2160
|
+
self.t = QBasicTerminal(); self.t.num_qubits = 1
|
|
2161
|
+
|
|
2162
|
+
def _runp(self, lines, nq=1):
|
|
2163
|
+
t = QBasicTerminal(); t.num_qubits = nq
|
|
2164
|
+
for l in lines:
|
|
2165
|
+
t.process(l, track_undo=False)
|
|
2166
|
+
_, out = capture(t.cmd_run)
|
|
2167
|
+
t._out = out
|
|
2168
|
+
return t
|
|
2169
|
+
|
|
2170
|
+
def test_round_half_away_from_zero(self):
|
|
2171
|
+
self.assertEqual(self.t.eval_expr('round(2.5)'), 3.0)
|
|
2172
|
+
self.assertEqual(self.t.eval_expr('round(3.5)'), 4.0)
|
|
2173
|
+
self.assertEqual(self.t.eval_expr('round(-2.5)'), -3.0)
|
|
2174
|
+
self.assertAlmostEqual(self.t.eval_expr('round(2.345, 2)'), 2.35)
|
|
2175
|
+
|
|
2176
|
+
def test_chained_comparison_raises(self):
|
|
2177
|
+
self.assertTrue(bool(self.t._safe_eval('3 < 5')))
|
|
2178
|
+
with self.assertRaises(ValueError):
|
|
2179
|
+
self.t._safe_eval('1 < 2 < 3')
|
|
2180
|
+
|
|
2181
|
+
def test_dim_inclusive_top_index(self):
|
|
2182
|
+
t = self._runp(['10 DIM a(5)', '20 LET a(5)=7', '30 LET v=a(5)'])
|
|
2183
|
+
self.assertEqual(t.variables.get('v'), 7.0)
|
|
2184
|
+
t2 = self._runp(['10 DIM a(5)', '20 LET a(6)=1'])
|
|
2185
|
+
self.assertIn('OUT OF RANGE', t2._out)
|
|
2186
|
+
|
|
2187
|
+
def test_redim_clear_vs_preserve(self):
|
|
2188
|
+
t = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM a(5)', '40 LET v=a(1)'])
|
|
2189
|
+
self.assertEqual(t.variables.get('v'), 0.0)
|
|
2190
|
+
t2 = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM PRESERVE a(5)', '40 LET v=a(1)'])
|
|
2191
|
+
self.assertEqual(t2.variables.get('v'), 9.0)
|
|
2192
|
+
|
|
2193
|
+
def test_meas_print_hint(self):
|
|
2194
|
+
t = self._runp(['10 X 0', '20 MEAS 0 -> m', '30 PRINT m', '40 MEASURE'])
|
|
2195
|
+
self.assertIn('mid-circuit', t._out)
|
|
2196
|
+
|
|
2197
|
+
def test_density_after_measure(self):
|
|
2198
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2199
|
+
t.process('SET_DENSITY [[0.5,0],[0,0.5]]', track_undo=False)
|
|
2200
|
+
for l in ['10 H 0', '20 MEASURE']:
|
|
2201
|
+
t.process(l, track_undo=False)
|
|
2202
|
+
capture(t.cmd_run)
|
|
2203
|
+
_, dout = capture(t.cmd_density, '')
|
|
2204
|
+
self.assertIn('Density matrix', dout)
|
|
2205
|
+
|
|
2206
|
+
|
|
2099
2207
|
if __name__ == '__main__':
|
|
2100
2208
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
2101
2209
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|