qubasic 0.11.0__tar.gz → 0.12.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.11.0 → qubasic-0.12.0}/CHANGELOG.md +23 -0
  2. {qubasic-0.11.0/qubasic.egg-info → qubasic-0.12.0}/PKG-INFO +4 -1
  3. {qubasic-0.11.0 → qubasic-0.12.0}/README.md +3 -0
  4. {qubasic-0.11.0 → qubasic-0.12.0}/pyproject.toml +1 -1
  5. {qubasic-0.11.0 → qubasic-0.12.0/qubasic.egg-info}/PKG-INFO +4 -1
  6. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/analysis.py +22 -0
  8. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/cli.py +10 -5
  9. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/control_flow.py +16 -3
  10. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/engine_state.py +7 -0
  11. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/executor.py +16 -0
  12. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/help_text.py +5 -4
  13. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/patterns.py +1 -1
  14. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/subs.py +14 -0
  15. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/terminal.py +39 -4
  16. {qubasic-0.11.0 → qubasic-0.12.0}/tests/test_qubasic.py +73 -0
  17. {qubasic-0.11.0 → qubasic-0.12.0}/LICENSE +0 -0
  18. {qubasic-0.11.0 → qubasic-0.12.0}/MANIFEST.in +0 -0
  19. {qubasic-0.11.0 → qubasic-0.12.0}/examples/bell.qb +0 -0
  20. {qubasic-0.11.0 → qubasic-0.12.0}/examples/grover3.qb +0 -0
  21. {qubasic-0.11.0 → qubasic-0.12.0}/examples/locc_teleport.qb +0 -0
  22. {qubasic-0.11.0 → qubasic-0.12.0}/examples/sweep_rx.qb +0 -0
  23. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic.egg-info/SOURCES.txt +0 -0
  24. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic.egg-info/dependency_links.txt +0 -0
  25. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic.egg-info/entry_points.txt +0 -0
  26. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic.egg-info/requires.txt +0 -0
  27. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic.egg-info/top_level.txt +0 -0
  28. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/__main__.py +0 -0
  29. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/algorithms.py +0 -0
  30. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/algos2.py +0 -0
  31. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/backend.py +0 -0
  32. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/benchmarking.py +0 -0
  33. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/bosonic.py +0 -0
  34. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/classic.py +0 -0
  35. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/debug.py +0 -0
  36. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/demos.py +0 -0
  37. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/display.py +0 -0
  38. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/dynamics.py +0 -0
  39. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/engine.py +0 -0
  40. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/errors.py +0 -0
  41. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/exec_context.py +0 -0
  42. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/expression.py +0 -0
  43. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/file_io.py +0 -0
  44. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/gates.py +0 -0
  45. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/io_protocol.py +0 -0
  46. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/locc.py +0 -0
  47. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/locc_commands.py +0 -0
  48. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/locc_display.py +0 -0
  49. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/locc_engine.py +0 -0
  50. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/locc_execution.py +0 -0
  51. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/memory.py +0 -0
  52. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/mock_backend.py +0 -0
  53. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/noise_mixin.py +0 -0
  54. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/parser.py +0 -0
  55. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/pauliprop.py +0 -0
  56. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/profiler.py +0 -0
  57. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/program_mgmt.py +0 -0
  58. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/protocol.py +0 -0
  59. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/qec.py +0 -0
  60. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/qol.py +0 -0
  61. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/qudits.py +0 -0
  62. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/resources.py +0 -0
  63. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/scope.py +0 -0
  64. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/screen.py +0 -0
  65. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/state_display.py +0 -0
  66. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/statements.py +0 -0
  67. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/strings.py +0 -0
  68. {qubasic-0.11.0 → qubasic-0.12.0}/qubasic_core/sweep.py +0 -0
  69. {qubasic-0.11.0 → qubasic-0.12.0}/setup.cfg +0 -0
  70. {qubasic-0.11.0 → qubasic-0.12.0}/tests/test_features.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 (2026-06-19)
4
+
5
+ Correctness and robustness pass on the classic-BASIC layer, from an extended
6
+ adversarial audit. The quantum engine is unchanged.
7
+
8
+ ### Fixed
9
+ - `SHARED` variables in a `SUB` now propagate their modifications back to the caller instead of being discarded when the call's scope is restored (an accumulator across two calls now sums correctly: `8`, not `3`).
10
+ - `RESTORE` resets the `DATA` read pointer inside a program; it was a no-op, so a `READ` after `RESTORE` raised `OUT OF DATA`.
11
+ - Writes to an explicitly `DIM`med array are bounds-checked like reads instead of silently auto-extending past the declared size. Implicit (undimensioned) arrays still grow on first assignment.
12
+ - String arrays work: `LET s$(i) = ...` stores and reads string elements.
13
+ - `SET_DENSITY` followed by inspection without a `MEASURE` no longer raises a raw "unable to translate" error from Aer; the density matrix is captured and shown by `DENSITY`.
14
+ - A script that issues an explicit `RUN` and also contains a `MEASURE` no longer auto-runs (and prints) the program a second time.
15
+
16
+ ### Changed
17
+ - `STATUS` reports the user-variable count only, excluding the internal `_DEPTH`/`_GATES`/`_TIME` set after a run.
18
+ - `DEF FN name(x) = expr` now works at the prompt (immediate mode), matching the in-program form.
19
+ - `HELP` refreshed (`STATUS`, `INT` floors / `FIX` truncates, implicit `LET`, `<>`/`XOR`, corrected `MEAS`); `--help` notes the `-q`/`-v`/`-h` short flags and `python -m qubasic_core`.
20
+
21
+ ## 0.11.1 (2026-06-19)
22
+
23
+ ### Fixed
24
+ - A configuration/session command (`QUBITS`, `SHOTS`, `METHOD`, `LOCC`, ...) placed on a numbered program line now reports a clear error explaining it must run without a line number (before `RUN`), instead of the misleading `UNKNOWN GATE`. Unnumbered config in a script was, and remains, the correct usage.
25
+
3
26
  ## 0.11.0 (2026-06-19)
4
27
 
5
28
  Convention-conformance and observability pass: make the language behave the way
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -255,11 +255,14 @@ Hex/binary literals: `&HFF`, `&B10110`
255
255
  DIM data(10) 1D array
256
256
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
257
  LET data(0) = PI
258
+ LET names$(0) = "alice" String array (name$ elements hold strings)
258
259
  REDIM data(20) Resize (preserves existing data)
259
260
  ERASE data Delete array
260
261
  OPTION BASE 1 Set array index base
261
262
  ```
262
263
 
264
+ A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
265
+
263
266
  ## Control flow
264
267
 
265
268
  ```
@@ -222,11 +222,14 @@ Hex/binary literals: `&HFF`, `&B10110`
222
222
  DIM data(10) 1D array
223
223
  DIM matrix(3, 3) Multi-dimensional (flat storage)
224
224
  LET data(0) = PI
225
+ LET names$(0) = "alice" String array (name$ elements hold strings)
225
226
  REDIM data(20) Resize (preserves existing data)
226
227
  ERASE data Delete array
227
228
  OPTION BASE 1 Set array index base
228
229
  ```
229
230
 
231
+ A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
232
+
230
233
  ## Control flow
231
234
 
232
235
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.11.0"
7
+ version = "0.12.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.11.0
3
+ Version: 0.12.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -255,11 +255,14 @@ Hex/binary literals: `&HFF`, `&B10110`
255
255
  DIM data(10) 1D array
256
256
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
257
  LET data(0) = PI
258
+ LET names$(0) = "alice" String array (name$ elements hold strings)
258
259
  REDIM data(20) Resize (preserves existing data)
259
260
  ERASE data Delete array
260
261
  OPTION BASE 1 Set array index base
261
262
  ```
262
263
 
264
+ A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
265
+
263
266
  ## Control flow
264
267
 
265
268
  ```
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.11.0'
31
+ __version__ = '0.12.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -109,6 +109,28 @@ 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.
114
+ dm = getattr(self, '_last_density', None)
115
+ if dm is not None and not rest.strip():
116
+ rho = np.ascontiguousarray(dm)
117
+ dim = rho.shape[0]
118
+ self.io.writeln(f"\n Density matrix ({dim}x{dim}):\n")
119
+ if dim <= 16:
120
+ for i in range(dim):
121
+ row = []
122
+ for j in range(dim):
123
+ v = complex(rho[i, j])
124
+ if abs(v.imag) < 1e-6:
125
+ row.append(f"{v.real:7.3f}")
126
+ else:
127
+ row.append(f"{v.real:+.2f}{v.imag:+.2f}j")
128
+ self.io.writeln(f" {' '.join(row)}")
129
+ else:
130
+ self.io.writeln(f" ({dim}x{dim}, too large to display)")
131
+ self.io.writeln(f"\n Purity: {float(np.real(np.trace(rho @ rho))):.6f}")
132
+ self.io.writeln('')
133
+ return
112
134
  sv, n, rest = self._resolve_analysis_target(rest)
113
135
  if sv is None:
114
136
  if self.locc_mode and self.locc and not self.locc.joint:
@@ -38,8 +38,12 @@ def run_script(path: str, terminal: 'QBasicTerminal') -> None:
38
38
  ProgramMgmtMixin._load_lines_with_defs(
39
39
  lines, lambda line: terminal.process(line, track_undo=False))
40
40
 
41
- # Auto-run if the program has a reachable MEASURE (incl. in subs / IF clauses)
42
- if terminal.program and terminal._program_has_measure(sorted(terminal.program)):
41
+ # Auto-run if the program has a reachable MEASURE (incl. in subs / IF
42
+ # clauses), unless the script already issued an explicit RUN (which would
43
+ # otherwise execute and print the program twice).
44
+ explicit_run = any(l.strip().upper() == 'RUN' for l in lines)
45
+ if (terminal.program and not explicit_run
46
+ and terminal._program_has_measure(sorted(terminal.program))):
43
47
  terminal.cmd_run()
44
48
 
45
49
 
@@ -78,12 +82,13 @@ def main():
78
82
  print("Usage:")
79
83
  print(" qubasic Interactive REPL")
80
84
  print(" qubasic script.qb Run a script file")
81
- print(" qubasic --quiet script Suppress banner and progress")
85
+ print(" qubasic --quiet script Suppress banner and progress (also -q)")
82
86
  print(" qubasic --json script Output results as JSON")
83
87
  print(" qubasic --agent script Confine file writes to the working dir")
84
88
  print(" qubasic --seed N script Set random seed for reproducibility")
85
- print(" qubasic --version Show version")
86
- print(" qubasic --help Show this help")
89
+ print(" qubasic --version Show version (also -v)")
90
+ print(" qubasic --help Show this help (also -h)")
91
+ print(" python -m qubasic_core Run without the installed console script")
87
92
  print()
88
93
  print("Type HELP inside the REPL for full command reference.")
89
94
  sys.exit(0)
@@ -75,7 +75,11 @@ class ControlFlowMixin:
75
75
  name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
76
76
  self._assert_assignable(name)
77
77
  base = getattr(self, '_option_base', 0)
78
- val = self._eval_with_vars(val_expr, run_vars)
78
+ # String arrays (name$) hold string values; numeric arrays hold floats.
79
+ if name.endswith('$'):
80
+ val = self._eval_string_expr(val_expr, run_vars)
81
+ else:
82
+ val = self._eval_with_vars(val_expr, run_vars)
79
83
  parts = self._split_arg_list(idx_expr)
80
84
  if len(parts) > 1:
81
85
  # Multi-dimensional write: flatten with the same stride convention
@@ -97,10 +101,19 @@ class ControlFlowMixin:
97
101
  idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
98
102
  if idx < 0:
99
103
  raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
104
+ dimmed = getattr(self, '_dimmed_arrays', set())
100
105
  if name not in self.arrays:
106
+ # Implicit array: created (and allowed to grow) on first assignment.
101
107
  self.arrays[name] = [0.0] * (idx + 1)
102
- while idx >= len(self.arrays[name]):
103
- self.arrays[name].append(0.0)
108
+ elif idx >= len(self.arrays[name]):
109
+ if name in dimmed:
110
+ # Explicitly DIMmed: writes are bounds-checked like reads,
111
+ # instead of silently auto-extending past the declared size.
112
+ raise RuntimeError(
113
+ f"ARRAY INDEX OUT OF RANGE: {name}({idx + base}), "
114
+ f"size {len(self.arrays[name])}")
115
+ while idx >= len(self.arrays[name]):
116
+ self.arrays[name].append(0.0)
104
117
  self.arrays[name][idx] = val
105
118
  return True, ExecResult.ADVANCE
106
119
 
@@ -54,6 +54,11 @@ class Engine:
54
54
  self.variables: dict[str, Any] = {}
55
55
  self.arrays: dict[str, Any] = {}
56
56
  self._array_dims: dict[str, list[int]] = {}
57
+ # Arrays declared with DIM/REDIM enforce their bounds on write;
58
+ # arrays created implicitly by first assignment keep auto-growing.
59
+ self._dimmed_arrays: set[str] = set()
60
+ # Density matrix captured by a no-MEASURE run with SET_DENSITY.
61
+ self._last_density: Any = None
57
62
 
58
63
  # Subroutines and registers
59
64
  self.subroutines: dict[str, Any] = {}
@@ -112,6 +117,8 @@ class Engine:
112
117
  self.variables.clear()
113
118
  self.arrays.clear()
114
119
  self._array_dims.clear()
120
+ self._dimmed_arrays.clear()
121
+ self._last_density = None
115
122
  self.last_counts = None
116
123
  self.last_sv = None
117
124
  self.last_circuit = None
@@ -113,6 +113,8 @@ 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).
117
+ self._last_density = None
116
118
  # Apply any qubit state preparation requested via POKE to $0100.
117
119
  if getattr(self, '_poke_state_prep', None):
118
120
  self._emit_poke_state_prep(qc)
@@ -289,6 +291,7 @@ class ExecutorMixin:
289
291
  # 3. Remaining statement handlers (not in control-flow dispatch)
290
292
  from qubasic_core.statements import RestoreStmt
291
293
  if isinstance(parsed, RestoreStmt):
294
+ self._data_ptr = 0 # reset the DATA read pointer (was a no-op)
292
295
  return ExecResult.ADVANCE
293
296
 
294
297
  # Multi-line IF block markers — no-ops during execution
@@ -333,6 +336,19 @@ class ExecutorMixin:
333
336
  return ExecResult.ADVANCE
334
337
  # Fall through to _apply_gate_str for custom gates not in GATE_TABLE
335
338
 
339
+ # A REPL/config command (QUBITS, SHOTS, METHOD, LOCC, ...) used as a
340
+ # numbered program line reaches here only because it is neither a gate
341
+ # nor a statement. Explain it instead of failing with "UNKNOWN GATE".
342
+ # SEND/SHARE are LOCC statements (handled on the LOCC path), so leave
343
+ # them to their own error.
344
+ _first = stmt.split(None, 1)[0].upper() if stmt.split() else ''
345
+ if ((_first in self._CMD_WITH_ARG or _first in self._CMD_NO_ARG)
346
+ and _first not in ('SEND', 'SHARE')):
347
+ raise QBasicBuildError(
348
+ f"{_first} is a configuration/session command, not a program "
349
+ f"statement; run it without a line number (before RUN), e.g. "
350
+ f"QUBITS 3 / SHOTS 1024 / METHOD statevector / LOCC 2 2")
351
+
336
352
  # Slow path: subroutine expansion + gate dispatch
337
353
  expanded = self._expand_statement(stmt)
338
354
  for gate_str in expanded:
@@ -42,7 +42,7 @@ REGISTERS & SUBROUTINES
42
42
  DEFS List subroutines
43
43
 
44
44
  VARIABLES & LOOPS
45
- LET angle = PI/4 Set a variable
45
+ LET angle = PI/4 Set a variable (LET optional: x = 5 also works)
46
46
  10 RX angle, 0 Use in gate parameters
47
47
  10 FOR I = 0 TO 3 Loop (variable substitution in body)
48
48
  20 H I
@@ -62,6 +62,7 @@ CONFIGURATION
62
62
  SHOTS n Set number of shots (default: 1024)
63
63
  METHOD name Set simulation method (automatic, statevector,
64
64
  matrix_product_state, stabilizer, ...)
65
+ STATUS [JSON] Show every active mode (qubits, method, LOCC, noise, ...)
65
66
 
66
67
  DEMOS
67
68
  DEMO LIST List available demos
@@ -141,7 +142,7 @@ INLINE CIRCUIT INSTRUCTIONS (in programs, results available after RUN)
141
142
  SAVE_EXPECT ZZ 0,1 -> v Expectation value -> variable
142
143
  SAVE_PROBS 0,1 -> p Probability snapshot -> array
143
144
  SAVE_AMPS 0,3 -> a Specific amplitudes -> array
144
- MEAS qubit -> var Mid-circuit measurement (LOCC mode)
145
+ MEAS qubit -> var Mid-circuit measurement + IF feedforward (any mode)
145
146
  MEASURE_X/Y/Z qubit Basis measurement
146
147
 
147
148
  FLOW CONTROL (in programs)
@@ -162,8 +163,8 @@ FLOW CONTROL (in programs)
162
163
  LET arr[i] = val Array assignment
163
164
 
164
165
  EXPRESSIONS
165
- PI, TAU, E, SQRT2, sin(), cos(), sqrt(), log(), etc.
166
- Comparisons: ==, !=, <, >, <=, >=, AND, OR, NOT
166
+ PI, TAU, E, SQRT2, sin(), cos(), sqrt(), log(), int() floors, fix() truncates
167
+ Comparisons: ==, !=, <>, <, >, <=, >= Logical/bitwise: AND, OR, NOT, XOR
167
168
  Arrays: arr(i) or arr[i]
168
169
  Example: LET theta = PI/4 + asin(0.5)
169
170
  """
@@ -24,7 +24,7 @@ RE_GET = re.compile(r'GET\s+(\w+\$?)', re.IGNORECASE)
24
24
  RE_INPUT = re.compile(r'INPUT\s+(?:"([^"]*)"\s*,\s*)?(\w+)', re.IGNORECASE)
25
25
  RE_CTRL = re.compile(r'CTRL\s+(\w+)\s+(.*)', re.IGNORECASE)
26
26
  RE_INV = re.compile(r'INV\s+(\w+)\s+(.*)', re.IGNORECASE)
27
- RE_LET_ARRAY = re.compile(r'LET\s+(\w+)\((.+?)\)\s*=\s*(.*)', re.IGNORECASE)
27
+ RE_LET_ARRAY = re.compile(r'LET\s+(\w+\$?)\((.+?)\)\s*=\s*(.*)', re.IGNORECASE)
28
28
  RE_LET_VAR = re.compile(r'LET\s+(\w+(?:\.\w+)?)\s*=\s*(.*)', re.IGNORECASE)
29
29
  RE_PRINT = re.compile(r'PRINT\s+(.*)', re.IGNORECASE)
30
30
  RE_GOTO = re.compile(r'GOTO\s+(\d+)\s*$', re.IGNORECASE)
@@ -27,6 +27,9 @@ class SubroutineMixin:
27
27
  self._static_vars: dict[str, dict[str, Any]] = {'_GLOBAL': {}}
28
28
  self._call_stack: list[dict[str, Any]] = []
29
29
  self._func_call_depth: int = 0
30
+ # Names declared SHARED in the current scope level, so their
31
+ # modifications survive _pop_scope instead of being discarded.
32
+ self._shared_stack: list[set[str]] = []
30
33
 
31
34
  def _scan_subs(self, sorted_lines: list[int]) -> None:
32
35
  """Scan program for SUB/FUNCTION blocks and build jump table."""
@@ -284,12 +287,15 @@ class SubroutineMixin:
284
287
  if vname in self._scope_stack[-1]:
285
288
  run_vars[vname] = self._scope_stack[-1].get(vname, 0)
286
289
  self.variables[vname] = run_vars[vname]
290
+ if self._shared_stack:
291
+ self._shared_stack[-1].add(vname)
287
292
  return True, ExecResult.ADVANCE
288
293
 
289
294
  # ── Scope management ───────────────────────────────────────────────
290
295
 
291
296
  def _push_scope(self) -> None:
292
297
  self._scope_stack.append(dict(self.variables))
298
+ self._shared_stack.append(set())
293
299
 
294
300
  def _pop_scope(self, frame: dict[str, Any] | None = None) -> None:
295
301
  # Save STATIC vars for the current frame before restoring outer scope.
@@ -303,5 +309,13 @@ class SubroutineMixin:
303
309
  for vname in list(self._static_vars[sub_name]):
304
310
  self._static_vars[sub_name][vname] = self.variables.get(vname, 0)
305
311
  if self._scope_stack:
312
+ # SHARED variables must keep the modifications made inside the
313
+ # call; capture them before restoring the caller snapshot and
314
+ # write them back, instead of discarding them with the snapshot.
315
+ shared = self._shared_stack.pop() if self._shared_stack else set()
316
+ shared_vals = {v: self.variables.get(v) for v in shared}
306
317
  self.variables.clear()
307
318
  self.variables.update(self._scope_stack.pop())
319
+ for v, val in shared_vals.items():
320
+ if val is not None:
321
+ self.variables[v] = val
@@ -915,6 +915,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
915
915
  if upper.startswith('BEGIN'):
916
916
  return self._def_multiline(rest[5:].strip())
917
917
 
918
+ # DEF FN name(params) = expr — a user expression function, same as the
919
+ # in-program form, so it works at the prompt too.
920
+ if upper.startswith('FN'):
921
+ from qubasic_core.patterns import RE_DEF_FN
922
+ fm = RE_DEF_FN.match(f"DEF {rest}")
923
+ if fm:
924
+ fparams = [p.strip() for p in fm.group(2).split(',') if p.strip()]
925
+ self._user_fns['FN' + fm.group(1).upper()] = {
926
+ 'params': fparams, 'body': fm.group(3).strip()}
927
+ self.io.writeln(f"DEF FN {fm.group(1).upper()}({', '.join(fparams)})")
928
+ return
929
+
918
930
  m = RE_DEF_SINGLE.match(rest)
919
931
  if not m:
920
932
  self.io.writeln("?USAGE: DEF NAME[(params)] = GATE : GATE : ...")
@@ -1067,7 +1079,11 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1067
1079
  parsed = LetArrayStmt(raw=f"LET {rest}", name=name,
1068
1080
  index_expr=idx_expr, value_expr=val_expr)
1069
1081
  self._cf_let_array(f"LET {rest}", self.variables, parsed)
1070
- self.io.writeln(f"{name}({idx_expr.strip()}) = {self.eval_expr(val_expr)}")
1082
+ shown = (self._eval_string_expr(val_expr) if name.endswith('$')
1083
+ else self.eval_expr(val_expr))
1084
+ self.io.writeln(f"{name}({idx_expr.strip()}) = {shown!r}"
1085
+ if isinstance(shown, str)
1086
+ else f"{name}({idx_expr.strip()}) = {shown}")
1071
1087
  return
1072
1088
  sm = RE_LET_STR.match(f"LET {rest}")
1073
1089
  if sm:
@@ -1122,7 +1138,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1122
1138
  'trace_mode': bool(getattr(self, '_trace_mode', False)),
1123
1139
  'bank': getattr(self, '_current_slot', 0),
1124
1140
  'program_lines': len(self.program),
1125
- 'variables': len(self.variables),
1141
+ 'variables': sum(1 for k in self.variables if not k.startswith('_')),
1126
1142
  'arrays': len(self.arrays),
1127
1143
  'subroutines': len(self.subroutines),
1128
1144
  'custom_gates': sorted(self._custom_gates.keys()),
@@ -1281,9 +1297,24 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1281
1297
  }
1282
1298
 
1283
1299
  def _run_no_measure(self, qc, qc_sv, t0: float) -> None:
1284
- """Execute the no-MEASURE path: statevector only, no shots."""
1300
+ """Execute the no-MEASURE path: statevector (or density matrix), no shots."""
1285
1301
  too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
1286
- if too_large:
1302
+ if getattr(self, '_pending_set_density', None) is not None and not too_large:
1303
+ # A mixed state has no statevector, so capture the density matrix
1304
+ # for DENSITY instead. Saving a statevector here used to raise an
1305
+ # untranslatable-circuit error from Aer.
1306
+ self.last_sv = None
1307
+ try:
1308
+ qc_sv.save_density_matrix()
1309
+ dm_backend = self._make_backend('density_matrix', include_noise=True)
1310
+ dm_result = dm_backend.run(
1311
+ transpile(qc_sv, dm_backend, optimization_level=self._transpile_opt_level)).result()
1312
+ data = dm_result.data(0)
1313
+ dm = data.get('density_matrix') if hasattr(data, 'get') else None
1314
+ self._last_density = np.array(dm) if dm is not None else None
1315
+ except Exception:
1316
+ self._last_density = None
1317
+ elif too_large:
1287
1318
  self.last_sv = None
1288
1319
  else:
1289
1320
  try:
@@ -1919,6 +1950,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1919
1950
  self.arrays[name] = [0.0] * total
1920
1951
  if len(dims) > 1:
1921
1952
  self._array_dims[name] = dims
1953
+ self._dimmed_arrays.add(name)
1922
1954
  return True
1923
1955
 
1924
1956
  def _try_exec_dim_type(self, stmt: str) -> bool:
@@ -1957,6 +1989,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1957
1989
  self.arrays[name] = old[:new_size]
1958
1990
  else:
1959
1991
  self.arrays[name] = [0.0] * new_size
1992
+ self._dimmed_arrays.add(name)
1960
1993
  return True
1961
1994
 
1962
1995
  def _try_exec_erase(self, stmt: str) -> bool:
@@ -1967,6 +2000,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1967
2000
  name = m.group(1)
1968
2001
  if name in self.arrays:
1969
2002
  del self.arrays[name]
2003
+ self._array_dims.pop(name, None)
2004
+ self._dimmed_arrays.discard(name)
1970
2005
  return True
1971
2006
 
1972
2007
  def _try_exec_get(self, stmt: str, run_vars: dict) -> bool:
@@ -2078,6 +2078,79 @@ class TestConventionAndStateRegressions(unittest.TestCase):
2078
2078
  self.assertIn('rightmost', note)
2079
2079
  self.assertIn('bit_order', t.result())
2080
2080
 
2081
+ def test_config_command_as_program_line_errors_clearly(self):
2082
+ # A config/REPL command used as a numbered program line explains itself
2083
+ # instead of failing with the misleading "UNKNOWN GATE".
2084
+ for cmd in ('QUBITS 3', 'SHOTS 256', 'METHOD stabilizer', 'LOCC 2 2'):
2085
+ t = QBasicTerminal(); t.num_qubits = 2
2086
+ t.program = {10: cmd, 20: 'H 0', 30: 'MEASURE'}
2087
+ _, out = capture(t.cmd_run)
2088
+ self.assertIn('configuration', out.lower())
2089
+ self.assertNotIn('UNKNOWN GATE', out)
2090
+
2091
+ def test_subroutine_call_not_treated_as_config_command(self):
2092
+ t = QBasicTerminal(); t.num_qubits = 2
2093
+ t.process('DEF BELL = H 0 : CX 0,1', track_undo=False)
2094
+ t.program = {10: 'BELL', 20: 'MEASURE'}
2095
+ capture(t.cmd_run)
2096
+ self.assertIsNotNone(t.last_counts) # ran; not blocked as a "command"
2097
+
2098
+
2099
+ class TestDeepFixRegressions(unittest.TestCase):
2100
+ """SHARED writeback, RESTORE, array write bounds, string arrays,
2101
+ SET_DENSITY inspection, STATUS var count, immediate DEF FN."""
2102
+
2103
+ def _runp(self, lines, nq=1):
2104
+ t = QBasicTerminal(); t.num_qubits = nq
2105
+ for l in lines:
2106
+ t.process(l, track_undo=False)
2107
+ capture(t.cmd_run)
2108
+ return t
2109
+
2110
+ def test_shared_writeback(self):
2111
+ t = self._runp(['10 LET acc=0', '20 SUB ADD(n)', '30 SHARED acc',
2112
+ '40 LET acc=acc+n', '50 END SUB', '60 CALL ADD(5)',
2113
+ '70 CALL ADD(3)'])
2114
+ self.assertEqual(t.variables.get('acc'), 8.0)
2115
+
2116
+ def test_restore_resets_data_pointer(self):
2117
+ t = self._runp(['10 DATA 5', '20 READ a', '30 RESTORE', '40 READ b',
2118
+ '50 LET s=a+b'])
2119
+ self.assertEqual(t.variables.get('s'), 10.0)
2120
+
2121
+ def test_dimmed_array_write_is_bounds_checked(self):
2122
+ t = QBasicTerminal(); t.num_qubits = 1
2123
+ for l in ['10 DIM a(3)', '20 LET a(99)=1']:
2124
+ t.process(l, track_undo=False)
2125
+ _, out = capture(t.cmd_run)
2126
+ self.assertIn('OUT OF RANGE', out)
2127
+ # An implicit (undimensioned) array still auto-grows on write.
2128
+ t2 = self._runp(['10 LET b(50)=7', '20 LET v=b(50)'])
2129
+ self.assertEqual(t2.variables.get('v'), 7.0)
2130
+
2131
+ def test_string_array(self):
2132
+ t = self._runp(['10 LET s$(0)="he"+"llo"', '20 LET t$=s$(0)'])
2133
+ self.assertEqual(t.variables.get('t$'), 'hello')
2134
+
2135
+ def test_set_density_no_measure_does_not_crash(self):
2136
+ t = QBasicTerminal(); t.num_qubits = 1
2137
+ t.process('SET_DENSITY [[0.5,0],[0,0.5]]', track_undo=False)
2138
+ t.process('10 ID 0', track_undo=False)
2139
+ _, out = capture(t.cmd_run)
2140
+ self.assertNotIn('Unable to translate', out)
2141
+ self.assertIsNotNone(getattr(t, '_last_density', None))
2142
+ _, dout = capture(t.cmd_density, '')
2143
+ self.assertIn('Density matrix', dout)
2144
+
2145
+ def test_status_excludes_internal_vars(self):
2146
+ t = self._runp(['10 H 0', '20 MEASURE'])
2147
+ self.assertEqual(t._status_dict()['variables'], 0)
2148
+
2149
+ def test_immediate_def_fn(self):
2150
+ t = QBasicTerminal(); t.num_qubits = 1
2151
+ capture(t.cmd_def, 'FN sq(x) = x*x')
2152
+ self.assertEqual(t.eval_expr('sq(5)'), 25.0)
2153
+
2081
2154
 
2082
2155
  if __name__ == '__main__':
2083
2156
  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