qubasic 0.12.0__tar.gz → 0.14.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.12.0 → qubasic-0.14.0}/CHANGELOG.md +33 -0
- {qubasic-0.12.0/qubasic.egg-info → qubasic-0.14.0}/PKG-INFO +6 -4
- {qubasic-0.12.0 → qubasic-0.14.0}/README.md +5 -3
- {qubasic-0.12.0 → qubasic-0.14.0}/pyproject.toml +1 -1
- {qubasic-0.12.0 → qubasic-0.14.0/qubasic.egg-info}/PKG-INFO +6 -4
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/analysis.py +5 -2
- qubasic-0.14.0/qubasic_core/cli.py +241 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/control_flow.py +9 -2
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/engine_state.py +4 -1
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/executor.py +3 -1
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/expression.py +27 -3
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/patterns.py +3 -3
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/strings.py +4 -1
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/terminal.py +57 -27
- {qubasic-0.12.0 → qubasic-0.14.0}/tests/test_features.py +22 -15
- {qubasic-0.12.0 → qubasic-0.14.0}/tests/test_qubasic.py +76 -1
- qubasic-0.12.0/qubasic_core/cli.py +0 -149
- {qubasic-0.12.0 → qubasic-0.14.0}/LICENSE +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/MANIFEST.in +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/examples/bell.qb +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/examples/grover3.qb +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/algos2.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/demos.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/display.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/help_text.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/memory.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/qec.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/qol.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/qudits.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/resources.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/subs.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.12.0 → qubasic-0.14.0}/setup.cfg +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Second audit-gap round: a complete agent contract, BASIC truth values, range
|
|
6
|
+
comparisons, louder mid-circuit-bit handling, string-array defaults, and lazy
|
|
7
|
+
density inspection. The quantum engine is unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `qubasic --spec` now reports the full language surface: a `statements` section (QFT, MEAS, SYNDROME, EVOLVE, QADD, CTRL/INV, the control-flow keywords, ...) alongside commands, each with a `signature` field, plus `true_value: -1`.
|
|
11
|
+
- `DIM s$(n)` declares a string array; unread elements default to `""` instead of `0.0`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Comparisons return BASIC truth values: `-1` for true, `0` for false (so `LET t = (a > b)` is `-1`).
|
|
15
|
+
- Comparison chaining is Python-style, so `0 <= x <= 10` reads as `(0 <= x) AND (x <= 10)`.
|
|
16
|
+
- Reading a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) in a classical expression now raises a clear error instead of silently using the placeholder 0; `IF <bit>` feedforward is unaffected.
|
|
17
|
+
- `DENSITY` solves the density matrix lazily, on demand, instead of on every measured `SET_DENSITY` run.
|
|
18
|
+
|
|
19
|
+
## 0.13.0 (2026-06-19)
|
|
20
|
+
|
|
21
|
+
Closes the remaining audit gaps in the classic-BASIC layer (conventions,
|
|
22
|
+
inspection, and an agent contract). The quantum engine is unchanged.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `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.
|
|
26
|
+
- `REDIM PRESERVE name(n)` keeps existing elements; plain `REDIM` clears to zeros (QBASIC semantics).
|
|
27
|
+
- `DENSITY` shows the density matrix after a measured `SET_DENSITY` run, not only a no-MEASURE one.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `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.
|
|
31
|
+
- `round()` rounds half away from zero (`round(2.5)` = 3), not Python's banker's rounding.
|
|
32
|
+
- `STR$(n)` reserves a leading space for non-negative numbers (`STR$(42)` = " 42").
|
|
33
|
+
- 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)`.
|
|
34
|
+
- `PRINT`ing a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) shows "mid-circuit bit, resolved per shot" rather than the placeholder 0.
|
|
35
|
+
|
|
3
36
|
## 0.12.0 (2026-06-19)
|
|
4
37
|
|
|
5
38
|
Correctness and robustness pass on the classic-BASIC layer, from an extended
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
|
@@ -244,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
244
245
|
|
|
245
246
|
### Operators
|
|
246
247
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
247
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
248
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
248
249
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
249
250
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
250
251
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -256,12 +257,13 @@ 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)
|
|
259
|
-
REDIM data(20) Resize
|
|
260
|
+
REDIM data(20) Resize, clearing to zeros
|
|
261
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
260
262
|
ERASE data Delete array
|
|
261
263
|
OPTION BASE 1 Set array index base
|
|
262
264
|
```
|
|
263
265
|
|
|
264
|
-
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
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.
|
|
265
267
|
|
|
266
268
|
## Control flow
|
|
267
269
|
|
|
@@ -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
|
|
|
@@ -211,7 +212,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
211
212
|
|
|
212
213
|
### Operators
|
|
213
214
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
214
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
215
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
215
216
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
216
217
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
217
218
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -223,12 +224,13 @@ 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)
|
|
226
|
-
REDIM data(20) Resize
|
|
227
|
+
REDIM data(20) Resize, clearing to zeros
|
|
228
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
227
229
|
ERASE data Delete array
|
|
228
230
|
OPTION BASE 1 Set array index base
|
|
229
231
|
```
|
|
230
232
|
|
|
231
|
-
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
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.
|
|
232
234
|
|
|
233
235
|
## Control flow
|
|
234
236
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
|
@@ -244,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
244
245
|
|
|
245
246
|
### Operators
|
|
246
247
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
247
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
248
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
248
249
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
249
250
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
250
251
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -256,12 +257,13 @@ 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)
|
|
259
|
-
REDIM data(20) Resize
|
|
260
|
+
REDIM data(20) Resize, clearing to zeros
|
|
261
|
+
REDIM PRESERVE data(20) Resize, keeping existing data
|
|
260
262
|
ERASE data Delete array
|
|
261
263
|
OPTION BASE 1 Set array index base
|
|
262
264
|
```
|
|
263
265
|
|
|
264
|
-
A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
|
|
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.
|
|
265
267
|
|
|
266
268
|
## Control flow
|
|
267
269
|
|
|
@@ -109,9 +109,12 @@ 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
|
|
113
|
-
#
|
|
112
|
+
# A mixed state set via SET_DENSITY has no statevector; solve its
|
|
113
|
+
# density matrix on demand (cached) from the captured circuit.
|
|
114
114
|
dm = getattr(self, '_last_density', None)
|
|
115
|
+
if dm is None and getattr(self, '_last_density_qc', None) is not None:
|
|
116
|
+
dm = self._density_from_qc(self._last_density_qc)
|
|
117
|
+
self._last_density = dm
|
|
115
118
|
if dm is not None and not rest.strip():
|
|
116
119
|
rho = np.ascontiguousarray(dm)
|
|
117
120
|
dim = rho.shape[0]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
QUBASIC — command-line entry point.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
qubasic Interactive REPL (installed console script)
|
|
7
|
+
python -m qubasic_core Same, run as a module
|
|
8
|
+
qubasic script.qb Run a script file
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
# Force UTF-8 output on Windows
|
|
15
|
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
|
|
21
|
+
try:
|
|
22
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from qubasic_core.terminal import QBasicTerminal
|
|
27
|
+
from qubasic_core.program_mgmt import ProgramMgmtMixin
|
|
28
|
+
|
|
29
|
+
# Statement-level operations that are parsed inline rather than dispatched as
|
|
30
|
+
# REPL commands or gates, so they are absent from the command tables and the
|
|
31
|
+
# gate table. Listed in --spec (name, signature, description) for agents.
|
|
32
|
+
_SPEC_STATEMENTS = [
|
|
33
|
+
('MEAS', 'MEAS <qubit> -> <bit>', 'mid-circuit measurement into a classical bit (drives IF feedforward)'),
|
|
34
|
+
('MEASURE', 'MEASURE [qubit list]', 'measure all qubits, or a subset, into the result histogram'),
|
|
35
|
+
('MEASURE_X', 'MEASURE_X <qubit>', 'measure in the X basis (result in mx_<q>)'),
|
|
36
|
+
('MEASURE_Y', 'MEASURE_Y <qubit>', 'measure in the Y basis (result in my_<q>)'),
|
|
37
|
+
('MEASURE_Z', 'MEASURE_Z <qubit>', 'measure in the Z basis (result in mz_<q>)'),
|
|
38
|
+
('SYNDROME', 'SYNDROME <paulis> <qubits> -> <var>', 'non-destructive stabilizer measurement via an ancilla'),
|
|
39
|
+
('RESET', 'RESET <qubit>', 'reset a qubit to |0>'),
|
|
40
|
+
('BARRIER', 'BARRIER', 'optimization barrier'),
|
|
41
|
+
('QFT', 'QFT <lo>-<hi>', 'quantum Fourier transform over a qubit range'),
|
|
42
|
+
('IQFT', 'IQFT <lo>-<hi>', 'inverse quantum Fourier transform'),
|
|
43
|
+
('DIFFUSE', 'DIFFUSE <lo>-<hi>', 'Grover diffusion operator'),
|
|
44
|
+
('MCX', 'MCX <ctrl,...>, <target>', 'multi-controlled X'),
|
|
45
|
+
('MCZ', 'MCZ <ctrl,...>', 'multi-controlled Z'),
|
|
46
|
+
('MCP', 'MCP <theta>, <ctrl,...>, <target>', 'multi-controlled phase'),
|
|
47
|
+
('QADD', 'QADD <a-range>, <b-range>', 'in-place register add A += B (mod 2^n)'),
|
|
48
|
+
('QADDC', 'QADDC <k>, <range>', 'in-place constant add A += k (mod 2^n)'),
|
|
49
|
+
('QPE', 'QPE <range> <target> <UGATE>', 'quantum phase estimation of a unitary'),
|
|
50
|
+
('AMPLIFY', 'AMPLIFY <marked>', 'one amplitude-amplification (Grover) step'),
|
|
51
|
+
('GRAPHSTATE', 'GRAPHSTATE <a-b, b-c, ...>', 'prepare a graph/cluster state'),
|
|
52
|
+
('FEATUREMAP', 'FEATUREMAP <x0> <x1> ...', 'ZZ feature-map data encoding'),
|
|
53
|
+
('EVOLVE', 'EVOLVE <H>, <time>, <steps>', 'Trotterized Hamiltonian time evolution'),
|
|
54
|
+
('APPLYCHANNEL', 'APPLYCHANNEL <name> <qubit>', 'apply a defined Kraus channel'),
|
|
55
|
+
('SAVE_EXPECT', 'SAVE_EXPECT <obs> <qubits> -> <var>', 'record an expectation value into a variable after RUN'),
|
|
56
|
+
('SAVE_PROBS', 'SAVE_PROBS <qubits> -> <array>', 'record a probability snapshot into an array after RUN'),
|
|
57
|
+
('SAVE_AMPS', 'SAVE_AMPS <lo>,<hi> -> <array>', 'record amplitudes into an array after RUN'),
|
|
58
|
+
('CTRL', 'CTRL <gate> <ctrl>, <target>', 'controlled version of any gate'),
|
|
59
|
+
('INV', 'INV <gate> <args>', 'inverse/dagger of a gate'),
|
|
60
|
+
('UNITARY', 'UNITARY <NAME> = [[...]]', 'define a custom gate from a unitary matrix'),
|
|
61
|
+
('GOTO', 'GOTO <line>', 'jump to a line number'),
|
|
62
|
+
('GOSUB', 'GOSUB <line>', 'call a line block; RETURN resumes'),
|
|
63
|
+
('FOR', 'FOR <v> = <a> TO <b> [STEP <s>]', 'counted loop, closed by NEXT'),
|
|
64
|
+
('WHILE', 'WHILE <cond>', 'pre-test loop, closed by WEND'),
|
|
65
|
+
('DO', 'DO [WHILE|UNTIL <cond>]', 'loop, closed by LOOP [WHILE|UNTIL]'),
|
|
66
|
+
('IF', 'IF <cond> THEN <stmt> [ELSE <stmt>]', 'conditional (single-line, or block ending in END IF)'),
|
|
67
|
+
('SELECT', 'SELECT CASE <expr>', 'multi-way branch (CASE / CASE ELSE / END SELECT)'),
|
|
68
|
+
('DATA', 'DATA <v1>, <v2>, ...', 'inline data consumed by READ'),
|
|
69
|
+
('READ', 'READ <var>, ...', 'read the next DATA values'),
|
|
70
|
+
('DEF', 'DEF <NAME>[(p)] = <gates>', 'define a gate-sequence subroutine (also DEF FN, DEF BEGIN)'),
|
|
71
|
+
('SUB', 'SUB <name>(<args>)', 'structured subroutine (END SUB; LOCAL/STATIC/SHARED)'),
|
|
72
|
+
('FUNCTION', 'FUNCTION <name>(<args>)', 'function returning a value (END FUNCTION)'),
|
|
73
|
+
('DIM', 'DIM <name>(<size>[,...])', 'declare an array (name$ for strings; inclusive sizing)'),
|
|
74
|
+
('REDIM', 'REDIM [PRESERVE] <name>(<size>)', 'resize an array; PRESERVE keeps existing data'),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_script(path: str, terminal: 'QBasicTerminal') -> None:
|
|
79
|
+
"""Run a .qb script file. Supports multi-line DEF blocks.
|
|
80
|
+
|
|
81
|
+
After loading all lines, auto-runs the program if it contains
|
|
82
|
+
numbered lines with a MEASURE statement.
|
|
83
|
+
"""
|
|
84
|
+
with open(path, 'r') as f:
|
|
85
|
+
lines = [l.rstrip('\n\r') for l in f.readlines()]
|
|
86
|
+
ProgramMgmtMixin._load_lines_with_defs(
|
|
87
|
+
lines, lambda line: terminal.process(line, track_undo=False))
|
|
88
|
+
|
|
89
|
+
# Auto-run if the program has a reachable MEASURE (incl. in subs / IF
|
|
90
|
+
# clauses), unless the script already issued an explicit RUN (which would
|
|
91
|
+
# otherwise execute and print the program twice).
|
|
92
|
+
explicit_run = any(l.strip().upper() == 'RUN' for l in lines)
|
|
93
|
+
if (terminal.program and not explicit_run
|
|
94
|
+
and terminal._program_has_measure(sorted(terminal.program))):
|
|
95
|
+
terminal.cmd_run()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
import json as _json
|
|
100
|
+
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
|
101
|
+
|
|
102
|
+
from qubasic_core import __version__
|
|
103
|
+
|
|
104
|
+
args = sys.argv[1:]
|
|
105
|
+
quiet = '--quiet' in args or '-q' in args
|
|
106
|
+
json_mode = '--json' in args
|
|
107
|
+
agent_mode = '--agent' in args
|
|
108
|
+
spec_mode = '--spec' in args
|
|
109
|
+
seed_val = None
|
|
110
|
+
for flag in ('--quiet', '-q', '--json', '--agent', '--spec'):
|
|
111
|
+
args = [a for a in args if a != flag]
|
|
112
|
+
# Parse --seed N
|
|
113
|
+
filtered = []
|
|
114
|
+
i = 0
|
|
115
|
+
while i < len(args):
|
|
116
|
+
if args[i] == '--seed' and i + 1 < len(args):
|
|
117
|
+
seed_val = int(args[i + 1])
|
|
118
|
+
i += 2
|
|
119
|
+
else:
|
|
120
|
+
filtered.append(args[i])
|
|
121
|
+
i += 1
|
|
122
|
+
args = filtered
|
|
123
|
+
|
|
124
|
+
if any(a in ('-v', '--version') for a in args):
|
|
125
|
+
print(f"QUBASIC {__version__}")
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
if any(a in ('-h', '--help') for a in args):
|
|
129
|
+
print(f"QUBASIC {__version__} — Quantum BASIC Interactive Terminal")
|
|
130
|
+
print()
|
|
131
|
+
print("Usage:")
|
|
132
|
+
print(" qubasic Interactive REPL")
|
|
133
|
+
print(" qubasic script.qb Run a script file")
|
|
134
|
+
print(" qubasic --quiet script Suppress banner and progress (also -q)")
|
|
135
|
+
print(" qubasic --json script Output results as JSON")
|
|
136
|
+
print(" qubasic --agent script Confine file writes to the working dir")
|
|
137
|
+
print(" qubasic --seed N script Set random seed for reproducibility")
|
|
138
|
+
print(" qubasic --version Show version (also -v)")
|
|
139
|
+
print(" qubasic --help Show this help (also -h)")
|
|
140
|
+
print(" python -m qubasic_core Run without the installed console script")
|
|
141
|
+
print()
|
|
142
|
+
print("Type HELP inside the REPL for full command reference.")
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
if spec_mode:
|
|
146
|
+
# Machine-readable contract for agents: commands, gates, functions,
|
|
147
|
+
# constants, and conventions for the installed version.
|
|
148
|
+
from qubasic_core.engine import GATE_TABLE
|
|
149
|
+
from qubasic_core.expression import ExpressionMixin
|
|
150
|
+
|
|
151
|
+
def _doc_parts(mname):
|
|
152
|
+
doc = (getattr(getattr(QBasicTerminal, mname, None), '__doc__', '') or '').strip()
|
|
153
|
+
line = doc.split('\n')[0].strip()
|
|
154
|
+
# Docstrings format the first line as "SIGNATURE — description".
|
|
155
|
+
for sep in ('—', ' - '):
|
|
156
|
+
if sep in line:
|
|
157
|
+
sig, _, hlp = line.partition(sep)
|
|
158
|
+
return sig.strip(), hlp.strip()
|
|
159
|
+
return '', line
|
|
160
|
+
|
|
161
|
+
cmds = []
|
|
162
|
+
for tbl, takes in ((QBasicTerminal._CMD_WITH_ARG, True),
|
|
163
|
+
(QBasicTerminal._CMD_NO_ARG, False)):
|
|
164
|
+
for cname, mname in tbl.items():
|
|
165
|
+
sig, hlp = _doc_parts(mname)
|
|
166
|
+
cmds.append({'name': cname, 'takes_arg': takes,
|
|
167
|
+
'signature': sig or cname, 'help': hlp})
|
|
168
|
+
statements = [{'name': n, 'signature': s, 'help': h}
|
|
169
|
+
for (n, s, h) in _SPEC_STATEMENTS]
|
|
170
|
+
spec = {
|
|
171
|
+
'name': 'qubasic',
|
|
172
|
+
'version': __version__,
|
|
173
|
+
'bit_order': 'little-endian (qubit 0 = rightmost bit)',
|
|
174
|
+
'true_value': -1,
|
|
175
|
+
'commands': sorted(cmds, key=lambda c: c['name']),
|
|
176
|
+
'statements': sorted(statements, key=lambda s: s['name']),
|
|
177
|
+
'gates': sorted(GATE_TABLE.keys()),
|
|
178
|
+
'functions': sorted(
|
|
179
|
+
set(ExpressionMixin._SAFE_FUNCS)
|
|
180
|
+
| {'RND', 'TIMER', 'POS', 'PEEK', 'USR', 'EOF', 'FRE',
|
|
181
|
+
'LEFT$', 'RIGHT$', 'MID$', 'CHR$', 'STR$', 'HEX$', 'BIN$',
|
|
182
|
+
'ASC', 'VAL', 'INSTR', 'LEN'}),
|
|
183
|
+
'constants': sorted(ExpressionMixin._SAFE_CONSTS.keys()),
|
|
184
|
+
}
|
|
185
|
+
print(_json.dumps(spec, indent=2))
|
|
186
|
+
sys.exit(0)
|
|
187
|
+
|
|
188
|
+
term = QBasicTerminal()
|
|
189
|
+
# JSON mode implies agent use, so confine file writes to the working dir.
|
|
190
|
+
term.agent_mode = agent_mode or json_mode
|
|
191
|
+
if seed_val is not None:
|
|
192
|
+
import numpy as _np
|
|
193
|
+
term._seed = seed_val
|
|
194
|
+
_np.random.seed(seed_val)
|
|
195
|
+
|
|
196
|
+
if args:
|
|
197
|
+
path = args[0]
|
|
198
|
+
if not os.path.isfile(path):
|
|
199
|
+
if json_mode:
|
|
200
|
+
print(_json.dumps({'error': f'FILE NOT FOUND: {path}'}, indent=2))
|
|
201
|
+
else:
|
|
202
|
+
print(f"?FILE NOT FOUND: {path}")
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
if quiet or json_mode:
|
|
205
|
+
import io
|
|
206
|
+
buf = io.StringIO()
|
|
207
|
+
old = sys.stdout
|
|
208
|
+
sys.stdout = buf
|
|
209
|
+
err = None
|
|
210
|
+
try:
|
|
211
|
+
run_script(path, term)
|
|
212
|
+
except Exception as e: # surface as structured error in JSON mode
|
|
213
|
+
err = str(e)
|
|
214
|
+
finally:
|
|
215
|
+
sys.stdout = old
|
|
216
|
+
if json_mode:
|
|
217
|
+
if err is not None:
|
|
218
|
+
print(_json.dumps({'error': err}, indent=2))
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
print(_json.dumps(term.result(), indent=2))
|
|
221
|
+
else:
|
|
222
|
+
# --quiet (no --json): the banner was never emitted into the
|
|
223
|
+
# buffer (only the non-captured path calls print_banner), so the
|
|
224
|
+
# captured text is exactly the results. Print them, matching the
|
|
225
|
+
# documented "suppress banner, output results only" behavior.
|
|
226
|
+
print(buf.getvalue(), end='')
|
|
227
|
+
if err is not None:
|
|
228
|
+
print(f"?ERROR: {err}")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
else:
|
|
231
|
+
term.print_banner()
|
|
232
|
+
run_script(path, term)
|
|
233
|
+
# Exit 0 on success; 1 if a measured program produced no counts.
|
|
234
|
+
expects_measure = bool(term.program) and term._program_has_measure(sorted(term.program))
|
|
235
|
+
sys.exit(0 if term.last_counts is not None or not expects_measure else 1)
|
|
236
|
+
else:
|
|
237
|
+
term.repl()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == '__main__':
|
|
241
|
+
main()
|
|
@@ -102,9 +102,10 @@ class ControlFlowMixin:
|
|
|
102
102
|
if idx < 0:
|
|
103
103
|
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
|
|
104
104
|
dimmed = getattr(self, '_dimmed_arrays', set())
|
|
105
|
+
fill = '' if name.endswith('$') else 0.0 # string arrays default to ""
|
|
105
106
|
if name not in self.arrays:
|
|
106
107
|
# Implicit array: created (and allowed to grow) on first assignment.
|
|
107
|
-
self.arrays[name] = [
|
|
108
|
+
self.arrays[name] = [fill] * (idx + 1)
|
|
108
109
|
elif idx >= len(self.arrays[name]):
|
|
109
110
|
if name in dimmed:
|
|
110
111
|
# Explicitly DIMmed: writes are bounds-checked like reads,
|
|
@@ -113,7 +114,7 @@ class ControlFlowMixin:
|
|
|
113
114
|
f"ARRAY INDEX OUT OF RANGE: {name}({idx + base}), "
|
|
114
115
|
f"size {len(self.arrays[name])}")
|
|
115
116
|
while idx >= len(self.arrays[name]):
|
|
116
|
-
self.arrays[name].append(
|
|
117
|
+
self.arrays[name].append(fill)
|
|
117
118
|
self.arrays[name][idx] = val
|
|
118
119
|
return True, ExecResult.ADVANCE
|
|
119
120
|
|
|
@@ -184,6 +185,12 @@ class ControlFlowMixin:
|
|
|
184
185
|
# Quoted literal: emit verbatim (no substitution, no SPC/TAB).
|
|
185
186
|
if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
|
|
186
187
|
return item[1:-1]
|
|
188
|
+
# A mid-circuit measurement bit (MEAS/SYNDROME -> var) has no single
|
|
189
|
+
# value in standard mode; it is resolved per shot inside the if_test.
|
|
190
|
+
# Show that instead of the placeholder 0.
|
|
191
|
+
cb = getattr(self, '_classical_bits', None)
|
|
192
|
+
if cb and item in cb:
|
|
193
|
+
return f"<{item}: mid-circuit bit, resolved per shot>"
|
|
187
194
|
text = self._substitute_vars(item, run_vars)
|
|
188
195
|
|
|
189
196
|
def _spaces(m):
|
|
@@ -57,8 +57,10 @@ class Engine:
|
|
|
57
57
|
# Arrays declared with DIM/REDIM enforce their bounds on write;
|
|
58
58
|
# arrays created implicitly by first assignment keep auto-growing.
|
|
59
59
|
self._dimmed_arrays: set[str] = set()
|
|
60
|
-
# Density matrix
|
|
60
|
+
# Density matrix (and the measure-free circuit to solve it lazily)
|
|
61
|
+
# for a run with SET_DENSITY, computed on demand by DENSITY.
|
|
61
62
|
self._last_density: Any = None
|
|
63
|
+
self._last_density_qc: Any = None
|
|
62
64
|
|
|
63
65
|
# Subroutines and registers
|
|
64
66
|
self.subroutines: dict[str, Any] = {}
|
|
@@ -119,6 +121,7 @@ class Engine:
|
|
|
119
121
|
self._array_dims.clear()
|
|
120
122
|
self._dimmed_arrays.clear()
|
|
121
123
|
self._last_density = None
|
|
124
|
+
self._last_density_qc = None
|
|
122
125
|
self.last_counts = None
|
|
123
126
|
self.last_sv = None
|
|
124
127
|
self.last_circuit = None
|
|
@@ -113,8 +113,10 @@ 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
|
|
116
|
+
# Density matrix (and circuit) captured by a SET_DENSITY run; the
|
|
117
|
+
# matrix is solved lazily by DENSITY from the stored circuit.
|
|
117
118
|
self._last_density = None
|
|
119
|
+
self._last_density_qc = None
|
|
118
120
|
# Apply any qubit state preparation requested via POKE to $0100.
|
|
119
121
|
if getattr(self, '_poke_state_prep', None):
|
|
120
122
|
self._emit_poke_state_prep(qc)
|
|
@@ -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 = {
|
|
@@ -178,6 +190,15 @@ class ExpressionMixin:
|
|
|
178
190
|
return node.value
|
|
179
191
|
raise ValueError(f"UNSUPPORTED CONSTANT: {node.value!r}")
|
|
180
192
|
if isinstance(node, ast.Name):
|
|
193
|
+
# A mid-circuit measurement bit has no classical value (it is
|
|
194
|
+
# resolved per shot); reading it outside an IF feedforward is an
|
|
195
|
+
# error rather than a silent placeholder 0.
|
|
196
|
+
cb = getattr(self, '_classical_bits', None)
|
|
197
|
+
if cb and node.id in cb:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"'{node.id}' is a mid-circuit measurement bit; its value is "
|
|
200
|
+
f"per-shot, so it can't be read in a classical expression "
|
|
201
|
+
f"(use IF {node.id} for feedforward, or LOCC mode for a live value)")
|
|
181
202
|
if node.id in ns:
|
|
182
203
|
return ns[node.id]
|
|
183
204
|
raise ValueError(f"UNDEFINED: {node.id}")
|
|
@@ -203,6 +224,9 @@ class ExpressionMixin:
|
|
|
203
224
|
result = op(result, self._ast_eval(val, ns))
|
|
204
225
|
return result
|
|
205
226
|
if isinstance(node, ast.Compare):
|
|
227
|
+
# Python-style chaining (a < b < c means (a<b) and (b<c)) so range
|
|
228
|
+
# checks like 0 <= x <= 10 work as written; results use BASIC truth
|
|
229
|
+
# values (-1 for true, 0 for false).
|
|
206
230
|
left = self._ast_eval(node.left, ns)
|
|
207
231
|
for op_node, comparator in zip(node.ops, node.comparators):
|
|
208
232
|
op = self._AST_OPS.get(type(op_node))
|
|
@@ -210,9 +234,9 @@ class ExpressionMixin:
|
|
|
210
234
|
raise ValueError(f"UNSUPPORTED OP: {type(op_node).__name__}")
|
|
211
235
|
right = self._ast_eval(comparator, ns)
|
|
212
236
|
if not op(left, right):
|
|
213
|
-
return
|
|
237
|
+
return 0
|
|
214
238
|
left = right
|
|
215
|
-
return
|
|
239
|
+
return -1
|
|
216
240
|
if isinstance(node, ast.Call):
|
|
217
241
|
if not isinstance(node.func, ast.Name):
|
|
218
242
|
raise ValueError("ONLY SIMPLE FUNCTION CALLS ALLOWED")
|
|
@@ -17,8 +17,8 @@ RE_SHARE = re.compile(r'SHARE\s+([A-Z])\s+(\d+)\s*,?\s*([A-Z])\s+(\d+)', re.IGNO
|
|
|
17
17
|
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
|
-
RE_DIM = re.compile(r'DIM\s+(\w
|
|
21
|
-
RE_REDIM = re.compile(r'REDIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
|
|
20
|
+
RE_DIM = re.compile(r'DIM\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)
|
|
@@ -100,7 +100,7 @@ RE_SCREEN = re.compile(r'SCREEN\s+(\d+)', re.IGNORECASE)
|
|
|
100
100
|
RE_LPRINT = re.compile(r'LPRINT\s+(.*)', re.IGNORECASE)
|
|
101
101
|
RE_ON_MEASURE = re.compile(r'ON\s+MEASURE\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
102
102
|
RE_ON_TIMER = re.compile(r'ON\s+TIMER\s*\((\d+)\)\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
103
|
-
RE_DIM_MULTI = re.compile(r'DIM\s+(\w
|
|
103
|
+
RE_DIM_MULTI = re.compile(r'DIM\s+(\w+\$?)\((\d+(?:\s*,\s*\d+)*)\)', re.IGNORECASE)
|
|
104
104
|
RE_LET_STR = re.compile(r'LET\s+(\w+\$)\s*=\s*(.*)', re.IGNORECASE)
|
|
105
105
|
# Implicit LET: an assignment written without the LET keyword (x = 5, s$ = "hi",
|
|
106
106
|
# a(1) = 5, p.x = 3). Anchored lvalue followed by a single '=' that is not part
|
|
@@ -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:
|
|
@@ -1244,6 +1244,28 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1244
1244
|
# it skip extraction; STATE/BLOCH then report no state.
|
|
1245
1245
|
_SV_EXTRACT_MAX_QUBITS = 24
|
|
1246
1246
|
|
|
1247
|
+
def _density_from_qc(self, qc_sv):
|
|
1248
|
+
"""Solve the density matrix of a measure-free circuit, for DENSITY.
|
|
1249
|
+
|
|
1250
|
+
Computed on demand (not on every run) from the circuit captured when a
|
|
1251
|
+
SET_DENSITY state was prepared.
|
|
1252
|
+
"""
|
|
1253
|
+
try:
|
|
1254
|
+
q2 = qc_sv.copy()
|
|
1255
|
+
q2.save_density_matrix()
|
|
1256
|
+
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1257
|
+
_kw = {}
|
|
1258
|
+
if self._seed is not None:
|
|
1259
|
+
_kw['seed_simulator'] = self._seed
|
|
1260
|
+
res = dm_backend.run(
|
|
1261
|
+
transpile(q2, dm_backend, optimization_level=self._transpile_opt_level),
|
|
1262
|
+
**_kw).result()
|
|
1263
|
+
data = res.data(0)
|
|
1264
|
+
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1265
|
+
return np.array(dm) if dm is not None else None
|
|
1266
|
+
except Exception:
|
|
1267
|
+
return None
|
|
1268
|
+
|
|
1247
1269
|
def _extract_statevector(self, qc_sv) -> None:
|
|
1248
1270
|
"""Run the measurement-free circuit copy to get last_sv.
|
|
1249
1271
|
|
|
@@ -1255,7 +1277,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1255
1277
|
2^n statevector that cannot be displayed anyway.
|
|
1256
1278
|
"""
|
|
1257
1279
|
if getattr(self, '_pending_set_density', None) is not None:
|
|
1258
|
-
|
|
1280
|
+
# Mixed state: no pure statevector. Defer the density-matrix solve
|
|
1281
|
+
# to DENSITY (lazy) so a measured run does not pay for it unused;
|
|
1282
|
+
# keep the measure-free circuit to solve from.
|
|
1283
|
+
self.last_sv = None
|
|
1284
|
+
self._last_density = None
|
|
1285
|
+
self._last_density_qc = (qc_sv if self.num_qubits <= self._SV_EXTRACT_MAX_QUBITS
|
|
1286
|
+
else None)
|
|
1259
1287
|
return
|
|
1260
1288
|
if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
|
|
1261
1289
|
self.last_sv = None
|
|
@@ -1300,20 +1328,12 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1300
1328
|
"""Execute the no-MEASURE path: statevector (or density matrix), no shots."""
|
|
1301
1329
|
too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
|
|
1302
1330
|
if getattr(self, '_pending_set_density', None) is not None and not too_large:
|
|
1303
|
-
# A mixed state has no statevector
|
|
1304
|
-
#
|
|
1305
|
-
# untranslatable-circuit error from Aer.
|
|
1331
|
+
# A mixed state has no statevector. Defer the density solve to
|
|
1332
|
+
# DENSITY (lazy); keep the measure-free circuit. Saving a statevector
|
|
1333
|
+
# here used to raise an untranslatable-circuit error from Aer.
|
|
1306
1334
|
self.last_sv = None
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
|
1335
|
+
self._last_density = None
|
|
1336
|
+
self._last_density_qc = qc_sv
|
|
1317
1337
|
elif too_large:
|
|
1318
1338
|
self.last_sv = None
|
|
1319
1339
|
else:
|
|
@@ -1943,13 +1963,17 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1943
1963
|
if not m:
|
|
1944
1964
|
return False
|
|
1945
1965
|
name = m.group(1)
|
|
1966
|
+
base = getattr(self, '_option_base', 0)
|
|
1946
1967
|
dims = [int(d.strip()) for d in m.group(2).split(',')]
|
|
1968
|
+
# Inclusive sizing (QBASIC): DIM a(n) spans indices base..n, so the
|
|
1969
|
+
# declared top index n is valid (n - base + 1 slots per dimension).
|
|
1970
|
+
sizes = [max(0, d - base + 1) for d in dims]
|
|
1947
1971
|
total = 1
|
|
1948
|
-
for
|
|
1949
|
-
total *=
|
|
1950
|
-
self.arrays[name] = [0.0] * total
|
|
1951
|
-
if len(
|
|
1952
|
-
self._array_dims[name] =
|
|
1972
|
+
for s in sizes:
|
|
1973
|
+
total *= s
|
|
1974
|
+
self.arrays[name] = [('' if name.endswith('$') else 0.0)] * total
|
|
1975
|
+
if len(sizes) > 1:
|
|
1976
|
+
self._array_dims[name] = sizes
|
|
1953
1977
|
self._dimmed_arrays.add(name)
|
|
1954
1978
|
return True
|
|
1955
1979
|
|
|
@@ -1975,20 +1999,26 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1975
1999
|
return True
|
|
1976
2000
|
|
|
1977
2001
|
def _try_exec_redim(self, stmt: str) -> bool:
|
|
1978
|
-
"""Handle REDIM name(size) — resize an
|
|
2002
|
+
"""Handle REDIM [PRESERVE] name(size) — resize an array.
|
|
2003
|
+
|
|
2004
|
+
Plain REDIM clears to zeros (QBASIC semantics); REDIM PRESERVE keeps the
|
|
2005
|
+
existing elements. Sizing is inclusive (REDIM a(n) spans indices base..n).
|
|
2006
|
+
"""
|
|
1979
2007
|
m = RE_REDIM.match(stmt)
|
|
1980
2008
|
if not m:
|
|
1981
2009
|
return False
|
|
1982
|
-
|
|
1983
|
-
|
|
2010
|
+
preserve = bool(m.group(1))
|
|
2011
|
+
name = m.group(2)
|
|
2012
|
+
base = getattr(self, '_option_base', 0)
|
|
2013
|
+
new_len = max(0, int(m.group(3)) - base + 1)
|
|
1984
2014
|
old = self.arrays.get(name, [])
|
|
1985
|
-
if isinstance(old, list):
|
|
1986
|
-
if
|
|
1987
|
-
self.arrays[name] = old + [0.0] * (
|
|
2015
|
+
if preserve and isinstance(old, list):
|
|
2016
|
+
if new_len > len(old):
|
|
2017
|
+
self.arrays[name] = old + [0.0] * (new_len - len(old))
|
|
1988
2018
|
else:
|
|
1989
|
-
self.arrays[name] = old[:
|
|
2019
|
+
self.arrays[name] = old[:new_len]
|
|
1990
2020
|
else:
|
|
1991
|
-
self.arrays[name] = [0.0] *
|
|
2021
|
+
self.arrays[name] = [0.0] * new_len
|
|
1992
2022
|
self._dimmed_arrays.add(name)
|
|
1993
2023
|
return True
|
|
1994
2024
|
|
|
@@ -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
|
# =====================================================================
|
|
@@ -2138,7 +2138,9 @@ class TestDeepFixRegressions(unittest.TestCase):
|
|
|
2138
2138
|
t.process('10 ID 0', track_undo=False)
|
|
2139
2139
|
_, out = capture(t.cmd_run)
|
|
2140
2140
|
self.assertNotIn('Unable to translate', out)
|
|
2141
|
-
|
|
2141
|
+
# Density is solved lazily: the measure-free circuit is captured now,
|
|
2142
|
+
# and DENSITY computes the matrix on demand.
|
|
2143
|
+
self.assertIsNotNone(getattr(t, '_last_density_qc', None))
|
|
2142
2144
|
_, dout = capture(t.cmd_density, '')
|
|
2143
2145
|
self.assertIn('Density matrix', dout)
|
|
2144
2146
|
|
|
@@ -2152,6 +2154,79 @@ class TestDeepFixRegressions(unittest.TestCase):
|
|
|
2152
2154
|
self.assertEqual(t.eval_expr('sq(5)'), 25.0)
|
|
2153
2155
|
|
|
2154
2156
|
|
|
2157
|
+
class TestConventionFixesV2(unittest.TestCase):
|
|
2158
|
+
"""round half-away, chained-comparison guard, inclusive DIM, REDIM
|
|
2159
|
+
PRESERVE, MEAS print hint, DENSITY after MEASURE."""
|
|
2160
|
+
|
|
2161
|
+
def setUp(self):
|
|
2162
|
+
self.t = QBasicTerminal(); self.t.num_qubits = 1
|
|
2163
|
+
|
|
2164
|
+
def _runp(self, lines, nq=1):
|
|
2165
|
+
t = QBasicTerminal(); t.num_qubits = nq
|
|
2166
|
+
for l in lines:
|
|
2167
|
+
t.process(l, track_undo=False)
|
|
2168
|
+
_, out = capture(t.cmd_run)
|
|
2169
|
+
t._out = out
|
|
2170
|
+
return t
|
|
2171
|
+
|
|
2172
|
+
def test_round_half_away_from_zero(self):
|
|
2173
|
+
self.assertEqual(self.t.eval_expr('round(2.5)'), 3.0)
|
|
2174
|
+
self.assertEqual(self.t.eval_expr('round(3.5)'), 4.0)
|
|
2175
|
+
self.assertEqual(self.t.eval_expr('round(-2.5)'), -3.0)
|
|
2176
|
+
self.assertAlmostEqual(self.t.eval_expr('round(2.345, 2)'), 2.35)
|
|
2177
|
+
|
|
2178
|
+
def test_comparison_truth_and_chaining(self):
|
|
2179
|
+
# Comparisons yield BASIC truth values: -1 for true, 0 for false.
|
|
2180
|
+
self.assertEqual(self.t._safe_eval('3 < 5'), -1)
|
|
2181
|
+
self.assertEqual(self.t._safe_eval('3 > 5'), 0)
|
|
2182
|
+
# Python-style chaining, so range checks work as written.
|
|
2183
|
+
self.assertEqual(self.t._safe_eval('0 <= 5 <= 10'), -1)
|
|
2184
|
+
self.assertEqual(self.t._safe_eval('0 <= 50 <= 10'), 0)
|
|
2185
|
+
|
|
2186
|
+
def test_dim_inclusive_top_index(self):
|
|
2187
|
+
t = self._runp(['10 DIM a(5)', '20 LET a(5)=7', '30 LET v=a(5)'])
|
|
2188
|
+
self.assertEqual(t.variables.get('v'), 7.0)
|
|
2189
|
+
t2 = self._runp(['10 DIM a(5)', '20 LET a(6)=1'])
|
|
2190
|
+
self.assertIn('OUT OF RANGE', t2._out)
|
|
2191
|
+
|
|
2192
|
+
def test_redim_clear_vs_preserve(self):
|
|
2193
|
+
t = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM a(5)', '40 LET v=a(1)'])
|
|
2194
|
+
self.assertEqual(t.variables.get('v'), 0.0)
|
|
2195
|
+
t2 = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM PRESERVE a(5)', '40 LET v=a(1)'])
|
|
2196
|
+
self.assertEqual(t2.variables.get('v'), 9.0)
|
|
2197
|
+
|
|
2198
|
+
def test_meas_print_hint(self):
|
|
2199
|
+
t = self._runp(['10 X 0', '20 MEAS 0 -> m', '30 PRINT m', '40 MEASURE'])
|
|
2200
|
+
self.assertIn('mid-circuit', t._out)
|
|
2201
|
+
|
|
2202
|
+
def test_density_after_measure(self):
|
|
2203
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2204
|
+
t.process('SET_DENSITY [[0.5,0],[0,0.5]]', track_undo=False)
|
|
2205
|
+
for l in ['10 H 0', '20 MEASURE']:
|
|
2206
|
+
t.process(l, track_undo=False)
|
|
2207
|
+
capture(t.cmd_run)
|
|
2208
|
+
_, dout = capture(t.cmd_density, '')
|
|
2209
|
+
self.assertIn('Density matrix', dout)
|
|
2210
|
+
|
|
2211
|
+
def test_meas_bit_classical_use_errors(self):
|
|
2212
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2213
|
+
for l in ['10 X 0', '20 MEAS 0 -> m', '30 LET y = m + 1', '40 MEASURE']:
|
|
2214
|
+
t.process(l, track_undo=False)
|
|
2215
|
+
_, out = capture(t.cmd_run)
|
|
2216
|
+
self.assertIn('mid-circuit', out)
|
|
2217
|
+
|
|
2218
|
+
def test_string_array_default_empty(self):
|
|
2219
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2220
|
+
t.process('DIM s$(3)', track_undo=False)
|
|
2221
|
+
self.assertEqual(t._safe_eval('s$(1)'), '')
|
|
2222
|
+
|
|
2223
|
+
def test_spec_statements_present(self):
|
|
2224
|
+
from qubasic_core.cli import _SPEC_STATEMENTS
|
|
2225
|
+
names = {n for (n, _s, _h) in _SPEC_STATEMENTS}
|
|
2226
|
+
for op in ('QFT', 'MEAS', 'SYNDROME', 'EVOLVE', 'QADD'):
|
|
2227
|
+
self.assertIn(op, names)
|
|
2228
|
+
|
|
2229
|
+
|
|
2155
2230
|
if __name__ == '__main__':
|
|
2156
2231
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
2157
2232
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
QUBASIC — command-line entry point.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
qubasic Interactive REPL (installed console script)
|
|
7
|
-
python -m qubasic_core Same, run as a module
|
|
8
|
-
qubasic script.qb Run a script file
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import sys
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
# Force UTF-8 output on Windows
|
|
15
|
-
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
|
16
|
-
try:
|
|
17
|
-
sys.stdout.reconfigure(encoding='utf-8')
|
|
18
|
-
except Exception:
|
|
19
|
-
pass
|
|
20
|
-
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
|
|
21
|
-
try:
|
|
22
|
-
sys.stderr.reconfigure(encoding='utf-8')
|
|
23
|
-
except Exception:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
from qubasic_core.terminal import QBasicTerminal
|
|
27
|
-
from qubasic_core.program_mgmt import ProgramMgmtMixin
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def run_script(path: str, terminal: 'QBasicTerminal') -> None:
|
|
31
|
-
"""Run a .qb script file. Supports multi-line DEF blocks.
|
|
32
|
-
|
|
33
|
-
After loading all lines, auto-runs the program if it contains
|
|
34
|
-
numbered lines with a MEASURE statement.
|
|
35
|
-
"""
|
|
36
|
-
with open(path, 'r') as f:
|
|
37
|
-
lines = [l.rstrip('\n\r') for l in f.readlines()]
|
|
38
|
-
ProgramMgmtMixin._load_lines_with_defs(
|
|
39
|
-
lines, lambda line: terminal.process(line, track_undo=False))
|
|
40
|
-
|
|
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))):
|
|
47
|
-
terminal.cmd_run()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def main():
|
|
51
|
-
import json as _json
|
|
52
|
-
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
|
53
|
-
|
|
54
|
-
from qubasic_core import __version__
|
|
55
|
-
|
|
56
|
-
args = sys.argv[1:]
|
|
57
|
-
quiet = '--quiet' in args or '-q' in args
|
|
58
|
-
json_mode = '--json' in args
|
|
59
|
-
agent_mode = '--agent' in args
|
|
60
|
-
seed_val = None
|
|
61
|
-
for flag in ('--quiet', '-q', '--json', '--agent'):
|
|
62
|
-
args = [a for a in args if a != flag]
|
|
63
|
-
# Parse --seed N
|
|
64
|
-
filtered = []
|
|
65
|
-
i = 0
|
|
66
|
-
while i < len(args):
|
|
67
|
-
if args[i] == '--seed' and i + 1 < len(args):
|
|
68
|
-
seed_val = int(args[i + 1])
|
|
69
|
-
i += 2
|
|
70
|
-
else:
|
|
71
|
-
filtered.append(args[i])
|
|
72
|
-
i += 1
|
|
73
|
-
args = filtered
|
|
74
|
-
|
|
75
|
-
if any(a in ('-v', '--version') for a in args):
|
|
76
|
-
print(f"QUBASIC {__version__}")
|
|
77
|
-
sys.exit(0)
|
|
78
|
-
|
|
79
|
-
if any(a in ('-h', '--help') for a in args):
|
|
80
|
-
print(f"QUBASIC {__version__} — Quantum BASIC Interactive Terminal")
|
|
81
|
-
print()
|
|
82
|
-
print("Usage:")
|
|
83
|
-
print(" qubasic Interactive REPL")
|
|
84
|
-
print(" qubasic script.qb Run a script file")
|
|
85
|
-
print(" qubasic --quiet script Suppress banner and progress (also -q)")
|
|
86
|
-
print(" qubasic --json script Output results as JSON")
|
|
87
|
-
print(" qubasic --agent script Confine file writes to the working dir")
|
|
88
|
-
print(" qubasic --seed N script Set random seed for reproducibility")
|
|
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")
|
|
92
|
-
print()
|
|
93
|
-
print("Type HELP inside the REPL for full command reference.")
|
|
94
|
-
sys.exit(0)
|
|
95
|
-
|
|
96
|
-
term = QBasicTerminal()
|
|
97
|
-
# JSON mode implies agent use, so confine file writes to the working dir.
|
|
98
|
-
term.agent_mode = agent_mode or json_mode
|
|
99
|
-
if seed_val is not None:
|
|
100
|
-
import numpy as _np
|
|
101
|
-
term._seed = seed_val
|
|
102
|
-
_np.random.seed(seed_val)
|
|
103
|
-
|
|
104
|
-
if args:
|
|
105
|
-
path = args[0]
|
|
106
|
-
if not os.path.isfile(path):
|
|
107
|
-
if json_mode:
|
|
108
|
-
print(_json.dumps({'error': f'FILE NOT FOUND: {path}'}, indent=2))
|
|
109
|
-
else:
|
|
110
|
-
print(f"?FILE NOT FOUND: {path}")
|
|
111
|
-
sys.exit(1)
|
|
112
|
-
if quiet or json_mode:
|
|
113
|
-
import io
|
|
114
|
-
buf = io.StringIO()
|
|
115
|
-
old = sys.stdout
|
|
116
|
-
sys.stdout = buf
|
|
117
|
-
err = None
|
|
118
|
-
try:
|
|
119
|
-
run_script(path, term)
|
|
120
|
-
except Exception as e: # surface as structured error in JSON mode
|
|
121
|
-
err = str(e)
|
|
122
|
-
finally:
|
|
123
|
-
sys.stdout = old
|
|
124
|
-
if json_mode:
|
|
125
|
-
if err is not None:
|
|
126
|
-
print(_json.dumps({'error': err}, indent=2))
|
|
127
|
-
sys.exit(1)
|
|
128
|
-
print(_json.dumps(term.result(), indent=2))
|
|
129
|
-
else:
|
|
130
|
-
# --quiet (no --json): the banner was never emitted into the
|
|
131
|
-
# buffer (only the non-captured path calls print_banner), so the
|
|
132
|
-
# captured text is exactly the results. Print them, matching the
|
|
133
|
-
# documented "suppress banner, output results only" behavior.
|
|
134
|
-
print(buf.getvalue(), end='')
|
|
135
|
-
if err is not None:
|
|
136
|
-
print(f"?ERROR: {err}")
|
|
137
|
-
sys.exit(1)
|
|
138
|
-
else:
|
|
139
|
-
term.print_banner()
|
|
140
|
-
run_script(path, term)
|
|
141
|
-
# Exit 0 on success; 1 if a measured program produced no counts.
|
|
142
|
-
expects_measure = bool(term.program) and term._program_has_measure(sorted(term.program))
|
|
143
|
-
sys.exit(0 if term.last_counts is not None or not expects_measure else 1)
|
|
144
|
-
else:
|
|
145
|
-
term.repl()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if __name__ == '__main__':
|
|
149
|
-
main()
|
|
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
|