qubasic 0.4.0__tar.gz → 0.5.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 (59) hide show
  1. {qubasic-0.4.0 → qubasic-0.5.0}/CHANGELOG.md +44 -0
  2. {qubasic-0.4.0 → qubasic-0.5.0}/LICENSE +1 -1
  3. {qubasic-0.4.0/qubasic.egg-info → qubasic-0.5.0}/PKG-INFO +1 -1
  4. {qubasic-0.4.0 → qubasic-0.5.0}/pyproject.toml +1 -1
  5. {qubasic-0.4.0 → qubasic-0.5.0/qubasic.egg-info}/PKG-INFO +1 -1
  6. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.egg-info/SOURCES.txt +1 -0
  7. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.py +17 -0
  8. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/__init__.py +1 -1
  9. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/classic.py +3 -0
  10. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/control_flow.py +5 -3
  11. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/demos.py +1 -1
  12. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/display.py +10 -0
  13. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/expression.py +9 -25
  14. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/locc_commands.py +9 -5
  15. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/locc_display.py +3 -3
  16. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/locc_execution.py +8 -6
  17. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/parser.py +2 -2
  18. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/profiler.py +9 -1
  19. qubasic-0.5.0/qubasic_core/qol.py +671 -0
  20. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/scope.py +0 -1
  21. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/subs.py +25 -1
  22. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/terminal.py +71 -24
  23. {qubasic-0.4.0 → qubasic-0.5.0}/MANIFEST.in +0 -0
  24. {qubasic-0.4.0 → qubasic-0.5.0}/README.md +0 -0
  25. {qubasic-0.4.0 → qubasic-0.5.0}/examples/bell.qb +0 -0
  26. {qubasic-0.4.0 → qubasic-0.5.0}/examples/grover3.qb +0 -0
  27. {qubasic-0.4.0 → qubasic-0.5.0}/examples/locc_teleport.qb +0 -0
  28. {qubasic-0.4.0 → qubasic-0.5.0}/examples/sweep_rx.qb +0 -0
  29. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.egg-info/dependency_links.txt +0 -0
  30. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.egg-info/entry_points.txt +0 -0
  31. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.egg-info/requires.txt +0 -0
  32. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic.egg-info/top_level.txt +0 -0
  33. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/__main__.py +0 -0
  34. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/analysis.py +0 -0
  35. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/backend.py +0 -0
  36. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/debug.py +0 -0
  37. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/engine.py +0 -0
  38. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/engine_state.py +0 -0
  39. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/errors.py +0 -0
  40. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/exec_context.py +0 -0
  41. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/executor.py +0 -0
  42. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/file_io.py +0 -0
  43. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/gates.py +0 -0
  44. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/help_text.py +0 -0
  45. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/io_protocol.py +0 -0
  46. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/locc.py +0 -0
  47. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/locc_engine.py +0 -0
  48. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/memory.py +0 -0
  49. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/mock_backend.py +0 -0
  50. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/noise_mixin.py +0 -0
  51. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/patterns.py +0 -0
  52. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/program_mgmt.py +0 -0
  53. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/protocol.py +0 -0
  54. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/screen.py +0 -0
  55. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/state_display.py +0 -0
  56. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/statements.py +0 -0
  57. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/strings.py +0 -0
  58. {qubasic-0.4.0 → qubasic-0.5.0}/qubasic_core/sweep.py +0 -0
  59. {qubasic-0.4.0 → qubasic-0.5.0}/setup.cfg +0 -0
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (2026-03-30)
4
+
5
+ - **Colorized histograms**: bars colored green/yellow/dim by probability in terminal
6
+ - **Animated STEP mode**: press A during STEP for auto-advance with configurable delay
7
+ - **"Did you mean?"**: typo suggestions for misspelled commands (BLASCH -> BLOCH)
8
+ - **Status bar prompt**: REPL prompt shows qubit count, LOCC, noise status
9
+ - **DRAW command**: braille-character Bloch sphere rendering
10
+ - **Color-coded LIST**: gates in cyan, flow in yellow, comments dim (THEME controls)
11
+ - **Gate throughput**: RUN summary shows gates/s and circuit complexity (T-count, CNOT count)
12
+ - **COMPARE command**: run circuit with two methods and diff output distributions
13
+ - **HEATMAP command**: qubit-qubit entanglement entropy grid
14
+ - **Startup tip of the day**: random tip shown at REPL launch
15
+ - **ANIMATE command**: animated parameter sweep with in-place terminal updates
16
+ - **QUIZ mode**: interactive quantum computing quiz with multiple choice
17
+ - **Tab completion for file paths**: SAVE/LOAD/INCLUDE complete .qb filenames
18
+ - **DIFF command**: diff current program against another BANK slot
19
+ - **PLOT command**: ASCII scatter plot of P(top state) vs swept variable
20
+ - **UNDO preview**: shows what lines will change before applying
21
+ - **THEME command**: switch color schemes (default, retro, none)
22
+ - **F1/F2/F3 demo shortcuts**: load Bell, GHZ, Grover demos via function keys
23
+ - **Quantum spinner**: |0>, |+>, |1>, |-> cycling during STATS and LOCC progress
24
+ - **EXPLAIN command**: describe each program line in plain English
25
+ - **LOCC progress spinner**: quantum-themed progress during SEND-based LOCC runs
26
+ - **CLIP command**: copy last results to system clipboard
27
+ - **Sound on completion**: terminal bell for runs exceeding 2 seconds
28
+ - New QoLMixin with 24 quality-of-life features
29
+
30
+ ## 0.4.1 (2026-03-30)
31
+
32
+ - **Fix SEED dispatch**: moved from no-arg to with-arg dispatch table so `SEED 42` works from the REPL
33
+ - **Fix GPU probe**: `cmd_method` GPU probe used undefined `_pqc` variable; now uses `_pqc_m`
34
+ - **Fix LOCC non-numeric args**: `LOCC 4 banana` no longer crashes with unguarded ValueError
35
+ - **Coverage threshold**: raised CI coverage floor from 60% to 75%
36
+ - **Property-based tests**: 4 new hypothesis tests (arithmetic identity, parser fuzzing, process fuzzing, FOR loop count)
37
+ - **CLI integration tests**: 8 new tests covering dispatch, SEED, LOCC error handling, METHOD probe, --seed flag, --help
38
+ - **Parser imports**: import regexes from patterns.py directly instead of double-indirection through engine.py
39
+ - **Expression simplification**: `_replace_dollar_outside_strings` reduced from two passes to one
40
+ - **Jump table**: pre-compute WHILE/WEND and DO/LOOP ip mappings in `_scan_subs`; `_find_matching_wend` and `_find_matching_loop` use O(1) lookup when available
41
+ - **Scope cleanup**: removed dual-write hack in `Scope.__setitem__`; writes go to `_runtime` only, `_persistent` is read-through fallback
42
+ - **STATS output**: redirect stdout during stats runs to suppress rich console output in non-TTY mode
43
+ - **Copyright year**: LICENSE updated from 2025 to 2026
44
+ - **CLI --seed flag**: `qubasic --seed N script.qb` sets deterministic seed before execution
45
+ - **Type annotations**: added return type annotations to 14 unannotated mixin methods across locc_commands, locc_display, locc_execution, demos
46
+
3
47
  ## 0.4.0 (2026-03-29)
4
48
 
5
49
  - **Noise correctness**: transpile with optimization_level=0 when noisy so gates survive for noise attachment
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Charles C. Norton
3
+ Copyright (c) 2026 Charles C. Norton
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.4.0"
7
+ version = "0.5.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.4.0
3
+ Version: 0.5.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -46,6 +46,7 @@ qubasic_core/patterns.py
46
46
  qubasic_core/profiler.py
47
47
  qubasic_core/program_mgmt.py
48
48
  qubasic_core/protocol.py
49
+ qubasic_core/qol.py
49
50
  qubasic_core/scope.py
50
51
  qubasic_core/screen.py
51
52
  qubasic_core/state_display.py
@@ -53,10 +53,22 @@ def main():
53
53
  args = sys.argv[1:]
54
54
  quiet = '--quiet' in args or '-q' in args
55
55
  json_mode = '--json' in args
56
+ seed_val = None
56
57
  if quiet:
57
58
  args = [a for a in args if a not in ('--quiet', '-q')]
58
59
  if json_mode:
59
60
  args = [a for a in args if a != '--json']
61
+ # Parse --seed N
62
+ filtered = []
63
+ i = 0
64
+ while i < len(args):
65
+ if args[i] == '--seed' and i + 1 < len(args):
66
+ seed_val = int(args[i + 1])
67
+ i += 2
68
+ else:
69
+ filtered.append(args[i])
70
+ i += 1
71
+ args = filtered
60
72
 
61
73
  if any(a in ('-h', '--help') for a in args):
62
74
  from qubasic_core import __version__
@@ -67,12 +79,17 @@ def main():
67
79
  print(" qubasic script.qb Run a script file")
68
80
  print(" qubasic --quiet script Suppress banner and progress")
69
81
  print(" qubasic --json script Output results as JSON")
82
+ print(" qubasic --seed N script Set random seed for reproducibility")
70
83
  print(" qubasic --help Show this help")
71
84
  print()
72
85
  print("Type HELP inside the REPL for full command reference.")
73
86
  sys.exit(0)
74
87
 
75
88
  term = QBasicTerminal()
89
+ if seed_val is not None:
90
+ import numpy as _np
91
+ term._seed = seed_val
92
+ _np.random.seed(seed_val)
76
93
 
77
94
  if args:
78
95
  path = args[0]
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.4.0'
31
+ __version__ = '0.5.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -217,6 +217,9 @@ class ClassicMixin:
217
217
  # ── DO / LOOP ─────────────────────────────────────────────────────
218
218
 
219
219
  def _find_matching_loop(self, sorted_lines: list[int], ip: int) -> int:
220
+ jt = getattr(self, '_jump_table', None)
221
+ if jt and ip in jt:
222
+ return jt[ip]
220
223
  depth = 1
221
224
  scan = ip + 1
222
225
  while scan < len(sorted_lines):
@@ -166,10 +166,12 @@ class ControlFlowMixin:
166
166
  def _find_matching_wend(self, sorted_lines: list[int], ip: int) -> int:
167
167
  """Find the ip after the WEND matching the WHILE at ip.
168
168
 
169
- Scans forward with proper nesting depth tracking. Returns the ip
170
- index past the matching WEND. Raises with the WHILE line number
171
- for clear diagnostics.
169
+ Uses pre-computed jump table when available (O(1)), falls back
170
+ to linear scan with nesting depth tracking.
172
171
  """
172
+ jt = getattr(self, '_jump_table', None)
173
+ if jt and ip in jt:
174
+ return jt[ip] + 1
173
175
  depth = 1
174
176
  scan = ip + 1
175
177
  while scan < len(sorted_lines):
@@ -8,7 +8,7 @@ class DemoMixin:
8
8
  self.shots, self.cmd_new(), self.cmd_run(), self.cmd_list(), self.cmd_locc().
9
9
  """
10
10
 
11
- def cmd_demo(self, name):
11
+ def cmd_demo(self, name: str) -> None:
12
12
  demos = {
13
13
  'BELL': self._demo_bell,
14
14
  'GHZ': self._demo_ghz,
@@ -88,11 +88,21 @@ class DisplayMixin:
88
88
  max_count = max(c for _, c in display) if display else 1
89
89
  max_label = max(len(k) for k, _ in display) if display else 1
90
90
 
91
+ _theme = getattr(self, '_theme', {})
91
92
  for state, count in display:
92
93
  pct = 100 * count / total
93
94
  bar_len = int(HISTOGRAM_BAR_WIDTH * count / max_count)
94
95
  bar = '\u2588' * bar_len
95
96
  ket = f"|{state}\u27E9"
97
+ # Colorize bar by probability
98
+ if _theme and sys.stdout.isatty():
99
+ rst = _theme.get('reset', '')
100
+ if pct > 40:
101
+ bar = f"{_theme.get('bar_hi', '')}{bar}{rst}"
102
+ elif pct > 10:
103
+ bar = f"{_theme.get('bar_mid', '')}{bar}{rst}"
104
+ else:
105
+ bar = f"{_theme.get('bar_lo', '')}{bar}{rst}"
96
106
  self.io.writeln(f" {ket:>{max_label+3}} {count:>6} ({pct:5.1f}%) {bar}")
97
107
 
98
108
  if len(sorted_counts) > MAX_HISTOGRAM_STATES:
@@ -12,44 +12,28 @@ from typing import Any
12
12
 
13
13
 
14
14
  def _replace_dollar_outside_strings(expr_str: str) -> str:
15
- """Apply $->_S_ replacement only outside quoted string literals."""
16
- result: list[str] = []
15
+ """Apply $->_S_ replacement only outside quoted string literals.
16
+
17
+ Single pass: splits on quote boundaries, substitutes only in
18
+ unquoted segments.
19
+ """
20
+ parts: list[str] = []
17
21
  i = 0
18
22
  n = len(expr_str)
19
23
  while i < n:
20
24
  ch = expr_str[i]
21
25
  if ch in ('"', "'"):
22
- # Copy the entire quoted region verbatim
23
26
  quote = ch
24
27
  j = i + 1
25
28
  while j < n and expr_str[j] != quote:
26
29
  j += 1
27
- # Include closing quote (or end of string if missing)
28
- result.append(expr_str[i:j + 1])
29
- i = j + 1
30
- else:
31
- result.append(ch)
32
- i += 1
33
- text = ''.join(result)
34
- # Now apply the substitution only to non-quoted segments
35
- parts: list[str] = []
36
- i = 0
37
- n = len(text)
38
- while i < n:
39
- ch = text[i]
40
- if ch in ('"', "'"):
41
- quote = ch
42
- j = i + 1
43
- while j < n and text[j] != quote:
44
- j += 1
45
- parts.append(text[i:j + 1])
30
+ parts.append(expr_str[i:j + 1])
46
31
  i = j + 1
47
32
  else:
48
- # Accumulate non-quoted text
49
33
  j = i
50
- while j < n and text[j] not in ('"', "'"):
34
+ while j < n and expr_str[j] not in ('"', "'"):
51
35
  j += 1
52
- segment = text[i:j]
36
+ segment = expr_str[i:j]
53
37
  parts.append(re.sub(r'(\w+)\$', r'\1_S_', segment))
54
38
  i = j
55
39
  return ''.join(parts)
@@ -24,7 +24,7 @@ class LOCCCommandsMixin:
24
24
  """
25
25
  return getattr(self, '_noise_depol_p', 0.0)
26
26
 
27
- def cmd_locc(self, rest):
27
+ def cmd_locc(self, rest: str) -> None:
28
28
  args = rest.upper().split()
29
29
  if not args:
30
30
  if self.locc_mode:
@@ -86,7 +86,11 @@ class LOCCCommandsMixin:
86
86
  self.io.writeln(f"?MAX {LOCC_MAX_REGISTERS} registers (A-Z)")
87
87
  return
88
88
 
89
- sizes = [int(n) for n in nums]
89
+ try:
90
+ sizes = [int(n) for n in nums]
91
+ except ValueError:
92
+ self.io.writeln("?LOCC register sizes must be integers")
93
+ return
90
94
  total = sum(sizes)
91
95
  if joint and total > LOCC_MAX_JOINT_QUBITS:
92
96
  self.io.writeln(f"?JOINT mode limited to {LOCC_MAX_JOINT_QUBITS} total qubits (requested {total})")
@@ -130,7 +134,7 @@ class LOCCCommandsMixin:
130
134
  self.io.writeln(f" WARNING: non-depolarizing noise model active but not supported "
131
135
  f"in LOCC numpy path. Only NOISE depolarizing propagates to LOCC.")
132
136
 
133
- def cmd_send(self, rest):
137
+ def cmd_send(self, rest: str) -> None:
134
138
  if not self.locc_mode:
135
139
  self.io.writeln("?SEND requires LOCC mode")
136
140
  return
@@ -153,7 +157,7 @@ class LOCCCommandsMixin:
153
157
  self.locc.classical[var] = outcome
154
158
  self.io.writeln(f" {reg}[{qubit}] -> {var} = {outcome}")
155
159
 
156
- def cmd_share(self, rest):
160
+ def cmd_share(self, rest: str) -> None:
157
161
  if not self.locc_mode:
158
162
  self.io.writeln("?SHARE requires LOCC mode")
159
163
  return
@@ -173,7 +177,7 @@ class LOCCCommandsMixin:
173
177
  except RuntimeError as e:
174
178
  self.io.writeln(f"?{e}")
175
179
 
176
- def cmd_loccinfo(self):
180
+ def cmd_loccinfo(self) -> None:
177
181
  """Show LOCC protocol metrics after a run."""
178
182
  if not self.locc_mode:
179
183
  self.io.writeln("?NOT IN LOCC MODE")
@@ -10,7 +10,7 @@ class LOCCDisplayMixin:
10
10
  self._print_bloch_single(), self.print_histogram(), self.io.
11
11
  """
12
12
 
13
- def _locc_state(self, rest=''):
13
+ def _locc_state(self, rest: str = '') -> None:
14
14
  reg = rest.strip().upper() if rest else ''
15
15
  if self.locc.joint:
16
16
  if not reg or reg in self.locc.names:
@@ -24,7 +24,7 @@ class LOCCDisplayMixin:
24
24
  self.io.writeln(f"\n Register {name} ({size} qubits):")
25
25
  self._print_statevector(self.locc.svs[name], size)
26
26
 
27
- def _locc_bloch(self, rest):
27
+ def _locc_bloch(self, rest: str) -> None:
28
28
  m = re.match(r'([A-Z])\s*(\d*)', rest.strip(), re.IGNORECASE) if rest.strip() else None
29
29
  if m and m.group(1):
30
30
  reg = m.group(1).upper()
@@ -49,7 +49,7 @@ class LOCCDisplayMixin:
49
49
  else:
50
50
  self.io.writeln(f"?USAGE: BLOCH <reg> [qubit] (registers: {', '.join(self.locc.names)})")
51
51
 
52
- def _locc_display_results(self, per_reg, counts_joint):
52
+ def _locc_display_results(self, per_reg: dict, counts_joint: dict) -> None:
53
53
  """Display per-register and joint histograms."""
54
54
  for name in self.locc.names:
55
55
  size = self.locc.get_size(name)
@@ -27,7 +27,7 @@ class LOCCExecutionMixin:
27
27
  self.last_counts, self.io.
28
28
  """
29
29
 
30
- def _locc_run(self):
30
+ def _locc_run(self) -> None:
31
31
  """Execute program in LOCC mode (N registers)."""
32
32
  sorted_lines = sorted(self.program.keys())
33
33
  if not sorted_lines:
@@ -44,7 +44,7 @@ class LOCCExecutionMixin:
44
44
  # Sync last_sv from LOCC engine so EXPECT/DENSITY/BLOCH work
45
45
  self.last_sv = self._active_sv
46
46
 
47
- def _locc_run_with_send(self, sorted_lines, has_measure):
47
+ def _locc_run_with_send(self, sorted_lines: list[int], has_measure: bool) -> None:
48
48
  """LOCC execution with SEND — prefix/suffix split optimization.
49
49
 
50
50
  Executes the deterministic prefix (before first SEND) once,
@@ -134,7 +134,9 @@ class LOCCExecutionMixin:
134
134
  counts_joint[jkey] = counts_joint.get(jkey, 0) + 1
135
135
  if shots > 50 and (shot + 1) % progress_interval == 0:
136
136
  pct = 100 * (shot + 1) // shots
137
- _prog = f" {pct}% ({shot+1}/{shots} shots)..."
137
+ from qubasic_core.qol import quantum_spin
138
+ spin = quantum_spin(shot)
139
+ _prog = f" {spin} {pct}% ({shot+1}/{shots} shots)..."
138
140
  if hasattr(self.io, 'write') and sys.stdout.isatty():
139
141
  self.io.write(_prog + '\r')
140
142
  # Non-terminal: skip progress to avoid \r noise
@@ -148,7 +150,7 @@ class LOCCExecutionMixin:
148
150
  self._locc_display_results(per_reg, counts_joint)
149
151
  self.last_counts = counts_joint
150
152
 
151
- def _locc_run_vectorized(self, sorted_lines, has_measure):
153
+ def _locc_run_vectorized(self, sorted_lines: list[int], has_measure: bool) -> None:
152
154
  """LOCC execution without SEND — single execution, vectorized sampling.
153
155
 
154
156
  When noise is active, re-executes per shot so that Monte Carlo noise
@@ -314,7 +316,7 @@ class LOCCExecutionMixin:
314
316
 
315
317
  raise ValueError(f"LOCC mode requires @A/@B prefix, SEND, SHARE, or IF: {stmt}")
316
318
 
317
- def _locc_try_special(self, reg, stmt, run_vars):
319
+ def _locc_try_special(self, reg: str, stmt: str, run_vars: dict) -> bool:
318
320
  """Handle MEAS/RESET/MEASURE_X/Y/Z/SYNDROME in LOCC mode."""
319
321
  from qubasic_core.parser import parse_stmt
320
322
  from qubasic_core.statements import MeasStmt, ResetStmt, MeasureBasisStmt, SyndromeStmt
@@ -373,7 +375,7 @@ class LOCCExecutionMixin:
373
375
 
374
376
  return False
375
377
 
376
- def _locc_apply_gate(self, reg, gate_stmt):
378
+ def _locc_apply_gate(self, reg: str, gate_stmt: str) -> None:
377
379
  """Parse and apply a gate to a LOCC register via numpy.
378
380
 
379
381
  Uses LOCCRegBackend for standard gates. CTRL and INV modifiers
@@ -4,8 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
 
7
- from qubasic_core.engine import (
8
- GATE_TABLE, GATE_ALIASES,
7
+ from qubasic_core.gates import GATE_TABLE, GATE_ALIASES
8
+ from qubasic_core.patterns import (
9
9
  RE_LET_ARRAY, RE_LET_VAR, RE_PRINT,
10
10
  RE_GOTO, RE_GOSUB, RE_FOR, RE_NEXT, RE_WHILE, RE_IF_THEN,
11
11
  RE_MEAS, RE_RESET, RE_SEND, RE_SHARE, RE_AT_REG_LINE, RE_AT_REG,
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import math
6
+ import os
7
+ import sys
6
8
  import time
7
9
  from typing import Any
8
10
 
@@ -111,10 +113,14 @@ class ProfilerMixin:
111
113
  self.io.writeln(f"\nRunning {n} trials...")
112
114
  for trial in range(n):
113
115
  old_io = self.io
116
+ old_stdout = sys.stdout
114
117
  self.io = _NullIOPort()
118
+ sys.stdout = open(os.devnull, 'w')
115
119
  try:
116
120
  self.cmd_run()
117
121
  finally:
122
+ sys.stdout.close()
123
+ sys.stdout = old_stdout
118
124
  self.io = old_io
119
125
  if self.last_counts:
120
126
  if len(self._stats_runs) >= MAX_STATS_RUNS:
@@ -122,7 +128,9 @@ class ProfilerMixin:
122
128
  break
123
129
  self._stats_runs.append(dict(self.last_counts))
124
130
  if n > 10 and (trial + 1) % (n // 10) == 0:
125
- self.io.write(f" {100 * (trial + 1) // n}%..." + '\r')
131
+ from qubasic_core.qol import quantum_spin
132
+ spin = quantum_spin(trial)
133
+ self.io.write(f" {spin} {100 * (trial + 1) // n}%..." + '\r')
126
134
  if n > 10:
127
135
  self.io.write(" " * 30 + '\r')
128
136
  self.io.writeln(f"Collected {len(self._stats_runs)} runs ({n} trials)")