qubasic 0.11.1__tar.gz → 0.12.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.12.0}/CHANGELOG.md +18 -0
- {qubasic-0.11.1/qubasic.egg-info → qubasic-0.12.0}/PKG-INFO +4 -1
- {qubasic-0.11.1 → qubasic-0.12.0}/README.md +3 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/pyproject.toml +1 -1
- {qubasic-0.11.1 → qubasic-0.12.0/qubasic.egg-info}/PKG-INFO +4 -1
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/analysis.py +22 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/cli.py +10 -5
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/control_flow.py +16 -3
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/engine_state.py +7 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/executor.py +3 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/help_text.py +5 -4
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/patterns.py +1 -1
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/subs.py +14 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/terminal.py +39 -4
- {qubasic-0.11.1 → qubasic-0.12.0}/tests/test_qubasic.py +56 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/LICENSE +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/MANIFEST.in +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/examples/bell.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/examples/grover3.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/algos2.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/demos.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/expression.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/memory.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/qec.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/qol.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/qudits.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/resources.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/strings.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/setup.cfg +0 -0
- {qubasic-0.11.1 → qubasic-0.12.0}/tests/test_features.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Correctness and robustness pass on the classic-BASIC layer, from an extended
|
|
6
|
+
adversarial audit. The quantum engine is unchanged.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- `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`).
|
|
10
|
+
- `RESTORE` resets the `DATA` read pointer inside a program; it was a no-op, so a `READ` after `RESTORE` raised `OUT OF DATA`.
|
|
11
|
+
- 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.
|
|
12
|
+
- String arrays work: `LET s$(i) = ...` stores and reads string elements.
|
|
13
|
+
- `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`.
|
|
14
|
+
- A script that issues an explicit `RUN` and also contains a `MEASURE` no longer auto-runs (and prints) the program a second time.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `STATUS` reports the user-variable count only, excluding the internal `_DEPTH`/`_GATES`/`_TIME` set after a run.
|
|
18
|
+
- `DEF FN name(x) = expr` now works at the prompt (immediate mode), matching the in-program form.
|
|
19
|
+
- `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`.
|
|
20
|
+
|
|
3
21
|
## 0.11.1 (2026-06-19)
|
|
4
22
|
|
|
5
23
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -255,11 +255,14 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
255
255
|
DIM data(10) 1D array
|
|
256
256
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
257
257
|
LET data(0) = PI
|
|
258
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
258
259
|
REDIM data(20) Resize (preserves existing data)
|
|
259
260
|
ERASE data Delete array
|
|
260
261
|
OPTION BASE 1 Set array index base
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
265
|
+
|
|
263
266
|
## Control flow
|
|
264
267
|
|
|
265
268
|
```
|
|
@@ -222,11 +222,14 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
222
222
|
DIM data(10) 1D array
|
|
223
223
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
224
224
|
LET data(0) = PI
|
|
225
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
225
226
|
REDIM data(20) Resize (preserves existing data)
|
|
226
227
|
ERASE data Delete array
|
|
227
228
|
OPTION BASE 1 Set array index base
|
|
228
229
|
```
|
|
229
230
|
|
|
231
|
+
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
232
|
+
|
|
230
233
|
## Control flow
|
|
231
234
|
|
|
232
235
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -255,11 +255,14 @@ Hex/binary literals: `&HFF`, `&B10110`
|
|
|
255
255
|
DIM data(10) 1D array
|
|
256
256
|
DIM matrix(3, 3) Multi-dimensional (flat storage)
|
|
257
257
|
LET data(0) = PI
|
|
258
|
+
LET names$(0) = "alice" String array (name$ elements hold strings)
|
|
258
259
|
REDIM data(20) Resize (preserves existing data)
|
|
259
260
|
ERASE data Delete array
|
|
260
261
|
OPTION BASE 1 Set array index base
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
265
|
+
|
|
263
266
|
## Control flow
|
|
264
267
|
|
|
265
268
|
```
|
|
@@ -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
|
|
|
@@ -78,12 +82,13 @@ def main():
|
|
|
78
82
|
print("Usage:")
|
|
79
83
|
print(" qubasic Interactive REPL")
|
|
80
84
|
print(" qubasic script.qb Run a script file")
|
|
81
|
-
print(" qubasic --quiet script Suppress banner and progress")
|
|
85
|
+
print(" qubasic --quiet script Suppress banner and progress (also -q)")
|
|
82
86
|
print(" qubasic --json script Output results as JSON")
|
|
83
87
|
print(" qubasic --agent script Confine file writes to the working dir")
|
|
84
88
|
print(" qubasic --seed N script Set random seed for reproducibility")
|
|
85
|
-
print(" qubasic --version Show version")
|
|
86
|
-
print(" qubasic --help Show this help")
|
|
89
|
+
print(" qubasic --version Show version (also -v)")
|
|
90
|
+
print(" qubasic --help Show this help (also -h)")
|
|
91
|
+
print(" python -m qubasic_core Run without the installed console script")
|
|
87
92
|
print()
|
|
88
93
|
print("Type HELP inside the REPL for full command reference.")
|
|
89
94
|
sys.exit(0)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
"""
|
|
@@ -24,7 +24,7 @@ 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)
|
|
@@ -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()),
|
|
@@ -1281,9 +1297,24 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1281
1297
|
}
|
|
1282
1298
|
|
|
1283
1299
|
def _run_no_measure(self, qc, qc_sv, t0: float) -> None:
|
|
1284
|
-
"""Execute the no-MEASURE path: statevector
|
|
1300
|
+
"""Execute the no-MEASURE path: statevector (or density matrix), no shots."""
|
|
1285
1301
|
too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
|
|
1286
|
-
if too_large:
|
|
1302
|
+
if getattr(self, '_pending_set_density', None) is not None and not too_large:
|
|
1303
|
+
# A mixed state has no statevector, so capture the density matrix
|
|
1304
|
+
# for DENSITY instead. Saving a statevector here used to raise an
|
|
1305
|
+
# untranslatable-circuit error from Aer.
|
|
1306
|
+
self.last_sv = None
|
|
1307
|
+
try:
|
|
1308
|
+
qc_sv.save_density_matrix()
|
|
1309
|
+
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1310
|
+
dm_result = dm_backend.run(
|
|
1311
|
+
transpile(qc_sv, dm_backend, optimization_level=self._transpile_opt_level)).result()
|
|
1312
|
+
data = dm_result.data(0)
|
|
1313
|
+
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1314
|
+
self._last_density = np.array(dm) if dm is not None else None
|
|
1315
|
+
except Exception:
|
|
1316
|
+
self._last_density = None
|
|
1317
|
+
elif too_large:
|
|
1287
1318
|
self.last_sv = None
|
|
1288
1319
|
else:
|
|
1289
1320
|
try:
|
|
@@ -1919,6 +1950,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1919
1950
|
self.arrays[name] = [0.0] * total
|
|
1920
1951
|
if len(dims) > 1:
|
|
1921
1952
|
self._array_dims[name] = dims
|
|
1953
|
+
self._dimmed_arrays.add(name)
|
|
1922
1954
|
return True
|
|
1923
1955
|
|
|
1924
1956
|
def _try_exec_dim_type(self, stmt: str) -> bool:
|
|
@@ -1957,6 +1989,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1957
1989
|
self.arrays[name] = old[:new_size]
|
|
1958
1990
|
else:
|
|
1959
1991
|
self.arrays[name] = [0.0] * new_size
|
|
1992
|
+
self._dimmed_arrays.add(name)
|
|
1960
1993
|
return True
|
|
1961
1994
|
|
|
1962
1995
|
def _try_exec_erase(self, stmt: str) -> bool:
|
|
@@ -1967,6 +2000,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1967
2000
|
name = m.group(1)
|
|
1968
2001
|
if name in self.arrays:
|
|
1969
2002
|
del self.arrays[name]
|
|
2003
|
+
self._array_dims.pop(name, None)
|
|
2004
|
+
self._dimmed_arrays.discard(name)
|
|
1970
2005
|
return True
|
|
1971
2006
|
|
|
1972
2007
|
def _try_exec_get(self, stmt: str, run_vars: dict) -> bool:
|
|
@@ -2096,6 +2096,62 @@ 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
|
+
|
|
2099
2155
|
if __name__ == '__main__':
|
|
2100
2156
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
2101
2157
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|