qubasic 0.12.0__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.12.0 → qubasic-0.13.0}/CHANGELOG.md +17 -0
  2. {qubasic-0.12.0/qubasic.egg-info → qubasic-0.13.0}/PKG-INFO +5 -3
  3. {qubasic-0.12.0 → qubasic-0.13.0}/README.md +4 -2
  4. {qubasic-0.12.0 → qubasic-0.13.0}/pyproject.toml +1 -1
  5. {qubasic-0.12.0 → qubasic-0.13.0/qubasic.egg-info}/PKG-INFO +5 -3
  6. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/cli.py +33 -1
  8. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/control_flow.py +6 -0
  9. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/expression.py +25 -11
  10. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/patterns.py +1 -1
  11. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/strings.py +4 -1
  12. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/terminal.py +41 -13
  13. {qubasic-0.12.0 → qubasic-0.13.0}/tests/test_features.py +22 -15
  14. {qubasic-0.12.0 → qubasic-0.13.0}/tests/test_qubasic.py +52 -0
  15. {qubasic-0.12.0 → qubasic-0.13.0}/LICENSE +0 -0
  16. {qubasic-0.12.0 → qubasic-0.13.0}/MANIFEST.in +0 -0
  17. {qubasic-0.12.0 → qubasic-0.13.0}/examples/bell.qb +0 -0
  18. {qubasic-0.12.0 → qubasic-0.13.0}/examples/grover3.qb +0 -0
  19. {qubasic-0.12.0 → qubasic-0.13.0}/examples/locc_teleport.qb +0 -0
  20. {qubasic-0.12.0 → qubasic-0.13.0}/examples/sweep_rx.qb +0 -0
  21. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic.egg-info/SOURCES.txt +0 -0
  22. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic.egg-info/dependency_links.txt +0 -0
  23. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic.egg-info/entry_points.txt +0 -0
  24. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic.egg-info/requires.txt +0 -0
  25. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic.egg-info/top_level.txt +0 -0
  26. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/__main__.py +0 -0
  27. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/algorithms.py +0 -0
  28. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/algos2.py +0 -0
  29. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/analysis.py +0 -0
  30. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/backend.py +0 -0
  31. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/benchmarking.py +0 -0
  32. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/bosonic.py +0 -0
  33. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/classic.py +0 -0
  34. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/debug.py +0 -0
  35. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/demos.py +0 -0
  36. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/display.py +0 -0
  37. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/dynamics.py +0 -0
  38. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/engine.py +0 -0
  39. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/engine_state.py +0 -0
  40. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/errors.py +0 -0
  41. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/exec_context.py +0 -0
  42. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/executor.py +0 -0
  43. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/file_io.py +0 -0
  44. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/gates.py +0 -0
  45. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/help_text.py +0 -0
  46. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/io_protocol.py +0 -0
  47. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/locc.py +0 -0
  48. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/locc_commands.py +0 -0
  49. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/locc_display.py +0 -0
  50. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/locc_engine.py +0 -0
  51. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/locc_execution.py +0 -0
  52. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/memory.py +0 -0
  53. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/mock_backend.py +0 -0
  54. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/noise_mixin.py +0 -0
  55. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/parser.py +0 -0
  56. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/pauliprop.py +0 -0
  57. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/profiler.py +0 -0
  58. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/program_mgmt.py +0 -0
  59. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/protocol.py +0 -0
  60. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/qec.py +0 -0
  61. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/qol.py +0 -0
  62. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/qudits.py +0 -0
  63. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/resources.py +0 -0
  64. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/scope.py +0 -0
  65. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/screen.py +0 -0
  66. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/state_display.py +0 -0
  67. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/statements.py +0 -0
  68. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/subs.py +0 -0
  69. {qubasic-0.12.0 → qubasic-0.13.0}/qubasic_core/sweep.py +0 -0
  70. {qubasic-0.12.0 → qubasic-0.13.0}/setup.cfg +0 -0
@@ -1,5 +1,22 @@
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
+
3
20
  ## 0.12.0 (2026-06-19)
4
21
 
5
22
  Correctness and robustness pass on the classic-BASIC layer, from an extended
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.12.0
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
 
@@ -256,12 +257,13 @@ DIM data(10) 1D array
256
257
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
258
  LET data(0) = PI
258
259
  LET names$(0) = "alice" String array (name$ elements hold strings)
259
- REDIM data(20) Resize (preserves existing data)
260
+ REDIM data(20) Resize, clearing to zeros
261
+ REDIM PRESERVE data(20) Resize, keeping existing data
260
262
  ERASE data Delete array
261
263
  OPTION BASE 1 Set array index base
262
264
  ```
263
265
 
264
- A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
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.
265
267
 
266
268
  ## Control flow
267
269
 
@@ -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
 
@@ -223,12 +224,13 @@ DIM data(10) 1D array
223
224
  DIM matrix(3, 3) Multi-dimensional (flat storage)
224
225
  LET data(0) = PI
225
226
  LET names$(0) = "alice" String array (name$ elements hold strings)
226
- REDIM data(20) Resize (preserves existing data)
227
+ REDIM data(20) Resize, clearing to zeros
228
+ REDIM PRESERVE data(20) Resize, keeping existing data
227
229
  ERASE data Delete array
228
230
  OPTION BASE 1 Set array index base
229
231
  ```
230
232
 
231
- A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
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.
232
234
 
233
235
  ## Control flow
234
236
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.12.0"
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.12.0
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
 
@@ -256,12 +257,13 @@ DIM data(10) 1D array
256
257
  DIM matrix(3, 3) Multi-dimensional (flat storage)
257
258
  LET data(0) = PI
258
259
  LET names$(0) = "alice" String array (name$ elements hold strings)
259
- REDIM data(20) Resize (preserves existing data)
260
+ REDIM data(20) Resize, clearing to zeros
261
+ REDIM PRESERVE data(20) Resize, keeping existing data
260
262
  ERASE data Delete array
261
263
  OPTION BASE 1 Set array index base
262
264
  ```
263
265
 
264
- A `DIM`med array enforces its declared bounds on write; an undimensioned array grows on first assignment.
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.
265
267
 
266
268
  ## Control flow
267
269
 
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.12.0'
31
+ __version__ = '0.13.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -57,8 +57,9 @@ def main():
57
57
  quiet = '--quiet' in args or '-q' in args
58
58
  json_mode = '--json' in args
59
59
  agent_mode = '--agent' in args
60
+ spec_mode = '--spec' in args
60
61
  seed_val = None
61
- for flag in ('--quiet', '-q', '--json', '--agent'):
62
+ for flag in ('--quiet', '-q', '--json', '--agent', '--spec'):
62
63
  args = [a for a in args if a != flag]
63
64
  # Parse --seed N
64
65
  filtered = []
@@ -93,6 +94,37 @@ def main():
93
94
  print("Type HELP inside the REPL for full command reference.")
94
95
  sys.exit(0)
95
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
+
96
128
  term = QBasicTerminal()
97
129
  # JSON mode implies agent use, so confine file writes to the working dir.
98
130
  term.agent_mode = agent_mode or json_mode
@@ -184,6 +184,12 @@ class ControlFlowMixin:
184
184
  # Quoted literal: emit verbatim (no substitution, no SPC/TAB).
185
185
  if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
186
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>"
187
193
  text = self._substitute_vars(item, run_vars)
188
194
 
189
195
  def _spaces(m):
@@ -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")
@@ -18,7 +18,7 @@ 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)
@@ -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:
@@ -1255,7 +1255,25 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1255
1255
  2^n statevector that cannot be displayed anyway.
1256
1256
  """
1257
1257
  if getattr(self, '_pending_set_density', None) is not None:
1258
- 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
1259
1277
  return
1260
1278
  if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
1261
1279
  self.last_sv = None
@@ -1943,13 +1961,17 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1943
1961
  if not m:
1944
1962
  return False
1945
1963
  name = m.group(1)
1964
+ base = getattr(self, '_option_base', 0)
1946
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]
1947
1969
  total = 1
1948
- for d in dims:
1949
- total *= d
1970
+ for s in sizes:
1971
+ total *= s
1950
1972
  self.arrays[name] = [0.0] * total
1951
- if len(dims) > 1:
1952
- self._array_dims[name] = dims
1973
+ if len(sizes) > 1:
1974
+ self._array_dims[name] = sizes
1953
1975
  self._dimmed_arrays.add(name)
1954
1976
  return True
1955
1977
 
@@ -1975,20 +1997,26 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1975
1997
  return True
1976
1998
 
1977
1999
  def _try_exec_redim(self, stmt: str) -> bool:
1978
- """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
+ """
1979
2005
  m = RE_REDIM.match(stmt)
1980
2006
  if not m:
1981
2007
  return False
1982
- name = m.group(1)
1983
- 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)
1984
2012
  old = self.arrays.get(name, [])
1985
- if isinstance(old, list):
1986
- if new_size > len(old):
1987
- 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))
1988
2016
  else:
1989
- self.arrays[name] = old[:new_size]
2017
+ self.arrays[name] = old[:new_len]
1990
2018
  else:
1991
- self.arrays[name] = [0.0] * new_size
2019
+ self.arrays[name] = [0.0] * new_len
1992
2020
  self._dimmed_arrays.add(name)
1993
2021
  return True
1994
2022
 
@@ -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
  # =====================================================================
@@ -2152,6 +2152,58 @@ class TestDeepFixRegressions(unittest.TestCase):
2152
2152
  self.assertEqual(t.eval_expr('sq(5)'), 25.0)
2153
2153
 
2154
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
+
2155
2207
  if __name__ == '__main__':
2156
2208
  if hasattr(sys.stdout, 'reconfigure'):
2157
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
File without changes