qubasic 0.13.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {qubasic-0.13.0 → qubasic-0.14.0}/CHANGELOG.md +16 -0
  2. {qubasic-0.13.0/qubasic.egg-info → qubasic-0.14.0}/PKG-INFO +2 -2
  3. {qubasic-0.13.0 → qubasic-0.14.0}/README.md +1 -1
  4. {qubasic-0.13.0 → qubasic-0.14.0}/pyproject.toml +1 -1
  5. {qubasic-0.13.0 → qubasic-0.14.0/qubasic.egg-info}/PKG-INFO +2 -2
  6. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/analysis.py +5 -2
  8. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/cli.py +63 -3
  9. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/control_flow.py +3 -2
  10. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/engine_state.py +4 -1
  11. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/executor.py +3 -1
  12. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/expression.py +22 -12
  13. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/patterns.py +2 -2
  14. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/terminal.py +34 -32
  15. {qubasic-0.13.0 → qubasic-0.14.0}/tests/test_qubasic.py +28 -5
  16. {qubasic-0.13.0 → qubasic-0.14.0}/LICENSE +0 -0
  17. {qubasic-0.13.0 → qubasic-0.14.0}/MANIFEST.in +0 -0
  18. {qubasic-0.13.0 → qubasic-0.14.0}/examples/bell.qb +0 -0
  19. {qubasic-0.13.0 → qubasic-0.14.0}/examples/grover3.qb +0 -0
  20. {qubasic-0.13.0 → qubasic-0.14.0}/examples/locc_teleport.qb +0 -0
  21. {qubasic-0.13.0 → qubasic-0.14.0}/examples/sweep_rx.qb +0 -0
  22. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/SOURCES.txt +0 -0
  23. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/dependency_links.txt +0 -0
  24. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/entry_points.txt +0 -0
  25. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/requires.txt +0 -0
  26. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/top_level.txt +0 -0
  27. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/__main__.py +0 -0
  28. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/algorithms.py +0 -0
  29. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/algos2.py +0 -0
  30. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/backend.py +0 -0
  31. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/benchmarking.py +0 -0
  32. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/bosonic.py +0 -0
  33. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/classic.py +0 -0
  34. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/debug.py +0 -0
  35. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/demos.py +0 -0
  36. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/display.py +0 -0
  37. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/dynamics.py +0 -0
  38. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/engine.py +0 -0
  39. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/errors.py +0 -0
  40. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/exec_context.py +0 -0
  41. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/file_io.py +0 -0
  42. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/gates.py +0 -0
  43. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/help_text.py +0 -0
  44. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/io_protocol.py +0 -0
  45. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc.py +0 -0
  46. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_commands.py +0 -0
  47. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_display.py +0 -0
  48. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_engine.py +0 -0
  49. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_execution.py +0 -0
  50. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/memory.py +0 -0
  51. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/mock_backend.py +0 -0
  52. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/noise_mixin.py +0 -0
  53. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/parser.py +0 -0
  54. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/pauliprop.py +0 -0
  55. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/profiler.py +0 -0
  56. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/program_mgmt.py +0 -0
  57. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/protocol.py +0 -0
  58. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qec.py +0 -0
  59. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qol.py +0 -0
  60. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qudits.py +0 -0
  61. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/resources.py +0 -0
  62. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/scope.py +0 -0
  63. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/screen.py +0 -0
  64. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/state_display.py +0 -0
  65. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/statements.py +0 -0
  66. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/strings.py +0 -0
  67. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/subs.py +0 -0
  68. {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/sweep.py +0 -0
  69. {qubasic-0.13.0 → qubasic-0.14.0}/setup.cfg +0 -0
  70. {qubasic-0.13.0 → qubasic-0.14.0}/tests/test_features.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 (2026-06-19)
4
+
5
+ Second audit-gap round: a complete agent contract, BASIC truth values, range
6
+ comparisons, louder mid-circuit-bit handling, string-array defaults, and lazy
7
+ density inspection. The quantum engine is unchanged.
8
+
9
+ ### Added
10
+ - `qubasic --spec` now reports the full language surface: a `statements` section (QFT, MEAS, SYNDROME, EVOLVE, QADD, CTRL/INV, the control-flow keywords, ...) alongside commands, each with a `signature` field, plus `true_value: -1`.
11
+ - `DIM s$(n)` declares a string array; unread elements default to `""` instead of `0.0`.
12
+
13
+ ### Changed
14
+ - Comparisons return BASIC truth values: `-1` for true, `0` for false (so `LET t = (a > b)` is `-1`).
15
+ - Comparison chaining is Python-style, so `0 <= x <= 10` reads as `(0 <= x) AND (x <= 10)`.
16
+ - Reading a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) in a classical expression now raises a clear error instead of silently using the placeholder 0; `IF <bit>` feedforward is unaffected.
17
+ - `DENSITY` solves the density matrix lazily, on demand, instead of on every measured `SET_DENSITY` run.
18
+
3
19
  ## 0.13.0 (2026-06-19)
4
20
 
5
21
  Closes the remaining audit gaps in the classic-BASIC layer (conventions,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -245,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
245
245
 
246
246
  ### Operators
247
247
  Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
248
- Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
248
+ Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
249
249
  Logical: `AND`, `OR`, `NOT`, `XOR`
250
250
  Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
251
251
  Hex/binary literals: `&HFF`, `&B10110`
@@ -212,7 +212,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
212
212
 
213
213
  ### Operators
214
214
  Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
215
- Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
215
+ Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
216
216
  Logical: `AND`, `OR`, `NOT`, `XOR`
217
217
  Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
218
218
  Hex/binary literals: `&HFF`, `&B10110`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.13.0"
7
+ version = "0.14.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.13.0
3
+ Version: 0.14.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -245,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
245
245
 
246
246
  ### Operators
247
247
  Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
248
- Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
248
+ Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
249
249
  Logical: `AND`, `OR`, `NOT`, `XOR`
250
250
  Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
251
251
  Hex/binary literals: `&HFF`, `&B10110`
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.13.0'
31
+ __version__ = '0.14.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -109,9 +109,12 @@ class AnalysisMixin:
109
109
 
110
110
  def cmd_density(self, rest: str = '') -> None:
111
111
  """Show density matrix (DENSITY [reg]); summarizes for large systems."""
112
- # A mixed state set via SET_DENSITY has a stored density matrix and no
113
- # statevector; display it directly.
112
+ # A mixed state set via SET_DENSITY has no statevector; solve its
113
+ # density matrix on demand (cached) from the captured circuit.
114
114
  dm = getattr(self, '_last_density', None)
115
+ if dm is None and getattr(self, '_last_density_qc', None) is not None:
116
+ dm = self._density_from_qc(self._last_density_qc)
117
+ self._last_density = dm
115
118
  if dm is not None and not rest.strip():
116
119
  rho = np.ascontiguousarray(dm)
117
120
  dim = rho.shape[0]
@@ -26,6 +26,54 @@ if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
26
26
  from qubasic_core.terminal import QBasicTerminal
27
27
  from qubasic_core.program_mgmt import ProgramMgmtMixin
28
28
 
29
+ # Statement-level operations that are parsed inline rather than dispatched as
30
+ # REPL commands or gates, so they are absent from the command tables and the
31
+ # gate table. Listed in --spec (name, signature, description) for agents.
32
+ _SPEC_STATEMENTS = [
33
+ ('MEAS', 'MEAS <qubit> -> <bit>', 'mid-circuit measurement into a classical bit (drives IF feedforward)'),
34
+ ('MEASURE', 'MEASURE [qubit list]', 'measure all qubits, or a subset, into the result histogram'),
35
+ ('MEASURE_X', 'MEASURE_X <qubit>', 'measure in the X basis (result in mx_<q>)'),
36
+ ('MEASURE_Y', 'MEASURE_Y <qubit>', 'measure in the Y basis (result in my_<q>)'),
37
+ ('MEASURE_Z', 'MEASURE_Z <qubit>', 'measure in the Z basis (result in mz_<q>)'),
38
+ ('SYNDROME', 'SYNDROME <paulis> <qubits> -> <var>', 'non-destructive stabilizer measurement via an ancilla'),
39
+ ('RESET', 'RESET <qubit>', 'reset a qubit to |0>'),
40
+ ('BARRIER', 'BARRIER', 'optimization barrier'),
41
+ ('QFT', 'QFT <lo>-<hi>', 'quantum Fourier transform over a qubit range'),
42
+ ('IQFT', 'IQFT <lo>-<hi>', 'inverse quantum Fourier transform'),
43
+ ('DIFFUSE', 'DIFFUSE <lo>-<hi>', 'Grover diffusion operator'),
44
+ ('MCX', 'MCX <ctrl,...>, <target>', 'multi-controlled X'),
45
+ ('MCZ', 'MCZ <ctrl,...>', 'multi-controlled Z'),
46
+ ('MCP', 'MCP <theta>, <ctrl,...>, <target>', 'multi-controlled phase'),
47
+ ('QADD', 'QADD <a-range>, <b-range>', 'in-place register add A += B (mod 2^n)'),
48
+ ('QADDC', 'QADDC <k>, <range>', 'in-place constant add A += k (mod 2^n)'),
49
+ ('QPE', 'QPE <range> <target> <UGATE>', 'quantum phase estimation of a unitary'),
50
+ ('AMPLIFY', 'AMPLIFY <marked>', 'one amplitude-amplification (Grover) step'),
51
+ ('GRAPHSTATE', 'GRAPHSTATE <a-b, b-c, ...>', 'prepare a graph/cluster state'),
52
+ ('FEATUREMAP', 'FEATUREMAP <x0> <x1> ...', 'ZZ feature-map data encoding'),
53
+ ('EVOLVE', 'EVOLVE <H>, <time>, <steps>', 'Trotterized Hamiltonian time evolution'),
54
+ ('APPLYCHANNEL', 'APPLYCHANNEL <name> <qubit>', 'apply a defined Kraus channel'),
55
+ ('SAVE_EXPECT', 'SAVE_EXPECT <obs> <qubits> -> <var>', 'record an expectation value into a variable after RUN'),
56
+ ('SAVE_PROBS', 'SAVE_PROBS <qubits> -> <array>', 'record a probability snapshot into an array after RUN'),
57
+ ('SAVE_AMPS', 'SAVE_AMPS <lo>,<hi> -> <array>', 'record amplitudes into an array after RUN'),
58
+ ('CTRL', 'CTRL <gate> <ctrl>, <target>', 'controlled version of any gate'),
59
+ ('INV', 'INV <gate> <args>', 'inverse/dagger of a gate'),
60
+ ('UNITARY', 'UNITARY <NAME> = [[...]]', 'define a custom gate from a unitary matrix'),
61
+ ('GOTO', 'GOTO <line>', 'jump to a line number'),
62
+ ('GOSUB', 'GOSUB <line>', 'call a line block; RETURN resumes'),
63
+ ('FOR', 'FOR <v> = <a> TO <b> [STEP <s>]', 'counted loop, closed by NEXT'),
64
+ ('WHILE', 'WHILE <cond>', 'pre-test loop, closed by WEND'),
65
+ ('DO', 'DO [WHILE|UNTIL <cond>]', 'loop, closed by LOOP [WHILE|UNTIL]'),
66
+ ('IF', 'IF <cond> THEN <stmt> [ELSE <stmt>]', 'conditional (single-line, or block ending in END IF)'),
67
+ ('SELECT', 'SELECT CASE <expr>', 'multi-way branch (CASE / CASE ELSE / END SELECT)'),
68
+ ('DATA', 'DATA <v1>, <v2>, ...', 'inline data consumed by READ'),
69
+ ('READ', 'READ <var>, ...', 'read the next DATA values'),
70
+ ('DEF', 'DEF <NAME>[(p)] = <gates>', 'define a gate-sequence subroutine (also DEF FN, DEF BEGIN)'),
71
+ ('SUB', 'SUB <name>(<args>)', 'structured subroutine (END SUB; LOCAL/STATIC/SHARED)'),
72
+ ('FUNCTION', 'FUNCTION <name>(<args>)', 'function returning a value (END FUNCTION)'),
73
+ ('DIM', 'DIM <name>(<size>[,...])', 'declare an array (name$ for strings; inclusive sizing)'),
74
+ ('REDIM', 'REDIM [PRESERVE] <name>(<size>)', 'resize an array; PRESERVE keeps existing data'),
75
+ ]
76
+
29
77
 
30
78
  def run_script(path: str, terminal: 'QBasicTerminal') -> None:
31
79
  """Run a .qb script file. Supports multi-line DEF blocks.
@@ -100,20 +148,32 @@ def main():
100
148
  from qubasic_core.engine import GATE_TABLE
101
149
  from qubasic_core.expression import ExpressionMixin
102
150
 
103
- def _help1(mname):
151
+ def _doc_parts(mname):
104
152
  doc = (getattr(getattr(QBasicTerminal, mname, None), '__doc__', '') or '').strip()
105
- return doc.split('\n')[0].strip()
153
+ line = doc.split('\n')[0].strip()
154
+ # Docstrings format the first line as "SIGNATURE — description".
155
+ for sep in ('—', ' - '):
156
+ if sep in line:
157
+ sig, _, hlp = line.partition(sep)
158
+ return sig.strip(), hlp.strip()
159
+ return '', line
106
160
 
107
161
  cmds = []
108
162
  for tbl, takes in ((QBasicTerminal._CMD_WITH_ARG, True),
109
163
  (QBasicTerminal._CMD_NO_ARG, False)):
110
164
  for cname, mname in tbl.items():
111
- cmds.append({'name': cname, 'takes_arg': takes, 'help': _help1(mname)})
165
+ sig, hlp = _doc_parts(mname)
166
+ cmds.append({'name': cname, 'takes_arg': takes,
167
+ 'signature': sig or cname, 'help': hlp})
168
+ statements = [{'name': n, 'signature': s, 'help': h}
169
+ for (n, s, h) in _SPEC_STATEMENTS]
112
170
  spec = {
113
171
  'name': 'qubasic',
114
172
  'version': __version__,
115
173
  'bit_order': 'little-endian (qubit 0 = rightmost bit)',
174
+ 'true_value': -1,
116
175
  'commands': sorted(cmds, key=lambda c: c['name']),
176
+ 'statements': sorted(statements, key=lambda s: s['name']),
117
177
  'gates': sorted(GATE_TABLE.keys()),
118
178
  'functions': sorted(
119
179
  set(ExpressionMixin._SAFE_FUNCS)
@@ -102,9 +102,10 @@ class ControlFlowMixin:
102
102
  if idx < 0:
103
103
  raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
104
104
  dimmed = getattr(self, '_dimmed_arrays', set())
105
+ fill = '' if name.endswith('$') else 0.0 # string arrays default to ""
105
106
  if name not in self.arrays:
106
107
  # Implicit array: created (and allowed to grow) on first assignment.
107
- self.arrays[name] = [0.0] * (idx + 1)
108
+ self.arrays[name] = [fill] * (idx + 1)
108
109
  elif idx >= len(self.arrays[name]):
109
110
  if name in dimmed:
110
111
  # Explicitly DIMmed: writes are bounds-checked like reads,
@@ -113,7 +114,7 @@ class ControlFlowMixin:
113
114
  f"ARRAY INDEX OUT OF RANGE: {name}({idx + base}), "
114
115
  f"size {len(self.arrays[name])}")
115
116
  while idx >= len(self.arrays[name]):
116
- self.arrays[name].append(0.0)
117
+ self.arrays[name].append(fill)
117
118
  self.arrays[name][idx] = val
118
119
  return True, ExecResult.ADVANCE
119
120
 
@@ -57,8 +57,10 @@ class Engine:
57
57
  # Arrays declared with DIM/REDIM enforce their bounds on write;
58
58
  # arrays created implicitly by first assignment keep auto-growing.
59
59
  self._dimmed_arrays: set[str] = set()
60
- # Density matrix captured by a no-MEASURE run with SET_DENSITY.
60
+ # Density matrix (and the measure-free circuit to solve it lazily)
61
+ # for a run with SET_DENSITY, computed on demand by DENSITY.
61
62
  self._last_density: Any = None
63
+ self._last_density_qc: Any = None
62
64
 
63
65
  # Subroutines and registers
64
66
  self.subroutines: dict[str, Any] = {}
@@ -119,6 +121,7 @@ class Engine:
119
121
  self._array_dims.clear()
120
122
  self._dimmed_arrays.clear()
121
123
  self._last_density = None
124
+ self._last_density_qc = None
122
125
  self.last_counts = None
123
126
  self.last_sv = None
124
127
  self.last_circuit = None
@@ -113,8 +113,10 @@ class ExecutorMixin:
113
113
  self._classical_bits = {}
114
114
  # Partial-measurement subset (None = measure all at the end).
115
115
  self._measure_subset = None
116
- # Density matrix captured by a no-MEASURE run (None unless set).
116
+ # Density matrix (and circuit) captured by a SET_DENSITY run; the
117
+ # matrix is solved lazily by DENSITY from the stored circuit.
117
118
  self._last_density = None
119
+ self._last_density_qc = None
118
120
  # Apply any qubit state preparation requested via POKE to $0100.
119
121
  if getattr(self, '_poke_state_prep', None):
120
122
  self._emit_poke_state_prep(qc)
@@ -190,6 +190,15 @@ class ExpressionMixin:
190
190
  return node.value
191
191
  raise ValueError(f"UNSUPPORTED CONSTANT: {node.value!r}")
192
192
  if isinstance(node, ast.Name):
193
+ # A mid-circuit measurement bit has no classical value (it is
194
+ # resolved per shot); reading it outside an IF feedforward is an
195
+ # error rather than a silent placeholder 0.
196
+ cb = getattr(self, '_classical_bits', None)
197
+ if cb and node.id in cb:
198
+ raise ValueError(
199
+ f"'{node.id}' is a mid-circuit measurement bit; its value is "
200
+ f"per-shot, so it can't be read in a classical expression "
201
+ f"(use IF {node.id} for feedforward, or LOCC mode for a live value)")
193
202
  if node.id in ns:
194
203
  return ns[node.id]
195
204
  raise ValueError(f"UNDEFINED: {node.id}")
@@ -215,18 +224,19 @@ class ExpressionMixin:
215
224
  result = op(result, self._ast_eval(val, ns))
216
225
  return result
217
226
  if isinstance(node, ast.Compare):
218
- # A chained comparison (a < b < c) is ambiguous: Python reads it as
219
- # (a<b) and (b<c), BASIC as the left-to-right (a<b)<c. Rather than
220
- # pick one silently, require it to be written explicitly.
221
- if len(node.ops) > 1:
222
- raise ValueError(
223
- "AMBIGUOUS CHAINED COMPARISON: write it explicitly, "
224
- "e.g. (a < b) AND (b < c)")
225
- op = self._AST_OPS.get(type(node.ops[0]))
226
- if op is None:
227
- raise ValueError(f"UNSUPPORTED OP: {type(node.ops[0]).__name__}")
228
- return op(self._ast_eval(node.left, ns),
229
- self._ast_eval(node.comparators[0], ns))
227
+ # Python-style chaining (a < b < c means (a<b) and (b<c)) so range
228
+ # checks like 0 <= x <= 10 work as written; results use BASIC truth
229
+ # values (-1 for true, 0 for false).
230
+ left = self._ast_eval(node.left, ns)
231
+ for op_node, comparator in zip(node.ops, node.comparators):
232
+ op = self._AST_OPS.get(type(op_node))
233
+ if op is None:
234
+ raise ValueError(f"UNSUPPORTED OP: {type(op_node).__name__}")
235
+ right = self._ast_eval(comparator, ns)
236
+ if not op(left, right):
237
+ return 0
238
+ left = right
239
+ return -1
230
240
  if isinstance(node, ast.Call):
231
241
  if not isinstance(node.func, ast.Name):
232
242
  raise ValueError("ONLY SIMPLE FUNCTION CALLS ALLOWED")
@@ -17,7 +17,7 @@ RE_SHARE = re.compile(r'SHARE\s+([A-Z])\s+(\d+)\s*,?\s*([A-Z])\s+(\d+)', re.IGNO
17
17
  RE_MEAS = re.compile(r'MEAS\s+(\S+)\s*->\s*(\w+)', re.IGNORECASE)
18
18
  RE_RESET = re.compile(r'RESET\s+(\S+)', re.IGNORECASE)
19
19
  RE_UNITARY = re.compile(r'UNITARY\s+(\w+)\s*=\s*(\[.+\])', re.IGNORECASE)
20
- RE_DIM = re.compile(r'DIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
20
+ RE_DIM = re.compile(r'DIM\s+(\w+\$?)\((\d+)\)', re.IGNORECASE)
21
21
  RE_REDIM = re.compile(r'REDIM\s+(PRESERVE\s+)?(\w+)\((\d+)\)', re.IGNORECASE)
22
22
  RE_ERASE = re.compile(r'ERASE\s+(\w+)', re.IGNORECASE)
23
23
  RE_GET = re.compile(r'GET\s+(\w+\$?)', re.IGNORECASE)
@@ -100,7 +100,7 @@ RE_SCREEN = re.compile(r'SCREEN\s+(\d+)', re.IGNORECASE)
100
100
  RE_LPRINT = re.compile(r'LPRINT\s+(.*)', re.IGNORECASE)
101
101
  RE_ON_MEASURE = re.compile(r'ON\s+MEASURE\s+GOSUB\s+(\d+)', re.IGNORECASE)
102
102
  RE_ON_TIMER = re.compile(r'ON\s+TIMER\s*\((\d+)\)\s+GOSUB\s+(\d+)', re.IGNORECASE)
103
- RE_DIM_MULTI = re.compile(r'DIM\s+(\w+)\((\d+(?:\s*,\s*\d+)*)\)', re.IGNORECASE)
103
+ RE_DIM_MULTI = re.compile(r'DIM\s+(\w+\$?)\((\d+(?:\s*,\s*\d+)*)\)', re.IGNORECASE)
104
104
  RE_LET_STR = re.compile(r'LET\s+(\w+\$)\s*=\s*(.*)', re.IGNORECASE)
105
105
  # Implicit LET: an assignment written without the LET keyword (x = 5, s$ = "hi",
106
106
  # a(1) = 5, p.x = 3). Anchored lvalue followed by a single '=' that is not part
@@ -1244,6 +1244,28 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1244
1244
  # it skip extraction; STATE/BLOCH then report no state.
1245
1245
  _SV_EXTRACT_MAX_QUBITS = 24
1246
1246
 
1247
+ def _density_from_qc(self, qc_sv):
1248
+ """Solve the density matrix of a measure-free circuit, for DENSITY.
1249
+
1250
+ Computed on demand (not on every run) from the circuit captured when a
1251
+ SET_DENSITY state was prepared.
1252
+ """
1253
+ try:
1254
+ q2 = qc_sv.copy()
1255
+ q2.save_density_matrix()
1256
+ dm_backend = self._make_backend('density_matrix', include_noise=True)
1257
+ _kw = {}
1258
+ if self._seed is not None:
1259
+ _kw['seed_simulator'] = self._seed
1260
+ res = dm_backend.run(
1261
+ transpile(q2, dm_backend, optimization_level=self._transpile_opt_level),
1262
+ **_kw).result()
1263
+ data = res.data(0)
1264
+ dm = data.get('density_matrix') if hasattr(data, 'get') else None
1265
+ return np.array(dm) if dm is not None else None
1266
+ except Exception:
1267
+ return None
1268
+
1247
1269
  def _extract_statevector(self, qc_sv) -> None:
1248
1270
  """Run the measurement-free circuit copy to get last_sv.
1249
1271
 
@@ -1255,25 +1277,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1255
1277
  2^n statevector that cannot be displayed anyway.
1256
1278
  """
1257
1279
  if getattr(self, '_pending_set_density', None) is not None:
1258
- # Mixed state: no pure statevector. Capture the density matrix so
1259
- # DENSITY works after a measured run, not just a no-MEASURE one.
1280
+ # Mixed state: no pure statevector. Defer the density-matrix solve
1281
+ # to DENSITY (lazy) so a measured run does not pay for it unused;
1282
+ # keep the measure-free circuit to solve from.
1260
1283
  self.last_sv = None
1261
- if self.num_qubits <= self._SV_EXTRACT_MAX_QUBITS:
1262
- try:
1263
- qc_sv.save_density_matrix()
1264
- dm_backend = self._make_backend('density_matrix', include_noise=True)
1265
- _kw = {}
1266
- if self._seed is not None:
1267
- _kw['seed_simulator'] = self._seed
1268
- dm_result = dm_backend.run(
1269
- transpile(qc_sv, dm_backend,
1270
- optimization_level=self._transpile_opt_level),
1271
- **_kw).result()
1272
- data = dm_result.data(0)
1273
- dm = data.get('density_matrix') if hasattr(data, 'get') else None
1274
- self._last_density = np.array(dm) if dm is not None else None
1275
- except Exception:
1276
- self._last_density = None
1284
+ self._last_density = None
1285
+ self._last_density_qc = (qc_sv if self.num_qubits <= self._SV_EXTRACT_MAX_QUBITS
1286
+ else None)
1277
1287
  return
1278
1288
  if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
1279
1289
  self.last_sv = None
@@ -1318,20 +1328,12 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1318
1328
  """Execute the no-MEASURE path: statevector (or density matrix), no shots."""
1319
1329
  too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
1320
1330
  if getattr(self, '_pending_set_density', None) is not None and not too_large:
1321
- # A mixed state has no statevector, so capture the density matrix
1322
- # for DENSITY instead. Saving a statevector here used to raise an
1323
- # untranslatable-circuit error from Aer.
1331
+ # A mixed state has no statevector. Defer the density solve to
1332
+ # DENSITY (lazy); keep the measure-free circuit. Saving a statevector
1333
+ # here used to raise an untranslatable-circuit error from Aer.
1324
1334
  self.last_sv = None
1325
- try:
1326
- qc_sv.save_density_matrix()
1327
- dm_backend = self._make_backend('density_matrix', include_noise=True)
1328
- dm_result = dm_backend.run(
1329
- transpile(qc_sv, dm_backend, optimization_level=self._transpile_opt_level)).result()
1330
- data = dm_result.data(0)
1331
- dm = data.get('density_matrix') if hasattr(data, 'get') else None
1332
- self._last_density = np.array(dm) if dm is not None else None
1333
- except Exception:
1334
- self._last_density = None
1335
+ self._last_density = None
1336
+ self._last_density_qc = qc_sv
1335
1337
  elif too_large:
1336
1338
  self.last_sv = None
1337
1339
  else:
@@ -1969,7 +1971,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1969
1971
  total = 1
1970
1972
  for s in sizes:
1971
1973
  total *= s
1972
- self.arrays[name] = [0.0] * total
1974
+ self.arrays[name] = [('' if name.endswith('$') else 0.0)] * total
1973
1975
  if len(sizes) > 1:
1974
1976
  self._array_dims[name] = sizes
1975
1977
  self._dimmed_arrays.add(name)
@@ -2138,7 +2138,9 @@ class TestDeepFixRegressions(unittest.TestCase):
2138
2138
  t.process('10 ID 0', track_undo=False)
2139
2139
  _, out = capture(t.cmd_run)
2140
2140
  self.assertNotIn('Unable to translate', out)
2141
- self.assertIsNotNone(getattr(t, '_last_density', None))
2141
+ # Density is solved lazily: the measure-free circuit is captured now,
2142
+ # and DENSITY computes the matrix on demand.
2143
+ self.assertIsNotNone(getattr(t, '_last_density_qc', None))
2142
2144
  _, dout = capture(t.cmd_density, '')
2143
2145
  self.assertIn('Density matrix', dout)
2144
2146
 
@@ -2173,10 +2175,13 @@ class TestConventionFixesV2(unittest.TestCase):
2173
2175
  self.assertEqual(self.t.eval_expr('round(-2.5)'), -3.0)
2174
2176
  self.assertAlmostEqual(self.t.eval_expr('round(2.345, 2)'), 2.35)
2175
2177
 
2176
- def test_chained_comparison_raises(self):
2177
- self.assertTrue(bool(self.t._safe_eval('3 < 5')))
2178
- with self.assertRaises(ValueError):
2179
- self.t._safe_eval('1 < 2 < 3')
2178
+ def test_comparison_truth_and_chaining(self):
2179
+ # Comparisons yield BASIC truth values: -1 for true, 0 for false.
2180
+ self.assertEqual(self.t._safe_eval('3 < 5'), -1)
2181
+ self.assertEqual(self.t._safe_eval('3 > 5'), 0)
2182
+ # Python-style chaining, so range checks work as written.
2183
+ self.assertEqual(self.t._safe_eval('0 <= 5 <= 10'), -1)
2184
+ self.assertEqual(self.t._safe_eval('0 <= 50 <= 10'), 0)
2180
2185
 
2181
2186
  def test_dim_inclusive_top_index(self):
2182
2187
  t = self._runp(['10 DIM a(5)', '20 LET a(5)=7', '30 LET v=a(5)'])
@@ -2203,6 +2208,24 @@ class TestConventionFixesV2(unittest.TestCase):
2203
2208
  _, dout = capture(t.cmd_density, '')
2204
2209
  self.assertIn('Density matrix', dout)
2205
2210
 
2211
+ def test_meas_bit_classical_use_errors(self):
2212
+ t = QBasicTerminal(); t.num_qubits = 1
2213
+ for l in ['10 X 0', '20 MEAS 0 -> m', '30 LET y = m + 1', '40 MEASURE']:
2214
+ t.process(l, track_undo=False)
2215
+ _, out = capture(t.cmd_run)
2216
+ self.assertIn('mid-circuit', out)
2217
+
2218
+ def test_string_array_default_empty(self):
2219
+ t = QBasicTerminal(); t.num_qubits = 1
2220
+ t.process('DIM s$(3)', track_undo=False)
2221
+ self.assertEqual(t._safe_eval('s$(1)'), '')
2222
+
2223
+ def test_spec_statements_present(self):
2224
+ from qubasic_core.cli import _SPEC_STATEMENTS
2225
+ names = {n for (n, _s, _h) in _SPEC_STATEMENTS}
2226
+ for op in ('QFT', 'MEAS', 'SYNDROME', 'EVOLVE', 'QADD'):
2227
+ self.assertIn(op, names)
2228
+
2206
2229
 
2207
2230
  if __name__ == '__main__':
2208
2231
  if hasattr(sys.stdout, 'reconfigure'):
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