qubasic 0.6.0__tar.gz → 0.6.3__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 (63) hide show
  1. {qubasic-0.6.0 → qubasic-0.6.3}/CHANGELOG.md +22 -1
  2. {qubasic-0.6.0/qubasic.egg-info → qubasic-0.6.3}/PKG-INFO +12 -8
  3. {qubasic-0.6.0 → qubasic-0.6.3}/README.md +11 -7
  4. {qubasic-0.6.0 → qubasic-0.6.3}/pyproject.toml +16 -7
  5. {qubasic-0.6.0 → qubasic-0.6.3/qubasic.egg-info}/PKG-INFO +12 -8
  6. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic.egg-info/SOURCES.txt +4 -2
  7. qubasic-0.6.3/qubasic.egg-info/entry_points.txt +2 -0
  8. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic.egg-info/top_level.txt +0 -1
  9. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/__init__.py +1 -1
  10. qubasic-0.6.3/qubasic_core/__main__.py +6 -0
  11. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/analysis.py +1 -1
  12. qubasic-0.6.0/qubasic.py → qubasic-0.6.3/qubasic_core/cli.py +4 -3
  13. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/control_flow.py +77 -37
  14. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/engine.py +2 -1
  15. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/engine_state.py +15 -0
  16. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/exec_context.py +1 -1
  17. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/executor.py +47 -19
  18. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/expression.py +1 -1
  19. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/file_io.py +2 -1
  20. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/gates.py +2 -2
  21. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/locc_engine.py +3 -3
  22. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/locc_execution.py +5 -5
  23. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/memory.py +8 -6
  24. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/parser.py +8 -0
  25. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/qol.py +7 -15
  26. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/terminal.py +30 -12
  27. qubasic-0.6.3/tests/test_features.py +2690 -0
  28. qubasic-0.6.3/tests/test_qubasic.py +1524 -0
  29. qubasic-0.6.0/qubasic.egg-info/entry_points.txt +0 -2
  30. qubasic-0.6.0/qubasic_core/__main__.py +0 -6
  31. {qubasic-0.6.0 → qubasic-0.6.3}/LICENSE +0 -0
  32. {qubasic-0.6.0 → qubasic-0.6.3}/MANIFEST.in +0 -0
  33. {qubasic-0.6.0 → qubasic-0.6.3}/examples/bell.qb +0 -0
  34. {qubasic-0.6.0 → qubasic-0.6.3}/examples/grover3.qb +0 -0
  35. {qubasic-0.6.0 → qubasic-0.6.3}/examples/locc_teleport.qb +0 -0
  36. {qubasic-0.6.0 → qubasic-0.6.3}/examples/sweep_rx.qb +0 -0
  37. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic.egg-info/dependency_links.txt +0 -0
  38. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic.egg-info/requires.txt +0 -0
  39. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/backend.py +0 -0
  40. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/classic.py +0 -0
  41. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/debug.py +0 -0
  42. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/demos.py +0 -0
  43. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/display.py +0 -0
  44. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/errors.py +0 -0
  45. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/help_text.py +0 -0
  46. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/io_protocol.py +0 -0
  47. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/locc.py +0 -0
  48. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/locc_commands.py +0 -0
  49. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/locc_display.py +0 -0
  50. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/mock_backend.py +0 -0
  51. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/noise_mixin.py +0 -0
  52. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/patterns.py +0 -0
  53. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/profiler.py +0 -0
  54. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/program_mgmt.py +0 -0
  55. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/protocol.py +0 -0
  56. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/scope.py +0 -0
  57. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/screen.py +0 -0
  58. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/state_display.py +0 -0
  59. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/statements.py +0 -0
  60. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/strings.py +0 -0
  61. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/subs.py +0 -0
  62. {qubasic-0.6.0 → qubasic-0.6.3}/qubasic_core/sweep.py +0 -0
  63. {qubasic-0.6.0 → qubasic-0.6.3}/setup.cfg +0 -0
@@ -1,8 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.3 (2026-06-18)
4
+
5
+ ### Fixed
6
+ - Colon-compound `@register` lines in LOCC mode now apply every gate. `@A H 0 : @A CX 0,1` (and the inherited form `@A H 0 : CX 0,1`) were captured as a single statement and mis-tokenized, so all but the first gate were silently dropped. The parser now splits them into per-statement parts with register inheritance, and the immediate-mode REPL path does the same.
7
+ - `SAVE_EXPECT`, `SAVE_PROBS`, and `SAVE_AMPS` no longer overwrite their target variable with 0 at circuit-build time. The placeholder keeps any prior value, so a program that re-runs with the SAVE line still present can read the previous result during the build pass (the post-run extraction then fills in the fresh value).
8
+ - `PRINT` honors `;` and `,` between multiple items: `PRINT "S ="; x` concatenates and `PRINT a, b` advances to the next 14-column zone. Separators inside quoted strings and inside call parentheses (`PRINT LEFT$(s$, 3)`) are no longer treated as item breaks.
9
+ - `PRINT` variable substitution no longer rewrites identifiers inside quoted string literals, so a label such as `PRINT "value of S"` is emitted verbatim even when `S` is a defined variable.
10
+
11
+ ## 0.6.2 (2026-06-18)
12
+
13
+ ### Fixed
14
+ - Changing the qubit count after a run (via `QUBITS n` or `POKE $D000`) now invalidates the cached statevector, so `MAP`, a `PEEK` of the qubit-state block, and `BLOCH` no longer reshape a stale statevector to the new qubit count and crash.
15
+
16
+ ## 0.6.1 (2026-06-18)
17
+
18
+ ### Fixed
19
+ - Changing noise strength between runs (for example `NOISE amplitude_damping 0.1` then `0.9`) now rebuilds the circuit instead of reusing the first noise level, so sequential noise comparisons are correct.
20
+
21
+ ### Changed
22
+ - The CLI now lives in `qubasic_core/cli.py`; the `qubasic` console script and `python -m qubasic_core` are unchanged, but the former top-level `qubasic` module is removed. Test suites moved under `tests/`.
23
+
3
24
  ## 0.6.0 (2026-06-18)
4
25
 
5
- Correctness, scalability, and usability pass (61 fixes).
26
+ Correctness, scalability, and usability improvements.
6
27
 
7
28
  ### Fixed
8
29
  - **Time-travel debugging now works**: STEP records statevector checkpoints, so REWIND/FORWARD/HISTORY operate (previously `_checkpoint_sv` was never called).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.6.0
3
+ Version: 0.6.3
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -73,11 +73,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
73
73
  ## Usage
74
74
 
75
75
  ```
76
- python qubasic.py Interactive REPL
77
- python qubasic.py script.qb Run a script file
78
- python qubasic.py --quiet script Suppress banner, output results only
79
- python qubasic.py --json script Machine-readable JSON output
80
- python qubasic.py --help Show CLI help
76
+ qubasic Interactive REPL (installed console script)
77
+ python -m qubasic_core Same, without installing
78
+ qubasic script.qb Run a script file
79
+ qubasic --quiet script Suppress banner, output results only
80
+ qubasic --json script Machine-readable JSON output
81
+ qubasic --help Show CLI help
81
82
  ```
82
83
 
83
84
  Headless (no REPL):
@@ -685,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
685
686
  ## JSON output
686
687
 
687
688
  ```
688
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
689
690
  ```
690
691
 
691
692
  ```json
@@ -699,8 +700,9 @@ python qubasic.py --json examples/bell.qb
699
700
  ## Architecture
700
701
 
701
702
  ```
702
- qubasic.py CLI entry point
703
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
704
706
  engine_state.py Engine: standalone state container
705
707
  terminal.py QBasicTerminal: REPL + command dispatch
706
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -729,6 +731,8 @@ qubasic_core/
729
731
  demos.py Built-in demo circuits
730
732
  protocol.py TerminalProtocol (mixin contract)
731
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
732
736
  ```
733
737
 
734
738
  `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
@@ -40,11 +40,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
40
40
  ## Usage
41
41
 
42
42
  ```
43
- python qubasic.py Interactive REPL
44
- python qubasic.py script.qb Run a script file
45
- python qubasic.py --quiet script Suppress banner, output results only
46
- python qubasic.py --json script Machine-readable JSON output
47
- python qubasic.py --help Show CLI help
43
+ qubasic Interactive REPL (installed console script)
44
+ python -m qubasic_core Same, without installing
45
+ qubasic script.qb Run a script file
46
+ qubasic --quiet script Suppress banner, output results only
47
+ qubasic --json script Machine-readable JSON output
48
+ qubasic --help Show CLI help
48
49
  ```
49
50
 
50
51
  Headless (no REPL):
@@ -652,7 +653,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
652
653
  ## JSON output
653
654
 
654
655
  ```
655
- python qubasic.py --json examples/bell.qb
656
+ qubasic --json examples/bell.qb
656
657
  ```
657
658
 
658
659
  ```json
@@ -666,8 +667,9 @@ python qubasic.py --json examples/bell.qb
666
667
  ## Architecture
667
668
 
668
669
  ```
669
- qubasic.py CLI entry point
670
670
  qubasic_core/
671
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
672
+ __main__.py Module entry point
671
673
  engine_state.py Engine: standalone state container
672
674
  terminal.py QBasicTerminal: REPL + command dispatch
673
675
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -696,6 +698,8 @@ qubasic_core/
696
698
  demos.py Built-in demo circuits
697
699
  protocol.py TerminalProtocol (mixin contract)
698
700
  mock_backend.py MockAerSimulator for fast testing
701
+ tests/ Test suites (test_qubasic.py, test_features.py)
702
+ examples/ Sample .qb programs
699
703
  ```
700
704
 
701
705
  `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.6.0"
7
+ version = "0.6.3"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -42,10 +42,9 @@ charts = ["plotille>=5.0"]
42
42
  readline = ["pyreadline3>=3.4; sys_platform == 'win32'"]
43
43
 
44
44
  [project.scripts]
45
- qubasic = "qubasic:main"
45
+ qubasic = "qubasic_core.cli:main"
46
46
 
47
47
  [tool.setuptools]
48
- py-modules = ["qubasic"]
49
48
  packages = ["qubasic_core"]
50
49
 
51
50
  [tool.setuptools.package-data]
@@ -66,9 +65,19 @@ skip_empty = true
66
65
 
67
66
  [tool.mypy]
68
67
  python_version = "3.10"
69
- warn_return_any = true
68
+ ignore_missing_imports = true
70
69
  warn_unused_configs = true
71
- disallow_untyped_defs = true
72
- check_untyped_defs = true
73
70
  warn_redundant_casts = true
74
- warn_unused_ignores = true
71
+ # QBasicTerminal composes 20 mixins; each mixin accesses attributes the host
72
+ # class provides, which mypy cannot resolve in isolation. The codebase is
73
+ # intentionally gradually typed, so the strict per-method settings and the
74
+ # cross-mixin attribute check are disabled while keeping mypy useful for real
75
+ # type mismatches, undefined names, and signature checks.
76
+ disallow_untyped_defs = false
77
+ check_untyped_defs = false
78
+ warn_return_any = false
79
+ warn_unused_ignores = false
80
+ # attr-defined: cross-mixin attributes the host class provides.
81
+ # has-type: types behind the lazy __getattr__ re-exports cannot be inferred.
82
+ # misc/union-attr: mixin base-class attribute overlap and optional access.
83
+ disable_error_code = ["attr-defined", "has-type", "misc", "union-attr"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.6.0
3
+ Version: 0.6.3
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -73,11 +73,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
73
73
  ## Usage
74
74
 
75
75
  ```
76
- python qubasic.py Interactive REPL
77
- python qubasic.py script.qb Run a script file
78
- python qubasic.py --quiet script Suppress banner, output results only
79
- python qubasic.py --json script Machine-readable JSON output
80
- python qubasic.py --help Show CLI help
76
+ qubasic Interactive REPL (installed console script)
77
+ python -m qubasic_core Same, without installing
78
+ qubasic script.qb Run a script file
79
+ qubasic --quiet script Suppress banner, output results only
80
+ qubasic --json script Machine-readable JSON output
81
+ qubasic --help Show CLI help
81
82
  ```
82
83
 
83
84
  Headless (no REPL):
@@ -685,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
685
686
  ## JSON output
686
687
 
687
688
  ```
688
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
689
690
  ```
690
691
 
691
692
  ```json
@@ -699,8 +700,9 @@ python qubasic.py --json examples/bell.qb
699
700
  ## Architecture
700
701
 
701
702
  ```
702
- qubasic.py CLI entry point
703
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
704
706
  engine_state.py Engine: standalone state container
705
707
  terminal.py QBasicTerminal: REPL + command dispatch
706
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -729,6 +731,8 @@ qubasic_core/
729
731
  demos.py Built-in demo circuits
730
732
  protocol.py TerminalProtocol (mixin contract)
731
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
732
736
  ```
733
737
 
734
738
  `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
@@ -3,7 +3,6 @@ LICENSE
3
3
  MANIFEST.in
4
4
  README.md
5
5
  pyproject.toml
6
- qubasic.py
7
6
  examples/bell.qb
8
7
  examples/grover3.qb
9
8
  examples/locc_teleport.qb
@@ -19,6 +18,7 @@ qubasic_core/__main__.py
19
18
  qubasic_core/analysis.py
20
19
  qubasic_core/backend.py
21
20
  qubasic_core/classic.py
21
+ qubasic_core/cli.py
22
22
  qubasic_core/control_flow.py
23
23
  qubasic_core/debug.py
24
24
  qubasic_core/demos.py
@@ -54,4 +54,6 @@ qubasic_core/statements.py
54
54
  qubasic_core/strings.py
55
55
  qubasic_core/subs.py
56
56
  qubasic_core/sweep.py
57
- qubasic_core/terminal.py
57
+ qubasic_core/terminal.py
58
+ tests/test_features.py
59
+ tests/test_qubasic.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qubasic = qubasic_core.cli:main
@@ -1,2 +1 @@
1
- qubasic
2
1
  qubasic_core
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.6.0'
31
+ __version__ = '0.6.3'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -0,0 +1,6 @@
1
+ """Run the CLI as a module: python -m qubasic_core [args]."""
2
+
3
+ from qubasic_core.cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -182,7 +182,7 @@ class AnalysisMixin:
182
182
  return
183
183
  n = self._active_nqubits
184
184
  sv = np.ascontiguousarray(sv).ravel()
185
- checks = []
185
+ checks: list = []
186
186
 
187
187
  # 1. SV normalization
188
188
  norm = float(np.sum(np.abs(sv)**2))
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- QUBASIC — Quantum BASIC Interactive Terminal
3
+ QUBASIC — command-line entry point.
4
4
 
5
5
  Usage:
6
- python qubasic.py Interactive REPL
7
- python qubasic.py script.qb Run a script file
6
+ qubasic Interactive REPL (installed console script)
7
+ python -m qubasic_core Same, run as a module
8
+ qubasic script.qb Run a script file
8
9
  """
9
10
 
10
11
  import sys
@@ -63,46 +63,86 @@ class ControlFlowMixin:
63
63
  self.variables[name] = val
64
64
  return True, ExecResult.ADVANCE
65
65
 
66
- def _cf_print(self, stmt: str, run_vars: dict[str, Any],
67
- parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
68
- raw_expr = parsed.expr
69
- text = self._substitute_vars(raw_expr.strip(), run_vars)
70
- # Determine trailing separator: ; suppresses newline, , advances to tab
71
- suppress_newline = raw_expr.rstrip().endswith(';')
72
- tab_advance = raw_expr.rstrip().endswith(',')
73
- if suppress_newline:
74
- text = text.rstrip().removesuffix(';').rstrip()
75
- elif tab_advance:
76
- text = text.rstrip().removesuffix(',').rstrip()
77
- # Evaluate SPC(n) and TAB(n) inline
78
- def _replace_spc(m_spc):
79
- n = int(self._eval_with_vars(m_spc.group(1), run_vars))
80
- return ' ' * max(0, n)
81
- def _replace_tab(m_tab):
82
- n = int(self._eval_with_vars(m_tab.group(1), run_vars))
83
- return ' ' * max(0, n)
84
- text = re.sub(r'\bSPC\s*\(([^)]+)\)', _replace_spc, text, flags=re.IGNORECASE)
85
- text = re.sub(r'\bTAB\s*\(([^)]+)\)', _replace_tab, text, flags=re.IGNORECASE)
86
- # Evaluate the expression
87
- if (text.startswith('"') and text.endswith('"')) or \
88
- (text.startswith("'") and text.endswith("'")):
89
- output = text[1:-1]
90
- else:
66
+ @staticmethod
67
+ def _split_print_items(expr: str) -> list[tuple[str, str]]:
68
+ """Split a PRINT argument list into (item, trailing-separator) pairs.
69
+
70
+ ';' and ',' are recognized only at top level (outside quotes and
71
+ parentheses/brackets), so PRINT LEFT$(s$, 3) stays one item and a comma
72
+ inside a quoted literal is preserved. The separator recorded with each
73
+ item is the one that follows it; '' marks the final item / no trailing
74
+ separator.
75
+ """
76
+ items: list[tuple[str, str]] = []
77
+ buf = ''
78
+ depth = 0
79
+ quote: str | None = None
80
+ for ch in expr:
81
+ if quote:
82
+ buf += ch
83
+ if ch == quote:
84
+ quote = None
85
+ elif ch in ('"', "'"):
86
+ quote = ch
87
+ buf += ch
88
+ elif ch in '([':
89
+ depth += 1
90
+ buf += ch
91
+ elif ch in ')]':
92
+ depth = max(0, depth - 1)
93
+ buf += ch
94
+ elif ch in ';,' and depth == 0:
95
+ items.append((buf.strip(), ch))
96
+ buf = ''
97
+ else:
98
+ buf += ch
99
+ if buf.strip():
100
+ items.append((buf.strip(), ''))
101
+ return items
102
+
103
+ def _eval_print_item(self, item: str, run_vars: dict[str, Any]) -> str:
104
+ """Evaluate a single PRINT item to its display string."""
105
+ item = item.strip()
106
+ if not item:
107
+ return ''
108
+ # Quoted literal: emit verbatim (no substitution, no SPC/TAB).
109
+ if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
110
+ return item[1:-1]
111
+ text = self._substitute_vars(item, run_vars)
112
+
113
+ def _spaces(m):
91
114
  try:
92
- ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else dict(run_vars) if not isinstance(run_vars, dict) else run_vars
93
- result = self._safe_eval(text, extra_ns=ns)
94
- output = str(result)
115
+ return ' ' * max(0, int(self._eval_with_vars(m.group(1), run_vars)))
95
116
  except Exception:
96
- output = text
97
- # Output with separator behavior
98
- if suppress_newline:
99
- self.io.write(output)
100
- elif tab_advance:
101
- col = len(output) % 14
102
- padding = 14 - col if col > 0 else 14
103
- self.io.write(output + ' ' * padding)
117
+ return ''
118
+ text = re.sub(r'\bSPC\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
119
+ text = re.sub(r'\bTAB\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
120
+ if not text.strip():
121
+ return text # standalone SPC/TAB -> whitespace
122
+ try:
123
+ ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
124
+ run_vars if isinstance(run_vars, dict) else dict(run_vars))
125
+ return str(self._safe_eval(text, extra_ns=ns))
126
+ except Exception:
127
+ return text
128
+
129
+ def _cf_print(self, stmt: str, run_vars: dict[str, Any],
130
+ parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
131
+ items = self._split_print_items(parsed.expr)
132
+ if not items:
133
+ self.io.writeln('') # bare PRINT -> blank line
134
+ return True, ExecResult.ADVANCE
135
+ out = ''
136
+ for item, sep in items:
137
+ out += self._eval_print_item(item, run_vars)
138
+ if sep == ',': # advance to next 14-column zone
139
+ col = len(out) % 14
140
+ out += ' ' * (14 - col if col else 14)
141
+ # A trailing ';' or ',' suppresses the newline (cursor stays on line).
142
+ if items[-1][1] in (';', ','):
143
+ self.io.write(out)
104
144
  else:
105
- self.io.writeln(output)
145
+ self.io.writeln(out)
106
146
  return True, ExecResult.ADVANCE
107
147
 
108
148
  def _cf_goto(self, stmt: str, sorted_lines: list[int],
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum, auto
6
+ from typing import Any
6
7
  import numpy as np
7
8
  from qubasic_core.patterns import (
8
9
  RE_LINE_NUM, RE_DEF_SINGLE, RE_DEF_BEGIN, RE_REG_INDEX,
@@ -79,7 +80,7 @@ try:
79
80
  from rich.panel import Panel as _RichPanel
80
81
  from rich.text import Text as _RichText
81
82
  _RICH = True
82
- _console = _RichConsole(highlight=False)
83
+ _console: Any = _RichConsole(highlight=False)
83
84
  except ImportError:
84
85
  _RICH = False
85
86
  _console = None
@@ -71,6 +71,7 @@ class Engine:
71
71
  self._circuit_cache_key: Any = None
72
72
  self._circuit_cache: Any = None
73
73
  self._pending_set_state: Any = None # immediate SET_STATE for next RUN
74
+ self._poke_state_prep: dict[Any, Any] = {} # qubit prep from POKE $0100
74
75
 
75
76
  # LOCC
76
77
  self.locc: Any = None
@@ -118,3 +119,17 @@ class Engine:
118
119
  self._circuit_cache = None
119
120
  self._pending_set_state = None
120
121
  self._poke_state_prep = {}
122
+
123
+ def _invalidate_run_state(self) -> None:
124
+ """Drop cached results that describe a now-stale configuration.
125
+
126
+ Called when the qubit count changes: last_sv, last_counts,
127
+ last_circuit, and the transpile cache all assume the previous
128
+ num_qubits, so reading them afterwards would mislabel (or, for
129
+ the reshape in _peek_qubit/_bloch_vector, crash) the state.
130
+ """
131
+ self.last_counts = None
132
+ self.last_sv = None
133
+ self.last_circuit = None
134
+ self._circuit_cache_key = None
135
+ self._circuit_cache = None
@@ -12,7 +12,7 @@ class ExecContext:
12
12
 
13
13
  sorted_lines: list[int]
14
14
  ip: int
15
- run_vars: dict[str, Any]
15
+ run_vars: Any # dict[str, Any] or a Scope wrapper
16
16
  loop_stack: list[dict[str, Any]] = field(default_factory=list)
17
17
  iteration_count: int = 0
18
18
  max_iterations: int = 100_000
@@ -328,20 +328,42 @@ class ExecutorMixin:
328
328
  set(self.subroutines.keys()) |
329
329
  set(self._custom_gates.keys())
330
330
  )
331
- # Tokenize: split on word boundaries, preserving delimiters
332
- tokens = re.split(r'(\b\w+\b)', stmt)
333
- for i, tok in enumerate(tokens):
334
- if not tok or not tok[0].isalpha():
335
- continue
336
- if tok in protected or tok.upper() in protected or tok.lower() in protected:
337
- continue
338
- if tok in merged:
339
- # Don't expand record bases (dicts) — leave p in p.x intact so
340
- # the expression evaluator can resolve the field.
341
- if isinstance(merged[tok], dict):
331
+
332
+ def _sub_segment(seg: str) -> str:
333
+ # Tokenize: split on word boundaries, preserving delimiters
334
+ tokens = re.split(r'(\b\w+\b)', seg)
335
+ for i, tok in enumerate(tokens):
336
+ if not tok or not tok[0].isalpha():
337
+ continue
338
+ if tok in protected or tok.upper() in protected or tok.lower() in protected:
342
339
  continue
343
- tokens[i] = str(merged[tok])
344
- return ''.join(tokens)
340
+ if tok in merged:
341
+ # Don't expand record bases (dicts) — leave p in p.x intact
342
+ # so the expression evaluator can resolve the field.
343
+ if isinstance(merged[tok], dict):
344
+ continue
345
+ tokens[i] = str(merged[tok])
346
+ return ''.join(tokens)
347
+
348
+ # Substitute only outside quoted string literals: a variable named in a
349
+ # PRINT label (PRINT "S ="; S) must survive verbatim inside the quotes.
350
+ out: list[str] = []
351
+ i, n = 0, len(stmt)
352
+ while i < n:
353
+ ch = stmt[i]
354
+ if ch in ('"', "'"):
355
+ j = i + 1
356
+ while j < n and stmt[j] != ch:
357
+ j += 1
358
+ out.append(stmt[i:j + 1]) # quoted span, verbatim
359
+ i = j + 1
360
+ else:
361
+ j = i
362
+ while j < n and stmt[j] not in ('"', "'"):
363
+ j += 1
364
+ out.append(_sub_segment(stmt[i:j])) # unquoted span
365
+ i = j
366
+ return ''.join(out)
345
367
 
346
368
  def _expand_statement(self, stmt, _call_stack: set[str] | None = None):
347
369
  """Expand subroutines. Returns list of gate strings.
@@ -636,18 +658,24 @@ class ExecutorMixin:
636
658
 
637
659
  Uses the same _exec_line pipeline as cmd_run for consistency.
638
660
  """
639
- # In LOCC mode, handle @register prefix via the numpy engine
640
- if self.locc_mode and self.locc:
641
- m = RE_AT_REG_LINE.match(line)
642
- if m:
661
+ # In LOCC mode, handle @register prefix via the numpy engine. Split
662
+ # colon-compound @REG lines so each gate is applied (a single @REG line
663
+ # is just the one-part case); a bare @REG line keeps prior behavior.
664
+ if self.locc_mode and self.locc and line.strip().startswith('@'):
665
+ parts = self._split_colon_stmts(line) if ':' in line else [line.strip()]
666
+ for part in parts:
667
+ m = RE_AT_REG_LINE.match(part)
668
+ if not m:
669
+ self.io.writeln(f"?BAD @register statement: {part}")
670
+ continue
643
671
  reg = m.group(1).upper()
644
672
  gate_stmt = m.group(2).strip()
645
673
  if reg not in self.locc.names:
646
674
  self.io.writeln(f"?UNKNOWN REGISTER: {reg} (have {', '.join(self.locc.names)})")
647
675
  return
648
676
  self._locc_apply_gate(reg, gate_stmt)
649
- self._locc_state()
650
- return
677
+ self._locc_state()
678
+ return
651
679
  if line.strip().startswith('@'):
652
680
  self.io.writeln("?@register syntax requires LOCC mode (try: LOCC <n1> <n2>)")
653
681
  return
@@ -116,7 +116,7 @@ class ExpressionMixin:
116
116
  'SQRT2': math.sqrt(2), 'sqrt2': math.sqrt(2),
117
117
  'True': True, 'False': False,
118
118
  }
119
- _AST_OPS = {
119
+ _AST_OPS: dict[type, Any] = {
120
120
  ast.Add: operator.add, ast.Sub: operator.sub,
121
121
  ast.Mult: operator.mul, ast.Div: operator.truediv,
122
122
  ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod,
@@ -6,6 +6,7 @@ import os
6
6
  import re
7
7
  import sys
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  import numpy as np
11
12
 
@@ -99,7 +100,7 @@ class FileIOMixin:
99
100
  else:
100
101
  f.write(f"LET {name} = {val}\n")
101
102
  # Save program (exclude lines injected by IMPORT)
102
- imported = getattr(self, '_imported_lines', set())
103
+ imported: set[int] = getattr(self, '_imported_lines', set())
103
104
  saved = [n for n in sorted(self.program.keys()) if n not in imported]
104
105
  for num in saved:
105
106
  f.write(f"{num} {self.program[num]}\n")
@@ -242,8 +242,8 @@ def _measure_np(
242
242
  """
243
243
  sv = np.ascontiguousarray(sv).reshape([2] * n_qubits)
244
244
  ax = n_qubits - 1 - qubit
245
- idx_0 = [slice(None)] * n_qubits
246
- idx_1 = [slice(None)] * n_qubits
245
+ idx_0: list = [slice(None)] * n_qubits
246
+ idx_1: list = [slice(None)] * n_qubits
247
247
  idx_0[ax] = 0
248
248
  idx_1[ax] = 1
249
249
  p0 = float(np.sum(np.abs(sv[tuple(idx_0)])**2))
@@ -42,7 +42,7 @@ class LOCCEngine:
42
42
  self.joint = joint
43
43
  self.noise_param = noise_param
44
44
  self.noise_type = noise_type
45
- self.classical = {}
45
+ self.classical: dict[str, int] = {}
46
46
  # Precompute offsets for JOINT mode
47
47
  self.offsets = []
48
48
  off = 0
@@ -237,8 +237,8 @@ class LOCCEngine:
237
237
  """Sample and return (per_reg_counts, joint_counts)."""
238
238
  if self.joint:
239
239
  raw = _sample_np(self.sv, self.n_total, shots)
240
- per_reg = {name: {} for name in self.names}
241
- joint = {}
240
+ per_reg: dict[str, dict[str, int]] = {name: {} for name in self.names}
241
+ joint: dict[str, int] = {}
242
242
  for state, count in raw.items():
243
243
  # Split the joint bitstring into per-register segments.
244
244
  # The joint statevector is ordered A[0..nA-1] B[0..nB-1] ...