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.
Files changed (70) hide show
  1. {qubasic-0.10.0 → qubasic-0.11.0}/CHANGELOG.md +28 -0
  2. {qubasic-0.10.0/qubasic.egg-info → qubasic-0.11.0}/PKG-INFO +11 -5
  3. {qubasic-0.10.0 → qubasic-0.11.0}/README.md +10 -4
  4. {qubasic-0.10.0 → qubasic-0.11.0}/pyproject.toml +1 -1
  5. {qubasic-0.10.0 → qubasic-0.11.0/qubasic.egg-info}/PKG-INFO +11 -5
  6. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/control_flow.py +24 -8
  8. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/display.py +21 -0
  9. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/expression.py +71 -8
  10. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/parser.py +10 -0
  11. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/patterns.py +6 -0
  12. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/strings.py +12 -14
  13. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/terminal.py +110 -5
  14. {qubasic-0.10.0 → qubasic-0.11.0}/tests/test_qubasic.py +131 -0
  15. {qubasic-0.10.0 → qubasic-0.11.0}/LICENSE +0 -0
  16. {qubasic-0.10.0 → qubasic-0.11.0}/MANIFEST.in +0 -0
  17. {qubasic-0.10.0 → qubasic-0.11.0}/examples/bell.qb +0 -0
  18. {qubasic-0.10.0 → qubasic-0.11.0}/examples/grover3.qb +0 -0
  19. {qubasic-0.10.0 → qubasic-0.11.0}/examples/locc_teleport.qb +0 -0
  20. {qubasic-0.10.0 → qubasic-0.11.0}/examples/sweep_rx.qb +0 -0
  21. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/SOURCES.txt +0 -0
  22. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/dependency_links.txt +0 -0
  23. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/entry_points.txt +0 -0
  24. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/requires.txt +0 -0
  25. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic.egg-info/top_level.txt +0 -0
  26. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/__main__.py +0 -0
  27. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/algorithms.py +0 -0
  28. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/algos2.py +0 -0
  29. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/analysis.py +0 -0
  30. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/backend.py +0 -0
  31. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/benchmarking.py +0 -0
  32. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/bosonic.py +0 -0
  33. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/classic.py +0 -0
  34. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/cli.py +0 -0
  35. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/debug.py +0 -0
  36. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/demos.py +0 -0
  37. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/dynamics.py +0 -0
  38. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/engine.py +0 -0
  39. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/engine_state.py +0 -0
  40. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/errors.py +0 -0
  41. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/exec_context.py +0 -0
  42. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/executor.py +0 -0
  43. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/file_io.py +0 -0
  44. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/gates.py +0 -0
  45. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/help_text.py +0 -0
  46. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/io_protocol.py +0 -0
  47. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc.py +0 -0
  48. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_commands.py +0 -0
  49. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_display.py +0 -0
  50. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_engine.py +0 -0
  51. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/locc_execution.py +0 -0
  52. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/memory.py +0 -0
  53. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/mock_backend.py +0 -0
  54. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/noise_mixin.py +0 -0
  55. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/pauliprop.py +0 -0
  56. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/profiler.py +0 -0
  57. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/program_mgmt.py +0 -0
  58. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/protocol.py +0 -0
  59. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qec.py +0 -0
  60. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qol.py +0 -0
  61. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/qudits.py +0 -0
  62. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/resources.py +0 -0
  63. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/scope.py +0 -0
  64. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/screen.py +0 -0
  65. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/state_display.py +0 -0
  66. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/statements.py +0 -0
  67. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/subs.py +0 -0
  68. {qubasic-0.10.0 → qubasic-0.11.0}/qubasic_core/sweep.py +0 -0
  69. {qubasic-0.10.0 → qubasic-0.11.0}/setup.cfg +0 -0
  70. {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.10.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
- LET x = sin(angle) * 2
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`, `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
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`, `NOT` (on integers)
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
- LET x = sin(angle) * 2
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`, `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
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`, `NOT` (on integers)
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.10.0"
7
+ version = "0.11.0"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.10.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
- LET x = sin(angle) * 2
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`, `float`, `min`, `max`, `round`, `ceil`, `floor`, `len`
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`, `NOT` (on integers)
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)
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.10.0'
31
+ __version__ = '0.11.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -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
- val = self._eval_with_vars(expr, run_vars)
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
- try:
171
- ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
172
- run_vars if isinstance(run_vars, dict) else dict(run_vars))
173
- return str(self._safe_eval(text, extra_ns=ns))
174
- except Exception:
175
- return text
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
- 'abs': abs, 'int': int, 'float': float,
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
- ast.And: lambda a, b: a and b,
130
- ast.Or: lambda a, b: a or b,
131
- ast.BitAnd: operator.and_,
132
- ast.BitOr: operator.or_,
133
- ast.BitXor: operator.xor,
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
- cond = _rewrite_logical_outside_strings(cond)
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 might return a string."""
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
- # Quoted string literal
91
- if (expr.startswith('"') and expr.endswith('"')) or \
92
- (expr.startswith("'") and expr.endswith("'")):
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) and array elements
1048
- (LET a(0) = PI, LET m(i, j) = x), matching the in-program LET."""
1049
- from qubasic_core.patterns import RE_LET_ARRAY
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
- val = self.eval_expr(m.group(2))
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