qubasic 0.10.1__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.1 → qubasic-0.11.0}/CHANGELOG.md +17 -0
  2. {qubasic-0.10.1/qubasic.egg-info → qubasic-0.11.0}/PKG-INFO +10 -4
  3. {qubasic-0.10.1 → qubasic-0.11.0}/README.md +9 -3
  4. {qubasic-0.10.1 → qubasic-0.11.0}/pyproject.toml +1 -1
  5. {qubasic-0.10.1 → qubasic-0.11.0/qubasic.egg-info}/PKG-INFO +10 -4
  6. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/control_flow.py +2 -0
  8. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/display.py +21 -0
  9. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/expression.py +13 -1
  10. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/parser.py +10 -0
  11. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/patterns.py +6 -0
  12. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/terminal.py +95 -0
  13. {qubasic-0.10.1 → qubasic-0.11.0}/tests/test_qubasic.py +64 -1
  14. {qubasic-0.10.1 → qubasic-0.11.0}/LICENSE +0 -0
  15. {qubasic-0.10.1 → qubasic-0.11.0}/MANIFEST.in +0 -0
  16. {qubasic-0.10.1 → qubasic-0.11.0}/examples/bell.qb +0 -0
  17. {qubasic-0.10.1 → qubasic-0.11.0}/examples/grover3.qb +0 -0
  18. {qubasic-0.10.1 → qubasic-0.11.0}/examples/locc_teleport.qb +0 -0
  19. {qubasic-0.10.1 → qubasic-0.11.0}/examples/sweep_rx.qb +0 -0
  20. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic.egg-info/SOURCES.txt +0 -0
  21. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic.egg-info/dependency_links.txt +0 -0
  22. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic.egg-info/entry_points.txt +0 -0
  23. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic.egg-info/requires.txt +0 -0
  24. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic.egg-info/top_level.txt +0 -0
  25. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/__main__.py +0 -0
  26. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/algorithms.py +0 -0
  27. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/algos2.py +0 -0
  28. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/analysis.py +0 -0
  29. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/backend.py +0 -0
  30. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/benchmarking.py +0 -0
  31. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/bosonic.py +0 -0
  32. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/classic.py +0 -0
  33. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/cli.py +0 -0
  34. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/debug.py +0 -0
  35. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/demos.py +0 -0
  36. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/dynamics.py +0 -0
  37. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/engine.py +0 -0
  38. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/engine_state.py +0 -0
  39. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/errors.py +0 -0
  40. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/exec_context.py +0 -0
  41. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/executor.py +0 -0
  42. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/file_io.py +0 -0
  43. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/gates.py +0 -0
  44. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/help_text.py +0 -0
  45. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/io_protocol.py +0 -0
  46. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/locc.py +0 -0
  47. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/locc_commands.py +0 -0
  48. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/locc_display.py +0 -0
  49. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/locc_engine.py +0 -0
  50. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/locc_execution.py +0 -0
  51. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/memory.py +0 -0
  52. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/mock_backend.py +0 -0
  53. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/noise_mixin.py +0 -0
  54. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/pauliprop.py +0 -0
  55. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/profiler.py +0 -0
  56. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/program_mgmt.py +0 -0
  57. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/protocol.py +0 -0
  58. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/qec.py +0 -0
  59. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/qol.py +0 -0
  60. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/qudits.py +0 -0
  61. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/resources.py +0 -0
  62. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/scope.py +0 -0
  63. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/screen.py +0 -0
  64. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/state_display.py +0 -0
  65. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/statements.py +0 -0
  66. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/strings.py +0 -0
  67. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/subs.py +0 -0
  68. {qubasic-0.10.1 → qubasic-0.11.0}/qubasic_core/sweep.py +0 -0
  69. {qubasic-0.10.1 → qubasic-0.11.0}/setup.cfg +0 -0
  70. {qubasic-0.10.1 → qubasic-0.11.0}/tests/test_features.py +0 -0
@@ -1,5 +1,22 @@
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
+
3
20
  ## 0.10.1 (2026-06-19)
4
21
 
5
22
  Expression, string, and PRINT fixes in the classic-BASIC layer. The quantum
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.10.1
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
@@ -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
@@ -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.1"
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.1
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
@@ -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.1'
31
+ __version__ = '0.11.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -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,6 +107,7 @@ 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
110
+ self._assert_assignable(name)
109
111
  raw = self._safe_eval(expr, extra_ns=run_vars)
110
112
  if isinstance(raw, str):
111
113
  raise RuntimeError(
@@ -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:
@@ -135,7 +135,9 @@ class ExpressionMixin:
135
135
  'asin': math.asin, 'acos': math.acos, 'atan': math.atan,
136
136
  'atan2': math.atan2,
137
137
  'sqrt': math.sqrt, 'log': math.log, 'exp': math.exp,
138
- '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,
139
141
  'min': min, 'max': max, 'round': round, 'len': len,
140
142
  'ceil': math.ceil, 'floor': math.floor,
141
143
  }
@@ -406,6 +408,16 @@ class ExpressionMixin:
406
408
  """
407
409
  return bool(self._safe_eval(cond, extra_ns=run_vars))
408
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
+
409
421
  def _run_timer(self) -> float:
410
422
  """Return elapsed time since terminal start."""
411
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
  ]
@@ -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)
@@ -1066,6 +1078,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1066
1078
  self.io.writeln("?USAGE: LET <var> = <expr>")
1067
1079
  return
1068
1080
  name = m.group(1)
1081
+ self._assert_assignable(name)
1069
1082
  raw = self._safe_eval(m.group(2))
1070
1083
  if isinstance(raw, str):
1071
1084
  self.io.writeln(
@@ -1080,6 +1093,78 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1080
1093
  rec[field] = val
1081
1094
  self.io.writeln(f"{name} = {val}")
1082
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
+
1083
1168
  # cmd_defs, cmd_regs, cmd_vars provided by ProgramMgmtMixin.
1084
1169
 
1085
1170
  # ── Run ───────────────────────────────────────────────────────────
@@ -1500,6 +1585,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1500
1585
  'counts': self.last_counts or {},
1501
1586
  'num_qubits': self.num_qubits,
1502
1587
  'shots': self.shots,
1588
+ # Bitstrings are little-endian: the rightmost character is qubit 0.
1589
+ 'bit_order': 'little-endian (qubit 0 = rightmost bit)',
1503
1590
  }
1504
1591
  uvars = {k: v for k, v in self.variables.items()
1505
1592
  if not k.startswith('_') and isinstance(v, (int, float, str, bool))}
@@ -1760,6 +1847,14 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1760
1847
  """
1761
1848
  m = RE_MEAS.match(stmt)
1762
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>.")
1763
1858
  return False
1764
1859
  qubit = int(self._eval_with_vars(m.group(1), run_vars))
1765
1860
  var = m.group(2)
@@ -1963,7 +1963,8 @@ class TestExpressionStringRegressions(unittest.TestCase):
1963
1963
  self.assertAlmostEqual(self.t.eval_expr('SQRT(2)'), self.t.eval_expr('sqrt(2)'))
1964
1964
  self.assertAlmostEqual(self.t.eval_expr('SIN(0)'), 0.0)
1965
1965
  self.assertEqual(self.t.eval_expr('ABS(-5)'), 5.0)
1966
- self.assertEqual(self.t.eval_expr('INT(-3.2)'), self.t.eval_expr('int(-3.2)'))
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
1967
1968
 
1968
1969
  def test_rnd_case_insensitive(self):
1969
1970
  for expr in ('RND(1)', 'rnd(1)'):
@@ -2016,6 +2017,68 @@ class TestExpressionStringRegressions(unittest.TestCase):
2016
2017
  self.t._eval_print_item('GARBAGEFUNC(3)', {})
2017
2018
 
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
+
2019
2082
  if __name__ == '__main__':
2020
2083
  if hasattr(sys.stdout, 'reconfigure'):
2021
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