qubasic 0.11.1__tar.gz → 0.13.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.1 → qubasic-0.13.0}/CHANGELOG.md +35 -0
  2. {qubasic-0.11.1/qubasic.egg-info → qubasic-0.13.0}/PKG-INFO +7 -2
  3. {qubasic-0.11.1 → qubasic-0.13.0}/README.md +6 -1
  4. {qubasic-0.11.1 → qubasic-0.13.0}/pyproject.toml +1 -1
  5. {qubasic-0.11.1 → qubasic-0.13.0/qubasic.egg-info}/PKG-INFO +7 -2
  6. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/analysis.py +22 -0
  8. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/cli.py +43 -6
  9. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/control_flow.py +22 -3
  10. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/engine_state.py +7 -0
  11. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/executor.py +3 -0
  12. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/expression.py +25 -11
  13. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/help_text.py +5 -4
  14. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/patterns.py +2 -2
  15. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/strings.py +4 -1
  16. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/subs.py +14 -0
  17. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/terminal.py +80 -17
  18. {qubasic-0.11.1 → qubasic-0.13.0}/tests/test_features.py +22 -15
  19. {qubasic-0.11.1 → qubasic-0.13.0}/tests/test_qubasic.py +108 -0
  20. {qubasic-0.11.1 → qubasic-0.13.0}/LICENSE +0 -0
  21. {qubasic-0.11.1 → qubasic-0.13.0}/MANIFEST.in +0 -0
  22. {qubasic-0.11.1 → qubasic-0.13.0}/examples/bell.qb +0 -0
  23. {qubasic-0.11.1 → qubasic-0.13.0}/examples/grover3.qb +0 -0
  24. {qubasic-0.11.1 → qubasic-0.13.0}/examples/locc_teleport.qb +0 -0
  25. {qubasic-0.11.1 → qubasic-0.13.0}/examples/sweep_rx.qb +0 -0
  26. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/SOURCES.txt +0 -0
  27. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/dependency_links.txt +0 -0
  28. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/entry_points.txt +0 -0
  29. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/requires.txt +0 -0
  30. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic.egg-info/top_level.txt +0 -0
  31. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/__main__.py +0 -0
  32. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/algorithms.py +0 -0
  33. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/algos2.py +0 -0
  34. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/backend.py +0 -0
  35. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/benchmarking.py +0 -0
  36. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/bosonic.py +0 -0
  37. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/classic.py +0 -0
  38. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/debug.py +0 -0
  39. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/demos.py +0 -0
  40. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/display.py +0 -0
  41. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/dynamics.py +0 -0
  42. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/engine.py +0 -0
  43. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/errors.py +0 -0
  44. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/exec_context.py +0 -0
  45. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/file_io.py +0 -0
  46. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/gates.py +0 -0
  47. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/io_protocol.py +0 -0
  48. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc.py +0 -0
  49. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_commands.py +0 -0
  50. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_display.py +0 -0
  51. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_engine.py +0 -0
  52. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/locc_execution.py +0 -0
  53. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/memory.py +0 -0
  54. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/mock_backend.py +0 -0
  55. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/noise_mixin.py +0 -0
  56. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/parser.py +0 -0
  57. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/pauliprop.py +0 -0
  58. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/profiler.py +0 -0
  59. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/program_mgmt.py +0 -0
  60. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/protocol.py +0 -0
  61. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qec.py +0 -0
  62. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qol.py +0 -0
  63. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/qudits.py +0 -0
  64. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/resources.py +0 -0
  65. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/scope.py +0 -0
  66. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/screen.py +0 -0
  67. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/state_display.py +0 -0
  68. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/statements.py +0 -0
  69. {qubasic-0.11.1 → qubasic-0.13.0}/qubasic_core/sweep.py +0 -0
  70. {qubasic-0.11.1 → qubasic-0.13.0}/setup.cfg +0 -0
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 (2026-06-19)
4
+
5
+ Closes the remaining audit gaps in the classic-BASIC layer (conventions,
6
+ inspection, and an agent contract). The quantum engine is unchanged.
7
+
8
+ ### Added
9
+ - `qubasic --spec` emits a machine-readable JSON contract (version, commands with arg flag and one-line help, gates, functions, constants, bit order) so an agent can load the exact surface of the installed version.
10
+ - `REDIM PRESERVE name(n)` keeps existing elements; plain `REDIM` clears to zeros (QBASIC semantics).
11
+ - `DENSITY` shows the density matrix after a measured `SET_DENSITY` run, not only a no-MEASURE one.
12
+
13
+ ### Changed
14
+ - `DIM` uses inclusive sizing: `DIM a(n)` spans indices base..n (the declared top index is valid), matching QBASIC instead of the previous C-style element count.
15
+ - `round()` rounds half away from zero (`round(2.5)` = 3), not Python's banker's rounding.
16
+ - `STR$(n)` reserves a leading space for non-negative numbers (`STR$(42)` = " 42").
17
+ - A chained comparison such as `a < b < c` now raises a clear "ambiguous" error instead of silently using Python chaining; write `(a < b) AND (b < c)`.
18
+ - `PRINT`ing a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) shows "mid-circuit bit, resolved per shot" rather than the placeholder 0.
19
+
20
+ ## 0.12.0 (2026-06-19)
21
+
22
+ Correctness and robustness pass on the classic-BASIC layer, from an extended
23
+ adversarial audit. The quantum engine is unchanged.
24
+
25
+ ### Fixed
26
+ - `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`).
27
+ - `RESTORE` resets the `DATA` read pointer inside a program; it was a no-op, so a `READ` after `RESTORE` raised `OUT OF DATA`.
28
+ - 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.
29
+ - String arrays work: `LET s$(i) = ...` stores and reads string elements.
30
+ - `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`.
31
+ - A script that issues an explicit `RUN` and also contains a `MEASURE` no longer auto-runs (and prints) the program a second time.
32
+
33
+ ### Changed
34
+ - `STATUS` reports the user-variable count only, excluding the internal `_DEPTH`/`_GATES`/`_TIME` set after a run.
35
+ - `DEF FN name(x) = expr` now works at the prompt (immediate mode), matching the in-program form.
36
+ - `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`.
37
+
3
38
  ## 0.11.1 (2026-06-19)
4
39
 
5
40
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.11.1
3
+ Version: 0.13.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -78,6 +78,7 @@ python -m qubasic_core Same, without installing
78
78
  qubasic script.qb Run a script file
79
79
  qubasic --quiet script Suppress banner, output results only
80
80
  qubasic --json script Machine-readable JSON output
81
+ qubasic --spec Print a JSON contract (commands, gates, functions)
81
82
  qubasic --help Show CLI help
82
83
  ```
83
84
 
@@ -255,11 +256,15 @@ Hex/binary literals: `&HFF`, `&B10110`
255
256
  DIM data(10) 1D array
256
257
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
258
  LET data(0) = PI
258
- REDIM data(20) Resize (preserves existing data)
259
+ LET names$(0) = "alice" String array (name$ elements hold strings)
260
+ REDIM data(20) Resize, clearing to zeros
261
+ REDIM PRESERVE data(20) Resize, keeping existing data
259
262
  ERASE data Delete array
260
263
  OPTION BASE 1 Set array index base
261
264
  ```
262
265
 
266
+ `DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
267
+
263
268
  ## Control flow
264
269
 
265
270
  ```
@@ -45,6 +45,7 @@ python -m qubasic_core Same, without installing
45
45
  qubasic script.qb Run a script file
46
46
  qubasic --quiet script Suppress banner, output results only
47
47
  qubasic --json script Machine-readable JSON output
48
+ qubasic --spec Print a JSON contract (commands, gates, functions)
48
49
  qubasic --help Show CLI help
49
50
  ```
50
51
 
@@ -222,11 +223,15 @@ Hex/binary literals: `&HFF`, `&B10110`
222
223
  DIM data(10) 1D array
223
224
  DIM matrix(3, 3) Multi-dimensional (flat storage)
224
225
  LET data(0) = PI
225
- REDIM data(20) Resize (preserves existing data)
226
+ LET names$(0) = "alice" String array (name$ elements hold strings)
227
+ REDIM data(20) Resize, clearing to zeros
228
+ REDIM PRESERVE data(20) Resize, keeping existing data
226
229
  ERASE data Delete array
227
230
  OPTION BASE 1 Set array index base
228
231
  ```
229
232
 
233
+ `DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
234
+
230
235
  ## Control flow
231
236
 
232
237
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.11.1"
7
+ version = "0.13.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.1
3
+ Version: 0.13.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -78,6 +78,7 @@ python -m qubasic_core Same, without installing
78
78
  qubasic script.qb Run a script file
79
79
  qubasic --quiet script Suppress banner, output results only
80
80
  qubasic --json script Machine-readable JSON output
81
+ qubasic --spec Print a JSON contract (commands, gates, functions)
81
82
  qubasic --help Show CLI help
82
83
  ```
83
84
 
@@ -255,11 +256,15 @@ Hex/binary literals: `&HFF`, `&B10110`
255
256
  DIM data(10) 1D array
256
257
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
258
  LET data(0) = PI
258
- REDIM data(20) Resize (preserves existing data)
259
+ LET names$(0) = "alice" String array (name$ elements hold strings)
260
+ REDIM data(20) Resize, clearing to zeros
261
+ REDIM PRESERVE data(20) Resize, keeping existing data
259
262
  ERASE data Delete array
260
263
  OPTION BASE 1 Set array index base
261
264
  ```
262
265
 
266
+ `DIM a(n)` is inclusive: it spans indices base..n, so the declared top index is valid. A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
267
+
263
268
  ## Control flow
264
269
 
265
270
  ```
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.11.1'
31
+ __version__ = '0.13.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
 
@@ -53,8 +57,9 @@ def main():
53
57
  quiet = '--quiet' in args or '-q' in args
54
58
  json_mode = '--json' in args
55
59
  agent_mode = '--agent' in args
60
+ spec_mode = '--spec' in args
56
61
  seed_val = None
57
- for flag in ('--quiet', '-q', '--json', '--agent'):
62
+ for flag in ('--quiet', '-q', '--json', '--agent', '--spec'):
58
63
  args = [a for a in args if a != flag]
59
64
  # Parse --seed N
60
65
  filtered = []
@@ -78,16 +83,48 @@ def main():
78
83
  print("Usage:")
79
84
  print(" qubasic Interactive REPL")
80
85
  print(" qubasic script.qb Run a script file")
81
- print(" qubasic --quiet script Suppress banner and progress")
86
+ print(" qubasic --quiet script Suppress banner and progress (also -q)")
82
87
  print(" qubasic --json script Output results as JSON")
83
88
  print(" qubasic --agent script Confine file writes to the working dir")
84
89
  print(" qubasic --seed N script Set random seed for reproducibility")
85
- print(" qubasic --version Show version")
86
- print(" qubasic --help Show this help")
90
+ print(" qubasic --version Show version (also -v)")
91
+ print(" qubasic --help Show this help (also -h)")
92
+ print(" python -m qubasic_core Run without the installed console script")
87
93
  print()
88
94
  print("Type HELP inside the REPL for full command reference.")
89
95
  sys.exit(0)
90
96
 
97
+ if spec_mode:
98
+ # Machine-readable contract for agents: commands, gates, functions,
99
+ # constants, and conventions for the installed version.
100
+ from qubasic_core.engine import GATE_TABLE
101
+ from qubasic_core.expression import ExpressionMixin
102
+
103
+ def _help1(mname):
104
+ doc = (getattr(getattr(QBasicTerminal, mname, None), '__doc__', '') or '').strip()
105
+ return doc.split('\n')[0].strip()
106
+
107
+ cmds = []
108
+ for tbl, takes in ((QBasicTerminal._CMD_WITH_ARG, True),
109
+ (QBasicTerminal._CMD_NO_ARG, False)):
110
+ for cname, mname in tbl.items():
111
+ cmds.append({'name': cname, 'takes_arg': takes, 'help': _help1(mname)})
112
+ spec = {
113
+ 'name': 'qubasic',
114
+ 'version': __version__,
115
+ 'bit_order': 'little-endian (qubit 0 = rightmost bit)',
116
+ 'commands': sorted(cmds, key=lambda c: c['name']),
117
+ 'gates': sorted(GATE_TABLE.keys()),
118
+ 'functions': sorted(
119
+ set(ExpressionMixin._SAFE_FUNCS)
120
+ | {'RND', 'TIMER', 'POS', 'PEEK', 'USR', 'EOF', 'FRE',
121
+ 'LEFT$', 'RIGHT$', 'MID$', 'CHR$', 'STR$', 'HEX$', 'BIN$',
122
+ 'ASC', 'VAL', 'INSTR', 'LEN'}),
123
+ 'constants': sorted(ExpressionMixin._SAFE_CONSTS.keys()),
124
+ }
125
+ print(_json.dumps(spec, indent=2))
126
+ sys.exit(0)
127
+
91
128
  term = QBasicTerminal()
92
129
  # JSON mode implies agent use, so confine file writes to the working dir.
93
130
  term.agent_mode = agent_mode or json_mode
@@ -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
 
@@ -171,6 +184,12 @@ class ControlFlowMixin:
171
184
  # Quoted literal: emit verbatim (no substitution, no SPC/TAB).
172
185
  if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
173
186
  return item[1:-1]
187
+ # A mid-circuit measurement bit (MEAS/SYNDROME -> var) has no single
188
+ # value in standard mode; it is resolved per shot inside the if_test.
189
+ # Show that instead of the placeholder 0.
190
+ cb = getattr(self, '_classical_bits', None)
191
+ if cb and item in cb:
192
+ return f"<{item}: mid-circuit bit, resolved per shot>"
174
193
  text = self._substitute_vars(item, run_vars)
175
194
 
176
195
  def _spaces(m):
@@ -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
@@ -124,6 +124,18 @@ def _basic_xor(a: Any, b: Any) -> int:
124
124
  return _as_int(a) ^ _as_int(b)
125
125
 
126
126
 
127
+ def _basic_round(x: Any, ndigits: Any = None) -> float:
128
+ """Round half away from zero (BASIC convention), not Python's half-to-even.
129
+
130
+ round(2.5) == 3, round(-2.5) == -3, round(2.345, 2) == 2.35.
131
+ """
132
+ nd = int(ndigits) if ndigits is not None else 0
133
+ f = 10 ** nd
134
+ y = float(x) * f
135
+ r = math.floor(y + 0.5) if y >= 0 else math.ceil(y - 0.5)
136
+ return (r / f) if nd else float(r)
137
+
138
+
127
139
  class ExpressionMixin:
128
140
  """AST-based safe expression evaluation. No eval().
129
141
 
@@ -138,7 +150,7 @@ class ExpressionMixin:
138
150
  # INT floors toward negative infinity, as in QBASIC (INT(-3.2) = -4).
139
151
  # FIX truncates toward zero (FIX(-3.2) = -3) for the other convention.
140
152
  'abs': abs, 'int': math.floor, 'fix': math.trunc, 'float': float,
141
- 'min': min, 'max': max, 'round': round, 'len': len,
153
+ 'min': min, 'max': max, 'round': _basic_round, 'len': len,
142
154
  'ceil': math.ceil, 'floor': math.floor,
143
155
  }
144
156
  _SAFE_CONSTS = {
@@ -203,16 +215,18 @@ class ExpressionMixin:
203
215
  result = op(result, self._ast_eval(val, ns))
204
216
  return result
205
217
  if isinstance(node, ast.Compare):
206
- left = self._ast_eval(node.left, ns)
207
- for op_node, comparator in zip(node.ops, node.comparators):
208
- op = self._AST_OPS.get(type(op_node))
209
- if op is None:
210
- raise ValueError(f"UNSUPPORTED OP: {type(op_node).__name__}")
211
- right = self._ast_eval(comparator, ns)
212
- if not op(left, right):
213
- return False
214
- left = right
215
- return True
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))
216
230
  if isinstance(node, ast.Call):
217
231
  if not isinstance(node.func, ast.Name):
218
232
  raise ValueError("ONLY SIMPLE FUNCTION CALLS ALLOWED")
@@ -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
  """
@@ -18,13 +18,13 @@ 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
20
  RE_DIM = re.compile(r'DIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
21
- RE_REDIM = re.compile(r'REDIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
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)
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)
@@ -29,8 +29,11 @@ def _chr_fn(n: float) -> str:
29
29
  return chr(int(n))
30
30
 
31
31
  def _str_fn(n: float) -> str:
32
+ # BASIC reserves a leading space for the sign of a non-negative number,
33
+ # so STR$(42) is " 42" and STR$(-5) is "-5".
32
34
  v = float(n)
33
- return str(int(v)) if v == int(v) else str(v)
35
+ s = str(int(v)) if v == int(v) else str(v)
36
+ return s if v < 0 else ' ' + s
34
37
 
35
38
  def _val_fn(s: str) -> float:
36
39
  try:
@@ -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()),
@@ -1239,7 +1255,25 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1239
1255
  2^n statevector that cannot be displayed anyway.
1240
1256
  """
1241
1257
  if getattr(self, '_pending_set_density', None) is not None:
1242
- self.last_sv = None # mixed state: no pure statevector to extract
1258
+ # Mixed state: no pure statevector. Capture the density matrix so
1259
+ # DENSITY works after a measured run, not just a no-MEASURE one.
1260
+ 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
1243
1277
  return
1244
1278
  if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
1245
1279
  self.last_sv = None
@@ -1281,9 +1315,24 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1281
1315
  }
1282
1316
 
1283
1317
  def _run_no_measure(self, qc, qc_sv, t0: float) -> None:
1284
- """Execute the no-MEASURE path: statevector only, no shots."""
1318
+ """Execute the no-MEASURE path: statevector (or density matrix), no shots."""
1285
1319
  too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
1286
- if too_large:
1320
+ 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.
1324
+ 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
+ elif too_large:
1287
1336
  self.last_sv = None
1288
1337
  else:
1289
1338
  try:
@@ -1912,13 +1961,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1912
1961
  if not m:
1913
1962
  return False
1914
1963
  name = m.group(1)
1964
+ base = getattr(self, '_option_base', 0)
1915
1965
  dims = [int(d.strip()) for d in m.group(2).split(',')]
1966
+ # Inclusive sizing (QBASIC): DIM a(n) spans indices base..n, so the
1967
+ # declared top index n is valid (n - base + 1 slots per dimension).
1968
+ sizes = [max(0, d - base + 1) for d in dims]
1916
1969
  total = 1
1917
- for d in dims:
1918
- total *= d
1970
+ for s in sizes:
1971
+ total *= s
1919
1972
  self.arrays[name] = [0.0] * total
1920
- if len(dims) > 1:
1921
- self._array_dims[name] = dims
1973
+ if len(sizes) > 1:
1974
+ self._array_dims[name] = sizes
1975
+ self._dimmed_arrays.add(name)
1922
1976
  return True
1923
1977
 
1924
1978
  def _try_exec_dim_type(self, stmt: str) -> bool:
@@ -1943,20 +1997,27 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1943
1997
  return True
1944
1998
 
1945
1999
  def _try_exec_redim(self, stmt: str) -> bool:
1946
- """Handle REDIM name(size) — resize an existing array."""
2000
+ """Handle REDIM [PRESERVE] name(size) — resize an array.
2001
+
2002
+ Plain REDIM clears to zeros (QBASIC semantics); REDIM PRESERVE keeps the
2003
+ existing elements. Sizing is inclusive (REDIM a(n) spans indices base..n).
2004
+ """
1947
2005
  m = RE_REDIM.match(stmt)
1948
2006
  if not m:
1949
2007
  return False
1950
- name = m.group(1)
1951
- new_size = int(m.group(2))
2008
+ preserve = bool(m.group(1))
2009
+ name = m.group(2)
2010
+ base = getattr(self, '_option_base', 0)
2011
+ new_len = max(0, int(m.group(3)) - base + 1)
1952
2012
  old = self.arrays.get(name, [])
1953
- if isinstance(old, list):
1954
- if new_size > len(old):
1955
- self.arrays[name] = old + [0.0] * (new_size - len(old))
2013
+ if preserve and isinstance(old, list):
2014
+ if new_len > len(old):
2015
+ self.arrays[name] = old + [0.0] * (new_len - len(old))
1956
2016
  else:
1957
- self.arrays[name] = old[:new_size]
2017
+ self.arrays[name] = old[:new_len]
1958
2018
  else:
1959
- self.arrays[name] = [0.0] * new_size
2019
+ self.arrays[name] = [0.0] * new_len
2020
+ self._dimmed_arrays.add(name)
1960
2021
  return True
1961
2022
 
1962
2023
  def _try_exec_erase(self, stmt: str) -> bool:
@@ -1967,6 +2028,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1967
2028
  name = m.group(1)
1968
2029
  if name in self.arrays:
1969
2030
  del self.arrays[name]
2031
+ self._array_dims.pop(name, None)
2032
+ self._dimmed_arrays.discard(name)
1970
2033
  return True
1971
2034
 
1972
2035
  def _try_exec_get(self, stmt: str, run_vars: dict) -> bool:
@@ -487,8 +487,9 @@ class TestStrings(unittest.TestCase):
487
487
  self.assertEqual(_instr("HELLO", "XYZ"), 0.0)
488
488
  self.assertEqual(_hex_fn(255), "FF")
489
489
  self.assertEqual(_bin_fn(5), "101")
490
- self.assertEqual(_str_fn(42.0), "42")
491
- self.assertEqual(_str_fn(3.14), "3.14")
490
+ self.assertEqual(_str_fn(42.0), " 42") # leading space for non-negative
491
+ self.assertEqual(_str_fn(3.14), " 3.14")
492
+ self.assertEqual(_str_fn(-5.0), "-5")
492
493
  self.assertEqual(_val_fn("42"), 42.0)
493
494
  self.assertEqual(_val_fn("abc"), 0.0)
494
495
  self.assertEqual(_len_fn("HELLO"), 5.0)
@@ -1048,7 +1049,7 @@ class TestProgramManagement(unittest.TestCase):
1048
1049
  _, out = capture(t_dim.cmd_run)
1049
1050
  self.assertIn('matrix', t_dim.arrays)
1050
1051
  self.assertIsInstance(t_dim.arrays['matrix'], list)
1051
- self.assertEqual(len(t_dim.arrays['matrix']), 9)
1052
+ self.assertEqual(len(t_dim.arrays['matrix']), 16) # inclusive: (0..3)^2
1052
1053
 
1053
1054
  # DIM single
1054
1055
  t_dim2 = QBasicTerminal()
@@ -1056,7 +1057,7 @@ class TestProgramManagement(unittest.TestCase):
1056
1057
  t_dim2.process('20 END')
1057
1058
  capture(t_dim2.cmd_run)
1058
1059
  self.assertIn('arr', t_dim2.arrays)
1059
- self.assertEqual(len(t_dim2.arrays['arr']), 5)
1060
+ self.assertEqual(len(t_dim2.arrays['arr']), 6) # inclusive: indices 0..5
1060
1061
 
1061
1062
  # IMPORT namespace
1062
1063
  with tempfile.NamedTemporaryFile(mode='w', suffix='.qb', dir='.', delete=False) as f:
@@ -2305,20 +2306,26 @@ class TestGapCoverage(unittest.TestCase):
2305
2306
 
2306
2307
  # 16 REDIM
2307
2308
  def test_redim(self):
2308
- """REDIM resizes an existing array, preserving or truncating data."""
2309
- # DIM first
2309
+ """REDIM resizes (inclusive sizing); plain clears, PRESERVE keeps."""
2310
+ # DIM a(5) spans indices 0..5 -> 6 slots (inclusive sizing).
2310
2311
  self.t._try_exec_dim('DIM arr(5)')
2311
- self.assertEqual(len(self.t.arrays['arr']), 5)
2312
- # REDIM larger
2312
+ self.assertEqual(len(self.t.arrays['arr']), 6)
2313
+ self.t.arrays['arr'][2] = 99.0
2314
+ # Plain REDIM clears to zeros.
2313
2315
  self.t._try_exec_redim('REDIM arr(8)')
2314
- self.assertEqual(len(self.t.arrays['arr']), 8)
2315
- self.assertEqual(self.t.arrays['arr'][:5], [0.0] * 5)
2316
- # REDIM smaller
2317
- self.t._try_exec_redim('REDIM arr(3)')
2318
- self.assertEqual(len(self.t.arrays['arr']), 3)
2319
- # REDIM non-existent array creates it
2316
+ self.assertEqual(len(self.t.arrays['arr']), 9)
2317
+ self.assertEqual(self.t.arrays['arr'][2], 0.0)
2318
+ # REDIM PRESERVE keeps existing data.
2319
+ self.t.arrays['arr'][1] = 7.0
2320
+ self.t._try_exec_redim('REDIM PRESERVE arr(10)')
2321
+ self.assertEqual(len(self.t.arrays['arr']), 11)
2322
+ self.assertEqual(self.t.arrays['arr'][1], 7.0)
2323
+ # PRESERVE smaller truncates.
2324
+ self.t._try_exec_redim('REDIM PRESERVE arr(3)')
2325
+ self.assertEqual(len(self.t.arrays['arr']), 4)
2326
+ # REDIM on a non-existent array creates it.
2320
2327
  self.t._try_exec_redim('REDIM newary(4)')
2321
- self.assertEqual(len(self.t.arrays['newary']), 4)
2328
+ self.assertEqual(len(self.t.arrays['newary']), 5)
2322
2329
 
2323
2330
 
2324
2331
  # =====================================================================
@@ -2096,6 +2096,114 @@ class TestConventionAndStateRegressions(unittest.TestCase):
2096
2096
  self.assertIsNotNone(t.last_counts) # ran; not blocked as a "command"
2097
2097
 
2098
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
+
2154
+
2155
+ class TestConventionFixesV2(unittest.TestCase):
2156
+ """round half-away, chained-comparison guard, inclusive DIM, REDIM
2157
+ PRESERVE, MEAS print hint, DENSITY after MEASURE."""
2158
+
2159
+ def setUp(self):
2160
+ self.t = QBasicTerminal(); self.t.num_qubits = 1
2161
+
2162
+ def _runp(self, lines, nq=1):
2163
+ t = QBasicTerminal(); t.num_qubits = nq
2164
+ for l in lines:
2165
+ t.process(l, track_undo=False)
2166
+ _, out = capture(t.cmd_run)
2167
+ t._out = out
2168
+ return t
2169
+
2170
+ def test_round_half_away_from_zero(self):
2171
+ self.assertEqual(self.t.eval_expr('round(2.5)'), 3.0)
2172
+ self.assertEqual(self.t.eval_expr('round(3.5)'), 4.0)
2173
+ self.assertEqual(self.t.eval_expr('round(-2.5)'), -3.0)
2174
+ self.assertAlmostEqual(self.t.eval_expr('round(2.345, 2)'), 2.35)
2175
+
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')
2180
+
2181
+ def test_dim_inclusive_top_index(self):
2182
+ t = self._runp(['10 DIM a(5)', '20 LET a(5)=7', '30 LET v=a(5)'])
2183
+ self.assertEqual(t.variables.get('v'), 7.0)
2184
+ t2 = self._runp(['10 DIM a(5)', '20 LET a(6)=1'])
2185
+ self.assertIn('OUT OF RANGE', t2._out)
2186
+
2187
+ def test_redim_clear_vs_preserve(self):
2188
+ t = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM a(5)', '40 LET v=a(1)'])
2189
+ self.assertEqual(t.variables.get('v'), 0.0)
2190
+ t2 = self._runp(['10 DIM a(3)', '20 LET a(1)=9', '30 REDIM PRESERVE a(5)', '40 LET v=a(1)'])
2191
+ self.assertEqual(t2.variables.get('v'), 9.0)
2192
+
2193
+ def test_meas_print_hint(self):
2194
+ t = self._runp(['10 X 0', '20 MEAS 0 -> m', '30 PRINT m', '40 MEASURE'])
2195
+ self.assertIn('mid-circuit', t._out)
2196
+
2197
+ def test_density_after_measure(self):
2198
+ t = QBasicTerminal(); t.num_qubits = 1
2199
+ t.process('SET_DENSITY [[0.5,0],[0,0.5]]', track_undo=False)
2200
+ for l in ['10 H 0', '20 MEASURE']:
2201
+ t.process(l, track_undo=False)
2202
+ capture(t.cmd_run)
2203
+ _, dout = capture(t.cmd_density, '')
2204
+ self.assertIn('Density matrix', dout)
2205
+
2206
+
2099
2207
  if __name__ == '__main__':
2100
2208
  if hasattr(sys.stdout, 'reconfigure'):
2101
2209
  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