qubasic 0.10.0__tar.gz → 0.11.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.10.0 → qubasic-0.11.0}/CHANGELOG.md +28 -0
- {qubasic-0.10.0/qubasic.egg-info → qubasic-0.11.0}/PKG-INFO +11 -5
- {qubasic-0.10.0 → qubasic-0.11.0}/README.md +10 -4
- {qubasic-0.10.0 → qubasic-0.11.0}/pyproject.toml +1 -1
- {qubasic-0.10.0 → qubasic-0.11.0/qubasic.egg-info}/PKG-INFO +11 -5
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/control_flow.py +24 -8
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/display.py +21 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/expression.py +71 -8
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/parser.py +10 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/patterns.py +6 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/strings.py +12 -14
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/terminal.py +110 -5
- {qubasic-0.10.0 → qubasic-0.11.0}/tests/test_qubasic.py +131 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/LICENSE +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/MANIFEST.in +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/examples/bell.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/examples/grover3.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/algos2.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/analysis.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/cli.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/demos.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/engine_state.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/executor.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/help_text.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/memory.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qec.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qudits.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/resources.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/subs.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/setup.cfg +0 -0
- {qubasic-0.10.0 → qubasic-0.11.0}/tests/test_features.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Convention-conformance and observability pass: make the language behave the way
|
|
6
|
+
a Qiskit/Python/BASIC user already expects, and make hidden state legible. The
|
|
7
|
+
quantum engine is unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `STATUS` (and `STATUS JSON`): dumps every active mode (qubits, shots, method, device, seed, `OPTION BASE`, LOCC mode, noise, coupling/basis, pending `SET_STATE`/`SET_DENSITY`, bank) so behavior can be read instead of inferred.
|
|
11
|
+
- Implicit `LET`: a bare assignment works without the keyword (`x = 5`, `s$ = "hi"`, `a(1) = 5`), as in every BASIC.
|
|
12
|
+
- `FIX(x)` truncates toward zero, complementing `INT`.
|
|
13
|
+
- Histograms print a `q(n-1) ... q1 q0` bit-order header and the JSON result carries a `bit_order` field, making the little-endian convention (qubit 0 = rightmost) explicit.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `INT(x)` floors toward negative infinity (`INT(-3.2)` = -4), matching QBASIC; use `FIX` for truncation toward zero.
|
|
17
|
+
- Built-in constants (`PI`, `E`, `TAU`, `SQRT2`) are reserved: a same-named variable can no longer silently shadow them, and assigning one is an error.
|
|
18
|
+
- `MEAS` without a target bit reports a clear error distinguishing it from `MEASURE` (mid-circuit measurement into a classical bit vs collapse into the histogram) instead of `UNKNOWN GATE`.
|
|
19
|
+
|
|
20
|
+
## 0.10.1 (2026-06-19)
|
|
21
|
+
|
|
22
|
+
Expression, string, and PRINT fixes in the classic-BASIC layer. The quantum
|
|
23
|
+
engine is unchanged.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Math functions resolve in any case: `SQRT`, `SIN`, `ABS`, `INT`, ... now work as well as their lowercase spellings, matching the rest of the language. Runtime and string functions (`RND`, `LEFT$`, ...) are likewise case-insensitive.
|
|
27
|
+
- `AND`, `OR`, `NOT`, `XOR`, and `<>` work in every expression context (`LET`, `PRINT`, gate parameters), not only in `IF` conditions. `AND`/`OR`/`XOR` are bitwise on integers (`6 AND 3` = 2) while preserving operator precedence below comparison, so `IF a > b AND c > d` still groups correctly; `NOT` is logical.
|
|
28
|
+
- String variables are fully usable: `LET s$ = "foo" + "bar"`, `LET t$ = LEFT$(s$, 3)`, and other computed strings store their real value in both immediate and program mode, instead of being mistaken for a quoted literal or forced through `float()`.
|
|
29
|
+
- `PRINT` surfaces evaluation errors instead of silently printing the unevaluated source text. Assigning a string to a numeric variable reports a `TYPE MISMATCH` rather than crashing on a float conversion.
|
|
30
|
+
|
|
3
31
|
## 0.10.0 (2026-06-19)
|
|
4
32
|
|
|
5
33
|
Completes the frontier set: fault-tolerant QEC extras and a compilation/resources
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -209,6 +209,8 @@ QUBITS 8 Set qubit count (1-32)
|
|
|
209
209
|
SHOTS 2048 Set measurement shots
|
|
210
210
|
METHOD statevector Set simulation method
|
|
211
211
|
METHOD GPU Set simulation device
|
|
212
|
+
STATUS Show every active mode (qubits, method, LOCC, noise, ...)
|
|
213
|
+
STATUS JSON Same, as machine-readable JSON
|
|
212
214
|
```
|
|
213
215
|
|
|
214
216
|
### Simulation methods
|
|
@@ -220,17 +222,19 @@ Automatic selection: stabilizer for Clifford-only circuits, MPS for >28 qubits,
|
|
|
220
222
|
|
|
221
223
|
```
|
|
222
224
|
LET angle = PI/4
|
|
223
|
-
|
|
225
|
+
x = sin(angle) * 2 LET is optional (x = 5 also works)
|
|
224
226
|
10 RX angle, 0 Use in gate parameters
|
|
225
227
|
VARS List all variables
|
|
226
228
|
CLEAR x Remove a variable
|
|
227
229
|
```
|
|
228
230
|
|
|
231
|
+
Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
232
|
+
|
|
229
233
|
### Constants
|
|
230
|
-
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False`
|
|
234
|
+
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False` (reserved; not usable as variable names)
|
|
231
235
|
|
|
232
236
|
### Math functions
|
|
233
|
-
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int
|
|
237
|
+
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int` (floors), `fix` (truncates), `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
|
|
234
238
|
|
|
235
239
|
### Runtime functions
|
|
236
240
|
`RND(x)` random, `TIMER` elapsed seconds, `FRE(0)` free RAM bytes, `POS(0)` cursor column, `PEEK(addr)` memory read, `USR(addr)` call routine
|
|
@@ -242,7 +246,7 @@ CLEAR x Remove a variable
|
|
|
242
246
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
243
247
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
244
248
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
245
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
249
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
246
250
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
247
251
|
|
|
248
252
|
## Arrays
|
|
@@ -364,6 +368,8 @@ DECOMPOSE Gate count breakdown
|
|
|
364
368
|
DENSITY Density matrix
|
|
365
369
|
```
|
|
366
370
|
|
|
371
|
+
Bitstrings are little-endian: qubit 0 is the rightmost character. Histograms print a `q(n-1) ... q1 q0` header so the mapping is explicit.
|
|
372
|
+
|
|
367
373
|
### Screen modes
|
|
368
374
|
```
|
|
369
375
|
SCREEN 0 Text (default)
|
|
@@ -176,6 +176,8 @@ QUBITS 8 Set qubit count (1-32)
|
|
|
176
176
|
SHOTS 2048 Set measurement shots
|
|
177
177
|
METHOD statevector Set simulation method
|
|
178
178
|
METHOD GPU Set simulation device
|
|
179
|
+
STATUS Show every active mode (qubits, method, LOCC, noise, ...)
|
|
180
|
+
STATUS JSON Same, as machine-readable JSON
|
|
179
181
|
```
|
|
180
182
|
|
|
181
183
|
### Simulation methods
|
|
@@ -187,17 +189,19 @@ Automatic selection: stabilizer for Clifford-only circuits, MPS for >28 qubits,
|
|
|
187
189
|
|
|
188
190
|
```
|
|
189
191
|
LET angle = PI/4
|
|
190
|
-
|
|
192
|
+
x = sin(angle) * 2 LET is optional (x = 5 also works)
|
|
191
193
|
10 RX angle, 0 Use in gate parameters
|
|
192
194
|
VARS List all variables
|
|
193
195
|
CLEAR x Remove a variable
|
|
194
196
|
```
|
|
195
197
|
|
|
198
|
+
Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
199
|
+
|
|
196
200
|
### Constants
|
|
197
|
-
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False`
|
|
201
|
+
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False` (reserved; not usable as variable names)
|
|
198
202
|
|
|
199
203
|
### Math functions
|
|
200
|
-
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int
|
|
204
|
+
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int` (floors), `fix` (truncates), `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
|
|
201
205
|
|
|
202
206
|
### Runtime functions
|
|
203
207
|
`RND(x)` random, `TIMER` elapsed seconds, `FRE(0)` free RAM bytes, `POS(0)` cursor column, `PEEK(addr)` memory read, `USR(addr)` call routine
|
|
@@ -209,7 +213,7 @@ CLEAR x Remove a variable
|
|
|
209
213
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
210
214
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
211
215
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
212
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
216
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
213
217
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
214
218
|
|
|
215
219
|
## Arrays
|
|
@@ -331,6 +335,8 @@ DECOMPOSE Gate count breakdown
|
|
|
331
335
|
DENSITY Density matrix
|
|
332
336
|
```
|
|
333
337
|
|
|
338
|
+
Bitstrings are little-endian: qubit 0 is the rightmost character. Histograms print a `q(n-1) ... q1 q0` header so the mapping is explicit.
|
|
339
|
+
|
|
334
340
|
### Screen modes
|
|
335
341
|
```
|
|
336
342
|
SCREEN 0 Text (default)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -209,6 +209,8 @@ QUBITS 8 Set qubit count (1-32)
|
|
|
209
209
|
SHOTS 2048 Set measurement shots
|
|
210
210
|
METHOD statevector Set simulation method
|
|
211
211
|
METHOD GPU Set simulation device
|
|
212
|
+
STATUS Show every active mode (qubits, method, LOCC, noise, ...)
|
|
213
|
+
STATUS JSON Same, as machine-readable JSON
|
|
212
214
|
```
|
|
213
215
|
|
|
214
216
|
### Simulation methods
|
|
@@ -220,17 +222,19 @@ Automatic selection: stabilizer for Clifford-only circuits, MPS for >28 qubits,
|
|
|
220
222
|
|
|
221
223
|
```
|
|
222
224
|
LET angle = PI/4
|
|
223
|
-
|
|
225
|
+
x = sin(angle) * 2 LET is optional (x = 5 also works)
|
|
224
226
|
10 RX angle, 0 Use in gate parameters
|
|
225
227
|
VARS List all variables
|
|
226
228
|
CLEAR x Remove a variable
|
|
227
229
|
```
|
|
228
230
|
|
|
231
|
+
Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
232
|
+
|
|
229
233
|
### Constants
|
|
230
|
-
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False`
|
|
234
|
+
`PI`, `TAU`, `E`, `SQRT2`, `True`, `False` (reserved; not usable as variable names)
|
|
231
235
|
|
|
232
236
|
### Math functions
|
|
233
|
-
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int
|
|
237
|
+
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sqrt`, `log`, `exp`, `abs`, `int` (floors), `fix` (truncates), `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
|
|
234
238
|
|
|
235
239
|
### Runtime functions
|
|
236
240
|
`RND(x)` random, `TIMER` elapsed seconds, `FRE(0)` free RAM bytes, `POS(0)` cursor column, `PEEK(addr)` memory read, `USR(addr)` call routine
|
|
@@ -242,7 +246,7 @@ CLEAR x Remove a variable
|
|
|
242
246
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
243
247
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
244
248
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
245
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
249
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
246
250
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
247
251
|
|
|
248
252
|
## Arrays
|
|
@@ -364,6 +368,8 @@ DECOMPOSE Gate count breakdown
|
|
|
364
368
|
DENSITY Density matrix
|
|
365
369
|
```
|
|
366
370
|
|
|
371
|
+
Bitstrings are little-endian: qubit 0 is the rightmost character. Histograms print a `q(n-1) ... q1 q0` header so the mapping is explicit.
|
|
372
|
+
|
|
367
373
|
### Screen modes
|
|
368
374
|
```
|
|
369
375
|
SCREEN 0 Text (default)
|
|
@@ -12,7 +12,7 @@ from qubasic_core.engine import ExecResult, ExecOutcome
|
|
|
12
12
|
from qubasic_core.parser import parse_stmt
|
|
13
13
|
from qubasic_core.statements import (
|
|
14
14
|
RawStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt, WendStmt,
|
|
15
|
-
LetArrayStmt, LetStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
15
|
+
LetArrayStmt, LetStmt, LetStrStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
16
16
|
ForStmt, NextStmt, WhileStmt, IfThenStmt,
|
|
17
17
|
DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
|
|
18
18
|
SelectCaseStmt, CaseStmt, EndSelectStmt, ElseStmt, EndIfStmt,
|
|
@@ -73,6 +73,7 @@ class ControlFlowMixin:
|
|
|
73
73
|
def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
|
|
74
74
|
parsed: LetArrayStmt) -> tuple[bool, ExecOutcome]:
|
|
75
75
|
name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
|
|
76
|
+
self._assert_assignable(name)
|
|
76
77
|
base = getattr(self, '_option_base', 0)
|
|
77
78
|
val = self._eval_with_vars(val_expr, run_vars)
|
|
78
79
|
parts = self._split_arg_list(idx_expr)
|
|
@@ -106,7 +107,21 @@ class ControlFlowMixin:
|
|
|
106
107
|
def _cf_let_var(self, stmt: str, run_vars: dict[str, Any],
|
|
107
108
|
parsed: LetStmt) -> tuple[bool, ExecOutcome]:
|
|
108
109
|
name, expr = parsed.name, parsed.expr
|
|
109
|
-
|
|
110
|
+
self._assert_assignable(name)
|
|
111
|
+
raw = self._safe_eval(expr, extra_ns=run_vars)
|
|
112
|
+
if isinstance(raw, str):
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
f"TYPE MISMATCH: '{name}' is numeric; use '{name}$' for strings")
|
|
115
|
+
val = float(raw)
|
|
116
|
+
run_vars[name] = val
|
|
117
|
+
self.variables[name] = val
|
|
118
|
+
return True, ExecResult.ADVANCE
|
|
119
|
+
|
|
120
|
+
def _cf_let_str(self, stmt: str, run_vars: dict[str, Any],
|
|
121
|
+
parsed: LetStrStmt) -> tuple[bool, ExecOutcome]:
|
|
122
|
+
"""LET v$ = <expr> — assign a string (or number) to a string variable."""
|
|
123
|
+
name, expr = parsed.name, parsed.expr
|
|
124
|
+
val = self._eval_string_expr(expr, run_vars)
|
|
110
125
|
run_vars[name] = val
|
|
111
126
|
self.variables[name] = val
|
|
112
127
|
return True, ExecResult.ADVANCE
|
|
@@ -167,12 +182,12 @@ class ControlFlowMixin:
|
|
|
167
182
|
text = re.sub(r'\bTAB\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
|
|
168
183
|
if not text.strip():
|
|
169
184
|
return text # standalone SPC/TAB -> whitespace
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
|
|
186
|
+
run_vars if isinstance(run_vars, dict) else dict(run_vars))
|
|
187
|
+
# Surface evaluation errors instead of silently printing the raw source
|
|
188
|
+
# text (which used to turn PRINT SQRT(9) into the literal "SQRT(9)" and
|
|
189
|
+
# an undefined variable into its own name).
|
|
190
|
+
return str(self._safe_eval(text, extra_ns=ns))
|
|
176
191
|
|
|
177
192
|
def _cf_print(self, stmt: str, run_vars: dict[str, Any],
|
|
178
193
|
parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
|
|
@@ -400,6 +415,7 @@ class ControlFlowMixin:
|
|
|
400
415
|
WendStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_wend(rv, ls, sl, ip),
|
|
401
416
|
LetArrayStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_array(st, rv, p),
|
|
402
417
|
LetStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_var(st, rv, p),
|
|
418
|
+
LetStrStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_str(st, rv, p),
|
|
403
419
|
PrintStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_print(st, rv, p),
|
|
404
420
|
GotoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_goto(st, sl, p),
|
|
405
421
|
GosubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_gosub(st, sl, ip, p),
|
|
@@ -40,12 +40,33 @@ class DisplayMixin:
|
|
|
40
40
|
Console(file=buf, highlight=False, force_terminal=force).print(*renderables)
|
|
41
41
|
self.io.write(buf.getvalue())
|
|
42
42
|
|
|
43
|
+
def _bit_order_note(self, display: list) -> str | None:
|
|
44
|
+
"""Human-facing reminder of which bit is which qubit.
|
|
45
|
+
|
|
46
|
+
Bitstrings are little-endian (qubit 0 is the rightmost character), the
|
|
47
|
+
most common source of off-by-reverse mistakes when reading a histogram.
|
|
48
|
+
When the result covers the whole register the positions are labelled
|
|
49
|
+
q(n-1) ... q1 q0; for a measured subset only the convention is shown.
|
|
50
|
+
"""
|
|
51
|
+
if not display:
|
|
52
|
+
return None
|
|
53
|
+
nbits = len(display[0][0])
|
|
54
|
+
nq = getattr(self, 'num_qubits', nbits)
|
|
55
|
+
if nbits == nq and nbits <= 16:
|
|
56
|
+
labels = ' '.join(f'q{i}' for i in range(nbits - 1, -1, -1))
|
|
57
|
+
return f" bit order {labels} (qubit 0 = rightmost)"
|
|
58
|
+
return " bit order qubit 0 = rightmost bit"
|
|
59
|
+
|
|
43
60
|
def print_histogram(self, counts: dict[str, int]) -> None:
|
|
44
61
|
"""Measurement histogram with optional rich-table formatting."""
|
|
45
62
|
total = sum(counts.values())
|
|
46
63
|
sorted_counts = sorted(counts.items(), key=lambda x: -x[1])
|
|
47
64
|
display = sorted_counts[:MAX_HISTOGRAM_STATES]
|
|
48
65
|
|
|
66
|
+
note = self._bit_order_note(display)
|
|
67
|
+
if note:
|
|
68
|
+
self.io.writeln(note)
|
|
69
|
+
|
|
49
70
|
if _RICH:
|
|
50
71
|
self._print_histogram_rich(display, sorted_counts, total)
|
|
51
72
|
else:
|
|
@@ -94,6 +94,36 @@ def _rewrite_logical_outside_strings(cond: str) -> str:
|
|
|
94
94
|
return ''.join(parts)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _as_int(x: Any) -> int:
|
|
98
|
+
"""Coerce a BASIC value to int for bitwise AND/OR/XOR.
|
|
99
|
+
|
|
100
|
+
QBasic rounds non-integers; truth values and non-numerics fold to 1/0 so
|
|
101
|
+
``(a > 0) AND (b > 0)`` works the same as ``6 AND 3``.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(x, bool):
|
|
104
|
+
return int(x)
|
|
105
|
+
if isinstance(x, int):
|
|
106
|
+
return x
|
|
107
|
+
if isinstance(x, float):
|
|
108
|
+
return int(round(x))
|
|
109
|
+
return 1 if x else 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _basic_and(a: Any, b: Any) -> int:
|
|
113
|
+
"""BASIC AND — bitwise on integers, and correct for truth values
|
|
114
|
+
(1 & 1 == 1, 1 & 0 == 0). Parsed from ``and`` so precedence stays below
|
|
115
|
+
comparison: ``a > b AND c > d`` groups as ``(a>b) AND (c>d)``."""
|
|
116
|
+
return _as_int(a) & _as_int(b)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _basic_or(a: Any, b: Any) -> int:
|
|
120
|
+
return _as_int(a) | _as_int(b)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _basic_xor(a: Any, b: Any) -> int:
|
|
124
|
+
return _as_int(a) ^ _as_int(b)
|
|
125
|
+
|
|
126
|
+
|
|
97
127
|
class ExpressionMixin:
|
|
98
128
|
"""AST-based safe expression evaluation. No eval().
|
|
99
129
|
|
|
@@ -105,7 +135,9 @@ class ExpressionMixin:
|
|
|
105
135
|
'asin': math.asin, 'acos': math.acos, 'atan': math.atan,
|
|
106
136
|
'atan2': math.atan2,
|
|
107
137
|
'sqrt': math.sqrt, 'log': math.log, 'exp': math.exp,
|
|
108
|
-
|
|
138
|
+
# INT floors toward negative infinity, as in QBASIC (INT(-3.2) = -4).
|
|
139
|
+
# FIX truncates toward zero (FIX(-3.2) = -3) for the other convention.
|
|
140
|
+
'abs': abs, 'int': math.floor, 'fix': math.trunc, 'float': float,
|
|
109
141
|
'min': min, 'max': max, 'round': round, 'len': len,
|
|
110
142
|
'ceil': math.ceil, 'floor': math.floor,
|
|
111
143
|
}
|
|
@@ -126,11 +158,14 @@ class ExpressionMixin:
|
|
|
126
158
|
ast.Eq: operator.eq, ast.NotEq: operator.ne,
|
|
127
159
|
ast.Lt: operator.lt, ast.LtE: operator.le,
|
|
128
160
|
ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ast.
|
|
133
|
-
ast.
|
|
161
|
+
# AND/OR/XOR are BASIC's bitwise-logical operators (6 AND 3 == 2),
|
|
162
|
+
# robust to float operands; NOT stays logical so IF NOT flag works
|
|
163
|
+
# with 0/1 truth values.
|
|
164
|
+
ast.And: _basic_and,
|
|
165
|
+
ast.Or: _basic_or,
|
|
166
|
+
ast.BitAnd: _basic_and,
|
|
167
|
+
ast.BitOr: _basic_or,
|
|
168
|
+
ast.BitXor: _basic_xor,
|
|
134
169
|
ast.Invert: operator.invert,
|
|
135
170
|
}
|
|
136
171
|
|
|
@@ -237,6 +272,13 @@ class ExpressionMixin:
|
|
|
237
272
|
ns['FRE'] = lambda x=0: psutil.virtual_memory().available
|
|
238
273
|
except ImportError:
|
|
239
274
|
ns['FRE'] = lambda x=0: 0
|
|
275
|
+
# BASIC is case-insensitive for built-in functions and constants, so
|
|
276
|
+
# register upper- and lower-case aliases for every builtin (SQRT and
|
|
277
|
+
# sqrt, RND and rnd, LEFT$ and left$). Variables are merged with their
|
|
278
|
+
# own case in _safe_eval and shadow these.
|
|
279
|
+
for _name in list(ns.keys()):
|
|
280
|
+
ns.setdefault(_name.upper(), ns[_name])
|
|
281
|
+
ns.setdefault(_name.lower(), ns[_name])
|
|
240
282
|
self._base_ns = ns
|
|
241
283
|
return ns
|
|
242
284
|
|
|
@@ -301,9 +343,17 @@ class ExpressionMixin:
|
|
|
301
343
|
expr_str = str(expr).strip()
|
|
302
344
|
# Normalize FN prefix: "FN square(x)" -> "square(x)"
|
|
303
345
|
expr_str = re.sub(r'\bFN\s+(\w+)\s*\(', r'\1(', expr_str, flags=re.IGNORECASE)
|
|
346
|
+
# Rewrite BASIC logical/relational operators (AND, OR, NOT, XOR, <>) to
|
|
347
|
+
# the Python forms the AST understands, only outside quoted strings.
|
|
348
|
+
# Applied to every expression (LET, PRINT, gate params), not just IF
|
|
349
|
+
# conditions, so the documented operators work everywhere.
|
|
350
|
+
expr_str = _rewrite_logical_outside_strings(expr_str)
|
|
304
351
|
# Rewrite numeric literals (&H, &B, $hex addresses) and the string
|
|
305
352
|
# sigil, each only outside quoted string literals.
|
|
306
353
|
expr_str = _replace_dollar_outside_strings(expr_str)
|
|
354
|
+
# The operator rewrite can pad a leading token (NOT x -> " not x"); strip
|
|
355
|
+
# so ast.parse(mode='eval') does not reject it as an unexpected indent.
|
|
356
|
+
expr_str = expr_str.strip()
|
|
307
357
|
if not expr_str:
|
|
308
358
|
raise ValueError("EMPTY EXPRESSION")
|
|
309
359
|
try:
|
|
@@ -351,10 +401,23 @@ class ExpressionMixin:
|
|
|
351
401
|
return float(self._safe_eval(expr, extra_ns=run_vars))
|
|
352
402
|
|
|
353
403
|
def _eval_condition(self, cond: str, run_vars: dict[str, Any]) -> bool:
|
|
354
|
-
"""Evaluate a boolean condition.
|
|
355
|
-
|
|
404
|
+
"""Evaluate a boolean condition.
|
|
405
|
+
|
|
406
|
+
The operator rewrite (AND/OR/NOT/XOR/<>) now happens inside _safe_eval,
|
|
407
|
+
so conditions and ordinary expressions share one consistent path.
|
|
408
|
+
"""
|
|
356
409
|
return bool(self._safe_eval(cond, extra_ns=run_vars))
|
|
357
410
|
|
|
411
|
+
# Built-in constant names that may not be used as variable/array names, so a
|
|
412
|
+
# value like E never silently shadows (or is shadowed by) the constant.
|
|
413
|
+
_RESERVED_CONST_NAMES = frozenset({'PI', 'TAU', 'E', 'SQRT2', 'TRUE', 'FALSE'})
|
|
414
|
+
|
|
415
|
+
def _assert_assignable(self, name: str) -> None:
|
|
416
|
+
"""Reject assignment to a built-in constant name (case-insensitive)."""
|
|
417
|
+
if name.upper() in self._RESERVED_CONST_NAMES:
|
|
418
|
+
raise ValueError(
|
|
419
|
+
f"RESERVED: '{name}' is a built-in constant; choose another name")
|
|
420
|
+
|
|
358
421
|
def _run_timer(self) -> float:
|
|
359
422
|
"""Return elapsed time since terminal start."""
|
|
360
423
|
return time.time() - getattr(self, '_start_time', time.time())
|
|
@@ -22,6 +22,7 @@ from qubasic_core.patterns import (
|
|
|
22
22
|
RE_LPRINT, RE_SCREEN, RE_COLOR, RE_LOCATE,
|
|
23
23
|
RE_ON_MEASURE, RE_ON_TIMER, RE_IMPORT, RE_CHAIN, RE_MERGE,
|
|
24
24
|
RE_LET_STR, RE_DIM_MULTI, RE_MEASURE_BASIS, RE_SYNDROME,
|
|
25
|
+
RE_IMPLICIT_ASSIGN,
|
|
25
26
|
)
|
|
26
27
|
from qubasic_core.statements import (
|
|
27
28
|
Stmt, RawStmt, GateStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt,
|
|
@@ -578,6 +579,15 @@ def parse_stmt(raw: str) -> Stmt:
|
|
|
578
579
|
if len(parts) > 1:
|
|
579
580
|
return CompoundStmt(raw=raw, parts=tuple(parts))
|
|
580
581
|
|
|
582
|
+
# ── Implicit LET (assignment without the LET keyword) ──────────
|
|
583
|
+
# A line whose lvalue is followed by a single '=' is an assignment, even
|
|
584
|
+
# when it collides with a gate name (X = 5 is "assign X", not the X gate).
|
|
585
|
+
# Tried before gate dispatch so bare assignment works as in every BASIC.
|
|
586
|
+
if RE_IMPLICIT_ASSIGN.match(text):
|
|
587
|
+
result = _handle_let(f"LET {text}", raw)
|
|
588
|
+
if result is not None:
|
|
589
|
+
return result
|
|
590
|
+
|
|
581
591
|
# ── Gate application ────────────────────────────────────────────
|
|
582
592
|
canonical = GATE_ALIASES.get(first_word, first_word)
|
|
583
593
|
if canonical in GATE_TABLE:
|
|
@@ -102,6 +102,11 @@ 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
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
|
+
# Implicit LET: an assignment written without the LET keyword (x = 5, s$ = "hi",
|
|
106
|
+
# a(1) = 5, p.x = 3). Anchored lvalue followed by a single '=' that is not part
|
|
107
|
+
# of '==' / '<=' / '>=' / '!=' / '<>'. Routed through the LET handlers.
|
|
108
|
+
RE_IMPLICIT_ASSIGN = re.compile(
|
|
109
|
+
r'[A-Za-z_]\w*\$?(?:\.\w+)?(?:\([^)]*\))?\s*=(?!=)', re.IGNORECASE)
|
|
105
110
|
|
|
106
111
|
__all__ = [
|
|
107
112
|
"RE_LINE_NUM",
|
|
@@ -185,4 +190,5 @@ __all__ = [
|
|
|
185
190
|
"RE_ON_TIMER",
|
|
186
191
|
"RE_DIM_MULTI",
|
|
187
192
|
"RE_LET_STR",
|
|
193
|
+
"RE_IMPLICIT_ASSIGN",
|
|
188
194
|
]
|
|
@@ -84,21 +84,19 @@ class StringMixin:
|
|
|
84
84
|
ns[k] = v
|
|
85
85
|
return ns
|
|
86
86
|
|
|
87
|
-
def _eval_string_expr(self, expr: str) -> str | float:
|
|
88
|
-
"""Evaluate an expression that
|
|
87
|
+
def _eval_string_expr(self, expr: str, run_vars: dict[str, Any] | None = None) -> str | float:
|
|
88
|
+
"""Evaluate an expression that may return a string or a number.
|
|
89
|
+
|
|
90
|
+
Uses the shared AST evaluator so concatenation (``"a" + "b"``), string
|
|
91
|
+
functions (``LEFT$(s$, 3)``), and string variables resolve to their real
|
|
92
|
+
values. A multi-part expression like ``"a" + "b"`` is no longer mistaken
|
|
93
|
+
for a single quoted literal, and a string result is no longer forced
|
|
94
|
+
through float(). Errors surface instead of silently returning the source.
|
|
95
|
+
"""
|
|
89
96
|
expr = expr.strip()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return expr[1:-1]
|
|
94
|
-
# String variable
|
|
95
|
-
if expr in self.variables and isinstance(self.variables[expr], str):
|
|
96
|
-
return self.variables[expr]
|
|
97
|
-
# Try numeric
|
|
98
|
-
try:
|
|
99
|
-
return self.eval_expr(expr)
|
|
100
|
-
except Exception:
|
|
101
|
-
return expr
|
|
97
|
+
if run_vars is not None:
|
|
98
|
+
return self._safe_eval(expr, extra_ns=run_vars)
|
|
99
|
+
return self._safe_eval(expr)
|
|
102
100
|
|
|
103
101
|
def cmd_let_str(self, name: str, expr: str) -> None:
|
|
104
102
|
"""Assign a string value to a string variable."""
|
|
@@ -508,6 +508,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
508
508
|
'LIST': 'cmd_list', 'QUBITS': 'cmd_qubits', 'SHOTS': 'cmd_shots',
|
|
509
509
|
'METHOD': 'cmd_method', 'DEF': 'cmd_def', 'REG': 'cmd_reg',
|
|
510
510
|
'LET': 'cmd_let', 'STATE': 'cmd_state', 'BLOCH': 'cmd_bloch',
|
|
511
|
+
'STATUS': 'cmd_status',
|
|
511
512
|
'DEMO': 'cmd_demo', 'DELETE': 'cmd_delete', 'RENUM': 'cmd_renum',
|
|
512
513
|
'SAVE': 'cmd_save', 'LOAD': 'cmd_load', 'SWEEP': 'cmd_sweep',
|
|
513
514
|
'INCLUDE': 'cmd_include', 'EXPORT': 'cmd_export',
|
|
@@ -593,6 +594,17 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
593
594
|
except Exception as e:
|
|
594
595
|
self.io.writeln(f"?ERROR: {e}")
|
|
595
596
|
else:
|
|
597
|
+
# Implicit LET: a bare assignment (x = 5, s$ = "hi") routed to the
|
|
598
|
+
# LET handler so it works without the keyword, as in every BASIC.
|
|
599
|
+
from qubasic_core.patterns import RE_IMPLICIT_ASSIGN
|
|
600
|
+
if RE_IMPLICIT_ASSIGN.match(line.strip()):
|
|
601
|
+
try:
|
|
602
|
+
self.cmd_let(line.strip())
|
|
603
|
+
except QBasicError as e:
|
|
604
|
+
self.io.writeln(f"?{e.message}")
|
|
605
|
+
except Exception as e:
|
|
606
|
+
self.io.writeln(f"?ERROR: {e}")
|
|
607
|
+
return
|
|
596
608
|
# Try as immediate gate / subroutine
|
|
597
609
|
try:
|
|
598
610
|
self.run_immediate(line)
|
|
@@ -1044,9 +1056,10 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1044
1056
|
|
|
1045
1057
|
def cmd_let(self, rest: str) -> None:
|
|
1046
1058
|
"""LET <var> = <expr> — assign a computed value to a variable.
|
|
1047
|
-
Supports record fields (LET p.x = 3.14)
|
|
1048
|
-
(LET a(0) = PI, LET m(i, j) = x), matching the
|
|
1049
|
-
|
|
1059
|
+
Supports string variables (LET s$ = "hi"), record fields (LET p.x = 3.14)
|
|
1060
|
+
and array elements (LET a(0) = PI, LET m(i, j) = x), matching the
|
|
1061
|
+
in-program LET."""
|
|
1062
|
+
from qubasic_core.patterns import RE_LET_ARRAY, RE_LET_STR
|
|
1050
1063
|
am = RE_LET_ARRAY.match(f"LET {rest}")
|
|
1051
1064
|
if am:
|
|
1052
1065
|
from qubasic_core.statements import LetArrayStmt
|
|
@@ -1056,12 +1069,22 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1056
1069
|
self._cf_let_array(f"LET {rest}", self.variables, parsed)
|
|
1057
1070
|
self.io.writeln(f"{name}({idx_expr.strip()}) = {self.eval_expr(val_expr)}")
|
|
1058
1071
|
return
|
|
1072
|
+
sm = RE_LET_STR.match(f"LET {rest}")
|
|
1073
|
+
if sm:
|
|
1074
|
+
self.cmd_let_str(sm.group(1), sm.group(2))
|
|
1075
|
+
return
|
|
1059
1076
|
m = re.match(r'(\w+(?:\.\w+)?)\s*=\s*(.*)', rest)
|
|
1060
1077
|
if not m:
|
|
1061
1078
|
self.io.writeln("?USAGE: LET <var> = <expr>")
|
|
1062
1079
|
return
|
|
1063
1080
|
name = m.group(1)
|
|
1064
|
-
|
|
1081
|
+
self._assert_assignable(name)
|
|
1082
|
+
raw = self._safe_eval(m.group(2))
|
|
1083
|
+
if isinstance(raw, str):
|
|
1084
|
+
self.io.writeln(
|
|
1085
|
+
f"?TYPE MISMATCH: '{name}' is numeric — use '{name}$' for strings")
|
|
1086
|
+
return
|
|
1087
|
+
val = float(raw)
|
|
1065
1088
|
self.variables[name] = val
|
|
1066
1089
|
if '.' in name: # mirror into the record dict for LIST VARS
|
|
1067
1090
|
base, field = name.split('.', 1)
|
|
@@ -1070,6 +1093,78 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1070
1093
|
rec[field] = val
|
|
1071
1094
|
self.io.writeln(f"{name} = {val}")
|
|
1072
1095
|
|
|
1096
|
+
def _status_dict(self) -> dict:
|
|
1097
|
+
"""Snapshot every active mode that silently changes how a line behaves.
|
|
1098
|
+
|
|
1099
|
+
Returned as plain JSON-serializable values so a caller can read context
|
|
1100
|
+
instead of inferring it from prior commands.
|
|
1101
|
+
"""
|
|
1102
|
+
cmap = getattr(self, '_coupling_map', None)
|
|
1103
|
+
return {
|
|
1104
|
+
'qubits': self.num_qubits,
|
|
1105
|
+
'shots': self.shots,
|
|
1106
|
+
'method': self.sim_method,
|
|
1107
|
+
'device': self.sim_device,
|
|
1108
|
+
'seed': getattr(self, '_seed', None),
|
|
1109
|
+
'bit_order': 'little-endian (qubit 0 = rightmost bit)',
|
|
1110
|
+
'option_base': getattr(self, '_option_base', 0),
|
|
1111
|
+
'locc_mode': bool(getattr(self, 'locc_mode', False)),
|
|
1112
|
+
'locc_registers': (list(self.locc.names)
|
|
1113
|
+
if getattr(self, 'locc_mode', False) and getattr(self, 'locc', None)
|
|
1114
|
+
else []),
|
|
1115
|
+
'noise_active': getattr(self, '_noise_model', None) is not None,
|
|
1116
|
+
'noise_spec': getattr(self, '_noise_spec', None),
|
|
1117
|
+
'coupling_map': ([list(e) for e in cmap] if cmap else None),
|
|
1118
|
+
'basis_gates': getattr(self, '_basis_gates', None),
|
|
1119
|
+
'pending_set_state': getattr(self, '_pending_set_state', None) is not None,
|
|
1120
|
+
'pending_set_density': getattr(self, '_pending_set_density', None) is not None,
|
|
1121
|
+
'screen_mode': getattr(self, '_screen_mode', 0),
|
|
1122
|
+
'trace_mode': bool(getattr(self, '_trace_mode', False)),
|
|
1123
|
+
'bank': getattr(self, '_current_slot', 0),
|
|
1124
|
+
'program_lines': len(self.program),
|
|
1125
|
+
'variables': len(self.variables),
|
|
1126
|
+
'arrays': len(self.arrays),
|
|
1127
|
+
'subroutines': len(self.subroutines),
|
|
1128
|
+
'custom_gates': sorted(self._custom_gates.keys()),
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
def cmd_status(self, rest: str = '') -> None:
|
|
1132
|
+
"""STATUS [JSON] — show every active mode (qubits, method, LOCC, noise,
|
|
1133
|
+
OPTION BASE, pending state injections, bank, ...) so behavior is readable
|
|
1134
|
+
rather than inferred. STATUS JSON emits the same data as JSON."""
|
|
1135
|
+
d = self._status_dict()
|
|
1136
|
+
if rest.strip().upper() == 'JSON':
|
|
1137
|
+
import json
|
|
1138
|
+
self.io.writeln(json.dumps(d, indent=2, default=str))
|
|
1139
|
+
return
|
|
1140
|
+
w = self.io.writeln
|
|
1141
|
+
w(" QUBASIC status")
|
|
1142
|
+
w(f" qubits {d['qubits']} shots {d['shots']} "
|
|
1143
|
+
f"method {d['method']} device {d['device']}")
|
|
1144
|
+
w(f" seed {d['seed']}")
|
|
1145
|
+
w(f" bit order {d['bit_order']}")
|
|
1146
|
+
w(f" option base {d['option_base']}")
|
|
1147
|
+
if d['locc_mode']:
|
|
1148
|
+
w(f" LOCC mode on registers {d['locc_registers']}")
|
|
1149
|
+
w(f" noise {('on ' + str(d['noise_spec'])) if d['noise_active'] else 'off'}")
|
|
1150
|
+
if d['coupling_map']:
|
|
1151
|
+
w(f" coupling {d['coupling_map']}")
|
|
1152
|
+
if d['basis_gates']:
|
|
1153
|
+
w(f" basis gates {d['basis_gates']}")
|
|
1154
|
+
if d['pending_set_state']:
|
|
1155
|
+
w(" pending SET_STATE applies on next RUN")
|
|
1156
|
+
if d['pending_set_density']:
|
|
1157
|
+
w(" pending SET_DENSITY applies on next RUN")
|
|
1158
|
+
if d['screen_mode']:
|
|
1159
|
+
w(f" screen mode {d['screen_mode']}")
|
|
1160
|
+
if d['trace_mode']:
|
|
1161
|
+
w(" trace on")
|
|
1162
|
+
w(f" bank {d['bank']}")
|
|
1163
|
+
w(f" program {d['program_lines']} lines, {d['variables']} vars, "
|
|
1164
|
+
f"{d['arrays']} arrays, {d['subroutines']} subs")
|
|
1165
|
+
if d['custom_gates']:
|
|
1166
|
+
w(f" custom gates {d['custom_gates']}")
|
|
1167
|
+
|
|
1073
1168
|
# cmd_defs, cmd_regs, cmd_vars provided by ProgramMgmtMixin.
|
|
1074
1169
|
|
|
1075
1170
|
# ── Run ───────────────────────────────────────────────────────────
|
|
@@ -1490,6 +1585,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1490
1585
|
'counts': self.last_counts or {},
|
|
1491
1586
|
'num_qubits': self.num_qubits,
|
|
1492
1587
|
'shots': self.shots,
|
|
1588
|
+
# Bitstrings are little-endian: the rightmost character is qubit 0.
|
|
1589
|
+
'bit_order': 'little-endian (qubit 0 = rightmost bit)',
|
|
1493
1590
|
}
|
|
1494
1591
|
uvars = {k: v for k, v in self.variables.items()
|
|
1495
1592
|
if not k.startswith('_') and isinstance(v, (int, float, str, bool))}
|
|
@@ -1750,6 +1847,14 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1750
1847
|
"""
|
|
1751
1848
|
m = RE_MEAS.match(stmt)
|
|
1752
1849
|
if not m:
|
|
1850
|
+
# Bare MEAS (no "-> bit") is the classic confusion with MEASURE:
|
|
1851
|
+
# MEAS is a mid-circuit measurement that must name a classical bit,
|
|
1852
|
+
# MEASURE collapses qubits into the result histogram.
|
|
1853
|
+
if re.match(r'MEAS\b', stmt, re.IGNORECASE):
|
|
1854
|
+
raise ValueError(
|
|
1855
|
+
"MEAS needs a target bit: MEAS <qubit> -> <bit> "
|
|
1856
|
+
"(mid-circuit measurement used by IF). To collapse qubits "
|
|
1857
|
+
"into the result histogram, use MEASURE <qubit>.")
|
|
1753
1858
|
return False
|
|
1754
1859
|
qubit = int(self._eval_with_vars(m.group(1), run_vars))
|
|
1755
1860
|
var = m.group(2)
|
|
@@ -2097,7 +2202,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
2097
2202
|
if not m:
|
|
2098
2203
|
return False
|
|
2099
2204
|
name = m.group(1)
|
|
2100
|
-
val = self._eval_string_expr(m.group(2))
|
|
2205
|
+
val = self._eval_string_expr(m.group(2), run_vars)
|
|
2101
2206
|
run_vars[name] = val
|
|
2102
2207
|
self.variables[name] = val
|
|
2103
2208
|
return True
|
|
@@ -1948,6 +1948,137 @@ class TestResources(unittest.TestCase):
|
|
|
1948
1948
|
self.assertIsNotNone(t.last_counts)
|
|
1949
1949
|
|
|
1950
1950
|
|
|
1951
|
+
class TestExpressionStringRegressions(unittest.TestCase):
|
|
1952
|
+
"""Regression coverage for the expression/string/PRINT fixes:
|
|
1953
|
+
case-insensitive functions, bitwise/logical operators in every expression
|
|
1954
|
+
context, string-variable assignment, and PRINT surfacing errors."""
|
|
1955
|
+
|
|
1956
|
+
def setUp(self):
|
|
1957
|
+
self.t = QBasicTerminal()
|
|
1958
|
+
self.t.num_qubits = 1
|
|
1959
|
+
|
|
1960
|
+
def test_math_functions_case_insensitive(self):
|
|
1961
|
+
# Uppercase now resolves as well as lowercase, and they agree.
|
|
1962
|
+
self.assertAlmostEqual(self.t.eval_expr('SQRT(2)'), math.sqrt(2))
|
|
1963
|
+
self.assertAlmostEqual(self.t.eval_expr('SQRT(2)'), self.t.eval_expr('sqrt(2)'))
|
|
1964
|
+
self.assertAlmostEqual(self.t.eval_expr('SIN(0)'), 0.0)
|
|
1965
|
+
self.assertEqual(self.t.eval_expr('ABS(-5)'), 5.0)
|
|
1966
|
+
self.assertEqual(self.t.eval_expr('INT(-3.2)'), -4.0) # BASIC INT floors
|
|
1967
|
+
self.assertEqual(self.t.eval_expr('FIX(-3.2)'), -3.0) # FIX truncates toward zero
|
|
1968
|
+
|
|
1969
|
+
def test_rnd_case_insensitive(self):
|
|
1970
|
+
for expr in ('RND(1)', 'rnd(1)'):
|
|
1971
|
+
v = self.t.eval_expr(expr)
|
|
1972
|
+
self.assertTrue(0.0 <= v < 1.0)
|
|
1973
|
+
|
|
1974
|
+
def test_bitwise_logical_operators_in_expressions(self):
|
|
1975
|
+
# AND/OR/XOR work in ordinary expressions (not just IF) and are bitwise.
|
|
1976
|
+
self.assertEqual(self.t.eval_expr('6 AND 3'), 2.0)
|
|
1977
|
+
self.assertEqual(self.t.eval_expr('5 OR 2'), 7.0)
|
|
1978
|
+
self.assertEqual(self.t.eval_expr('5 XOR 3'), 6.0)
|
|
1979
|
+
# <> works as an expression operator.
|
|
1980
|
+
self.assertTrue(self.t._safe_eval('3 <> 4'))
|
|
1981
|
+
self.assertFalse(self.t._safe_eval('3 <> 3'))
|
|
1982
|
+
|
|
1983
|
+
def test_logical_operator_precedence_preserved(self):
|
|
1984
|
+
# AND must bind below comparison: a > b AND c > d groups correctly.
|
|
1985
|
+
self.assertTrue(self.t._eval_condition('3 > 2 AND 5 > 1', {}))
|
|
1986
|
+
self.assertFalse(self.t._eval_condition('3 > 5 AND 5 > 1', {}))
|
|
1987
|
+
self.assertTrue(self.t._eval_condition('1 > 5 OR 5 > 1', {}))
|
|
1988
|
+
self.assertTrue(self.t._eval_condition('NOT 0', {}))
|
|
1989
|
+
self.assertFalse(self.t._eval_condition('NOT 1', {}))
|
|
1990
|
+
|
|
1991
|
+
def test_string_assignment_program_mode(self):
|
|
1992
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
1993
|
+
t.program = {10: 'LET s$ = "foo" + "bar"',
|
|
1994
|
+
20: 'LET t$ = LEFT$("hello", 3)',
|
|
1995
|
+
30: 'LET u$ = MID$("hello", 2)'}
|
|
1996
|
+
capture(t.cmd_run)
|
|
1997
|
+
self.assertEqual(t.variables['s$'], 'foobar')
|
|
1998
|
+
self.assertEqual(t.variables['t$'], 'hel')
|
|
1999
|
+
self.assertEqual(t.variables['u$'], 'ello')
|
|
2000
|
+
|
|
2001
|
+
def test_string_assignment_immediate_mode(self):
|
|
2002
|
+
capture(self.t.cmd_let, 's$ = "hi"')
|
|
2003
|
+
self.assertEqual(self.t.variables['s$'], 'hi')
|
|
2004
|
+
capture(self.t.cmd_let, 'g$ = "foo" + "bar"')
|
|
2005
|
+
self.assertEqual(self.t.variables['g$'], 'foobar')
|
|
2006
|
+
|
|
2007
|
+
def test_numeric_var_rejects_string(self):
|
|
2008
|
+
_, out = capture(self.t.cmd_let, 'n = "hi"')
|
|
2009
|
+
self.assertIn('TYPE MISMATCH', out)
|
|
2010
|
+
self.assertNotIn('n', self.t.variables)
|
|
2011
|
+
|
|
2012
|
+
def test_print_surfaces_errors_no_masking(self):
|
|
2013
|
+
# A valid (now case-insensitive) call evaluates instead of printing source.
|
|
2014
|
+
self.assertEqual(self.t._eval_print_item('SQRT(9)', {}), '3.0')
|
|
2015
|
+
# A genuine error is raised, not silently printed as raw text.
|
|
2016
|
+
with self.assertRaises(Exception):
|
|
2017
|
+
self.t._eval_print_item('GARBAGEFUNC(3)', {})
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
class TestConventionAndStateRegressions(unittest.TestCase):
|
|
2021
|
+
"""Convention-conformance and hidden-state fixes: INT floors, implicit LET,
|
|
2022
|
+
MEAS/MEASURE disambiguation, reserved constants, STATUS, bit-order labels."""
|
|
2023
|
+
|
|
2024
|
+
def setUp(self):
|
|
2025
|
+
self.t = QBasicTerminal()
|
|
2026
|
+
self.t.num_qubits = 2
|
|
2027
|
+
|
|
2028
|
+
def test_int_floors_fix_truncates(self):
|
|
2029
|
+
self.assertEqual(self.t.eval_expr('INT(-3.2)'), -4.0)
|
|
2030
|
+
self.assertEqual(self.t.eval_expr('INT(3.7)'), 3.0)
|
|
2031
|
+
self.assertEqual(self.t.eval_expr('FIX(-3.2)'), -3.0)
|
|
2032
|
+
self.assertEqual(self.t.eval_expr('FIX(3.7)'), 3.0)
|
|
2033
|
+
|
|
2034
|
+
def test_implicit_let_parses_as_assignment(self):
|
|
2035
|
+
from qubasic_core.parser import parse_stmt
|
|
2036
|
+
from qubasic_core.statements import LetStmt, LetStrStmt, LetArrayStmt
|
|
2037
|
+
self.assertIsInstance(parse_stmt('x = 5'), LetStmt)
|
|
2038
|
+
self.assertIsInstance(parse_stmt('s$ = "hi"'), LetStrStmt)
|
|
2039
|
+
self.assertIsInstance(parse_stmt('a(1) = 5'), LetArrayStmt)
|
|
2040
|
+
# '==' is a comparison, never an implicit assignment.
|
|
2041
|
+
self.assertNotIsInstance(parse_stmt('x == 5'), LetStmt)
|
|
2042
|
+
|
|
2043
|
+
def test_implicit_let_runs(self):
|
|
2044
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2045
|
+
t.program = {10: 'x = 5', 20: 's$ = "ok"', 30: 'y = x + 1'}
|
|
2046
|
+
capture(t.cmd_run)
|
|
2047
|
+
self.assertEqual(t.variables['x'], 5.0)
|
|
2048
|
+
self.assertEqual(t.variables['s$'], 'ok')
|
|
2049
|
+
self.assertEqual(t.variables['y'], 6.0)
|
|
2050
|
+
|
|
2051
|
+
def test_meas_bare_form_errors_measure_does_not(self):
|
|
2052
|
+
# MEAS without a target bit raises a helpful error...
|
|
2053
|
+
with self.assertRaises(ValueError):
|
|
2054
|
+
self.t._try_exec_meas('MEAS 0', None, {})
|
|
2055
|
+
# ...while MEASURE is a different statement and is left untouched here.
|
|
2056
|
+
self.assertFalse(self.t._try_exec_meas('MEASURE 0', None, {}))
|
|
2057
|
+
|
|
2058
|
+
def test_reserved_constants_not_assignable(self):
|
|
2059
|
+
for name in ('E', 'e', 'PI', 'pi', 'TAU', 'SQRT2'):
|
|
2060
|
+
with self.assertRaises(ValueError):
|
|
2061
|
+
self.t._assert_assignable(name)
|
|
2062
|
+
self.t._assert_assignable('x') # ordinary names are fine
|
|
2063
|
+
self.t._assert_assignable('e1')
|
|
2064
|
+
|
|
2065
|
+
def test_status_dict_reports_modes(self):
|
|
2066
|
+
t = QBasicTerminal(); t.num_qubits = 3; t.shots = 128
|
|
2067
|
+
d = t._status_dict()
|
|
2068
|
+
self.assertEqual(d['qubits'], 3)
|
|
2069
|
+
self.assertEqual(d['shots'], 128)
|
|
2070
|
+
self.assertIn('rightmost', d['bit_order'])
|
|
2071
|
+
self.assertFalse(d['noise_active'])
|
|
2072
|
+
self.assertIn('option_base', d)
|
|
2073
|
+
|
|
2074
|
+
def test_bit_order_label_and_result(self):
|
|
2075
|
+
t = QBasicTerminal(); t.num_qubits = 3
|
|
2076
|
+
note = t._bit_order_note([('001', 5)])
|
|
2077
|
+
self.assertIn('q2 q1 q0', note)
|
|
2078
|
+
self.assertIn('rightmost', note)
|
|
2079
|
+
self.assertIn('bit_order', t.result())
|
|
2080
|
+
|
|
2081
|
+
|
|
1951
2082
|
if __name__ == '__main__':
|
|
1952
2083
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
1953
2084
|
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
|
|
File without changes
|
|
File without changes
|