qubasic 0.6.1__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.
- {qubasic-0.6.1 → qubasic-0.6.3}/CHANGELOG.md +13 -0
- {qubasic-0.6.1/qubasic.egg-info → qubasic-0.6.3}/PKG-INFO +1 -1
- {qubasic-0.6.1 → qubasic-0.6.3}/pyproject.toml +1 -1
- {qubasic-0.6.1 → qubasic-0.6.3/qubasic.egg-info}/PKG-INFO +1 -1
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/__init__.py +1 -1
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/control_flow.py +77 -37
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/engine_state.py +14 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/executor.py +47 -19
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/memory.py +8 -2
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/parser.py +8 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/terminal.py +15 -6
- {qubasic-0.6.1 → qubasic-0.6.3}/tests/test_features.py +36 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/tests/test_qubasic.py +86 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/LICENSE +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/MANIFEST.in +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/README.md +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/examples/bell.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/examples/grover3.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/examples/locc_teleport.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/examples/sweep_rx.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/__main__.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/analysis.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/backend.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/classic.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/cli.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/debug.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/demos.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/engine.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/errors.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/expression.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/file_io.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/gates.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/help_text.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/locc.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/patterns.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/profiler.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/protocol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/qol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/scope.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/screen.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/state_display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/statements.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/strings.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/subs.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/qubasic_core/sweep.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.3}/setup.cfg +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 0.6.1 (2026-06-18)
|
|
4
17
|
|
|
5
18
|
### Fixed
|
|
@@ -63,46 +63,86 @@ class ControlFlowMixin:
|
|
|
63
63
|
self.variables[name] = val
|
|
64
64
|
return True, ExecResult.ADVANCE
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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],
|
|
@@ -119,3 +119,17 @@ class Engine:
|
|
|
119
119
|
self._circuit_cache = None
|
|
120
120
|
self._pending_set_state = None
|
|
121
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
|
|
@@ -328,20 +328,42 @@ class ExecutorMixin:
|
|
|
328
328
|
set(self.subroutines.keys()) |
|
|
329
329
|
set(self._custom_gates.keys())
|
|
330
330
|
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
|
@@ -103,7 +103,10 @@ class MemoryMixin:
|
|
|
103
103
|
qubit, field = off // QUBIT_BLOCK, off % QUBIT_BLOCK
|
|
104
104
|
if self.last_sv is None or qubit >= self.num_qubits:
|
|
105
105
|
return 0.0
|
|
106
|
-
|
|
106
|
+
sv_flat = np.ascontiguousarray(self.last_sv).ravel()
|
|
107
|
+
if sv_flat.size != 2 ** self.num_qubits:
|
|
108
|
+
return 0.0 # stale statevector from a different qubit count
|
|
109
|
+
sv = sv_flat.reshape([2] * self.num_qubits)
|
|
107
110
|
ax = self.num_qubits - 1 - qubit
|
|
108
111
|
t = np.moveaxis(sv, ax, 0)
|
|
109
112
|
t0, t1 = t[0].flatten(), t[1].flatten()
|
|
@@ -155,7 +158,10 @@ class MemoryMixin:
|
|
|
155
158
|
self._zero_page[a] = v
|
|
156
159
|
return
|
|
157
160
|
if a == 0xD000:
|
|
158
|
-
|
|
161
|
+
new_n = max(1, min(32, int(v)))
|
|
162
|
+
if new_n != self.num_qubits:
|
|
163
|
+
self._invalidate_run_state()
|
|
164
|
+
self.num_qubits = new_n
|
|
159
165
|
elif a == 0xD001:
|
|
160
166
|
self.shots = max(1, int(v))
|
|
161
167
|
elif a == 0xD002:
|
|
@@ -554,6 +554,14 @@ def parse_stmt(raw: str) -> Stmt:
|
|
|
554
554
|
|
|
555
555
|
# ── @REG lines ────────────────────────────────────────────────
|
|
556
556
|
if text.startswith('@'):
|
|
557
|
+
# A colon-compound @REG line (e.g. "@A H 0 : @A CX 0,1" or
|
|
558
|
+
# "@A H 0 : CX 0,1") must split into per-statement parts with
|
|
559
|
+
# @register inheritance; otherwise the whole tail is captured as one
|
|
560
|
+
# AtRegStmt.inner and the LOCC executor mis-tokenizes it.
|
|
561
|
+
if ':' in text:
|
|
562
|
+
parts = _split_colon_stmts(text)
|
|
563
|
+
if len(parts) > 1:
|
|
564
|
+
return CompoundStmt(raw=raw, parts=tuple(parts))
|
|
557
565
|
m = RE_AT_REG_LINE.match(text)
|
|
558
566
|
if m:
|
|
559
567
|
return AtRegStmt(raw=raw, reg=m.group(1).upper(),
|
|
@@ -571,6 +571,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
571
571
|
from qubasic_core.errors import QBasicRangeError
|
|
572
572
|
raise QBasicRangeError(f"RANGE: 1-{MAX_QUBITS}")
|
|
573
573
|
|
|
574
|
+
if n != self.num_qubits:
|
|
575
|
+
self._invalidate_run_state()
|
|
574
576
|
self.num_qubits = n
|
|
575
577
|
self.registers.clear()
|
|
576
578
|
est = _estimate_gb(n)
|
|
@@ -1980,8 +1982,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1980
1982
|
full_pauli[self.num_qubits - 1 - qubits[i]] = p
|
|
1981
1983
|
op = SparsePauliOp(''.join(full_pauli))
|
|
1982
1984
|
qc.save_expectation_value(op, list(range(self.num_qubits)), label=f'exp_{var}')
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
+
# Placeholder until the run fills the real value via
|
|
1986
|
+
# _extract_save_results. Preserve any prior value instead of
|
|
1987
|
+
# zeroing it, so a re-run that re-includes this SAVE line leaves
|
|
1988
|
+
# earlier results readable to LET/PRINT during the build pass.
|
|
1989
|
+
_prev = self.variables.get(var, 0)
|
|
1990
|
+
run_vars[var] = _prev
|
|
1991
|
+
self.variables[var] = _prev
|
|
1985
1992
|
except Exception as e:
|
|
1986
1993
|
self.io.writeln(f"?SAVE_EXPECT ERROR: {e}")
|
|
1987
1994
|
return True
|
|
@@ -1996,8 +2003,9 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1996
2003
|
var = m.group(2)
|
|
1997
2004
|
try:
|
|
1998
2005
|
qc.save_probabilities(qubits, label=f'prob_{var}')
|
|
1999
|
-
|
|
2000
|
-
|
|
2006
|
+
_prev = self.variables.get(var, 0) # preserve prior value (see SAVE_EXPECT)
|
|
2007
|
+
run_vars[var] = _prev
|
|
2008
|
+
self.variables[var] = _prev
|
|
2001
2009
|
except Exception as e:
|
|
2002
2010
|
self.io.writeln(f"?SAVE_PROBS ERROR: {e}")
|
|
2003
2011
|
return True
|
|
@@ -2012,8 +2020,9 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
2012
2020
|
var = m.group(2)
|
|
2013
2021
|
try:
|
|
2014
2022
|
qc.save_amplitudes(indices, label=f'amp_{var}')
|
|
2015
|
-
|
|
2016
|
-
|
|
2023
|
+
_prev = self.variables.get(var, 0) # preserve prior value (see SAVE_EXPECT)
|
|
2024
|
+
run_vars[var] = _prev
|
|
2025
|
+
self.variables[var] = _prev
|
|
2017
2026
|
except Exception as e:
|
|
2018
2027
|
self.io.writeln(f"?SAVE_AMPS ERROR: {e}")
|
|
2019
2028
|
return True
|
|
@@ -359,6 +359,42 @@ class TestMemoryMap(unittest.TestCase):
|
|
|
359
359
|
self.assertEqual(ntype, 2)
|
|
360
360
|
self.assertAlmostEqual(nparam, 0.1)
|
|
361
361
|
|
|
362
|
+
def test_qubit_count_change_invalidates_stale_state(self):
|
|
363
|
+
"""Changing the qubit count after a run must not leave a stale last_sv
|
|
364
|
+
that MAP/PEEK/BLOCH then reshape to the wrong size and crash."""
|
|
365
|
+
import numpy as np
|
|
366
|
+
|
|
367
|
+
# Run a 1-qubit circuit so last_sv has size 2.
|
|
368
|
+
t = QBasicTerminal()
|
|
369
|
+
t.num_qubits = 1
|
|
370
|
+
t.process('10 H 0')
|
|
371
|
+
t.process('20 MEASURE')
|
|
372
|
+
capture(t.cmd_run)
|
|
373
|
+
|
|
374
|
+
# QUBITS grows the count: the size-2 statevector no longer applies.
|
|
375
|
+
capture(t.cmd_qubits, '2')
|
|
376
|
+
self.assertIsNone(t.last_sv)
|
|
377
|
+
self.assertIsNone(t._circuit_cache_key)
|
|
378
|
+
self.assertEqual(t._peek(0x0100), 0.0) # would have crashed on reshape
|
|
379
|
+
capture(t.cmd_map) # MAP walks the qubit block
|
|
380
|
+
capture(t.cmd_bloch, '0') # BLOCH reshapes too
|
|
381
|
+
|
|
382
|
+
# The $D000 POKE path invalidates the same way.
|
|
383
|
+
t2 = QBasicTerminal()
|
|
384
|
+
t2.num_qubits = 1
|
|
385
|
+
t2.process('10 H 0')
|
|
386
|
+
t2.process('20 MEASURE')
|
|
387
|
+
capture(t2.cmd_run)
|
|
388
|
+
t2._poke(0xD000, 3)
|
|
389
|
+
self.assertIsNone(t2.last_sv)
|
|
390
|
+
self.assertEqual(t2._peek(0x0100), 0.0)
|
|
391
|
+
|
|
392
|
+
# Defensive guard: even a hand-set mismatched last_sv reads as 0, not a crash.
|
|
393
|
+
t3 = QBasicTerminal()
|
|
394
|
+
t3.num_qubits = 2
|
|
395
|
+
t3.last_sv = np.array([1.0, 0.0], dtype=complex) # size 2, but 2 qubits
|
|
396
|
+
self.assertEqual(t3._peek(0x0100), 0.0)
|
|
397
|
+
|
|
362
398
|
def test_sys(self):
|
|
363
399
|
"""SYS builtin BELL, SYS unmapped, SYS in program, SYS INSTALL."""
|
|
364
400
|
# SYS 0xE000 BELL demo
|
|
@@ -1432,6 +1432,92 @@ class TestAdditionalCoverage(unittest.TestCase):
|
|
|
1432
1432
|
self.assertEqual(t4.last_counts.get('1', 0), 100)
|
|
1433
1433
|
|
|
1434
1434
|
|
|
1435
|
+
# ---------------------------------------------------------------------------
|
|
1436
|
+
# TestBugFixes063 — regressions fixed in 0.6.3
|
|
1437
|
+
# ---------------------------------------------------------------------------
|
|
1438
|
+
class TestBugFixes063(unittest.TestCase):
|
|
1439
|
+
"""Regression tests for the 0.6.3 fixes."""
|
|
1440
|
+
|
|
1441
|
+
def _ghz3(self):
|
|
1442
|
+
v = np.zeros(8, dtype=complex)
|
|
1443
|
+
v[0] = v[7] = 1 / np.sqrt(2)
|
|
1444
|
+
return v
|
|
1445
|
+
|
|
1446
|
+
def test_locc_colon_compound_at_register(self):
|
|
1447
|
+
"""LOCC: colon-compound @REG lines apply every gate (explicit and inherited)."""
|
|
1448
|
+
for line in ('@A H 0 : @A CX 0,1 : @A CX 0,2', # explicit @A per clause
|
|
1449
|
+
'@A H 0 : CX 0,1 : CX 0,2'): # @A inherited
|
|
1450
|
+
t = QBasicTerminal()
|
|
1451
|
+
t.locc = LOCCEngine([3], joint=True)
|
|
1452
|
+
t.locc_mode = True
|
|
1453
|
+
t.shots = 1
|
|
1454
|
+
t.process(f'10 {line}', track_undo=False)
|
|
1455
|
+
t.process('20 MEASURE', track_undo=False)
|
|
1456
|
+
capture(t.cmd_run)
|
|
1457
|
+
sv = np.ascontiguousarray(t.locc.sv).ravel()
|
|
1458
|
+
fid = abs(np.vdot(self._ghz3(), sv)) ** 2
|
|
1459
|
+
self.assertGreater(fid, 0.999, f"GHZ not built by: {line}")
|
|
1460
|
+
|
|
1461
|
+
def test_save_expect_preserved_across_rerun(self):
|
|
1462
|
+
"""SAVE_EXPECT keeps prior values on re-run instead of zeroing them."""
|
|
1463
|
+
t = QBasicTerminal()
|
|
1464
|
+
t.num_qubits = 2
|
|
1465
|
+
t.shots = 64
|
|
1466
|
+
t.process('10 H 0', track_undo=False)
|
|
1467
|
+
t.process('20 CX 0,1', track_undo=False)
|
|
1468
|
+
t.process('30 SAVE_EXPECT ZZ 0,1 -> zz', track_undo=False)
|
|
1469
|
+
t.process('40 SAVE_EXPECT XX 0,1 -> xx', track_undo=False)
|
|
1470
|
+
capture(t.cmd_run)
|
|
1471
|
+
self.assertAlmostEqual(t.variables['zz'], 1.0, places=6)
|
|
1472
|
+
self.assertAlmostEqual(t.variables['xx'], 1.0, places=6)
|
|
1473
|
+
# A second run that still contains the SAVE lines must not zero zz/xx
|
|
1474
|
+
# before the LET on line 50 reads them.
|
|
1475
|
+
t.process('50 LET chsh = SQRT2 * (zz + xx)', track_undo=False)
|
|
1476
|
+
capture(t.cmd_run)
|
|
1477
|
+
self.assertAlmostEqual(t.variables['chsh'], 2 * math.sqrt(2), places=6)
|
|
1478
|
+
|
|
1479
|
+
def test_print_multi_item_separators(self):
|
|
1480
|
+
"""PRINT concatenates ';' items and tab-aligns ',' items."""
|
|
1481
|
+
t = QBasicTerminal()
|
|
1482
|
+
t.num_qubits = 1
|
|
1483
|
+
t.variables['S'] = 2.8284271247
|
|
1484
|
+
t.process('10 PRINT "S ="; S', track_undo=False)
|
|
1485
|
+
_, out = capture(t.cmd_run)
|
|
1486
|
+
self.assertIn('S =2.8284', out)
|
|
1487
|
+
|
|
1488
|
+
t2 = QBasicTerminal()
|
|
1489
|
+
t2.num_qubits = 1
|
|
1490
|
+
t2.process('10 PRINT "a", "b"', track_undo=False)
|
|
1491
|
+
_, out2 = capture(t2.cmd_run)
|
|
1492
|
+
line = next(l for l in out2.splitlines() if 'a' in l and 'b' in l)
|
|
1493
|
+
self.assertGreaterEqual(line.index('b') - line.index('a'), 14)
|
|
1494
|
+
|
|
1495
|
+
def test_print_does_not_substitute_inside_quotes(self):
|
|
1496
|
+
"""A variable name inside a quoted PRINT literal is emitted verbatim."""
|
|
1497
|
+
t = QBasicTerminal()
|
|
1498
|
+
t.num_qubits = 1
|
|
1499
|
+
t.variables['S'] = 99.0
|
|
1500
|
+
t.process('10 PRINT "value of S here"', track_undo=False)
|
|
1501
|
+
_, out = capture(t.cmd_run)
|
|
1502
|
+
self.assertIn('value of S here', out)
|
|
1503
|
+
self.assertNotIn('99', out)
|
|
1504
|
+
|
|
1505
|
+
def test_print_preserves_commas_in_quotes_and_calls(self):
|
|
1506
|
+
"""Top-level split skips commas inside quotes and inside call parens."""
|
|
1507
|
+
t = QBasicTerminal()
|
|
1508
|
+
t.num_qubits = 1
|
|
1509
|
+
t.process('10 PRINT "x,y,z"', track_undo=False)
|
|
1510
|
+
_, out = capture(t.cmd_run)
|
|
1511
|
+
self.assertIn('x,y,z', out)
|
|
1512
|
+
|
|
1513
|
+
t2 = QBasicTerminal()
|
|
1514
|
+
t2.num_qubits = 1
|
|
1515
|
+
t2.process('10 LET name$ = "hello"', track_undo=False)
|
|
1516
|
+
t2.process('20 PRINT LEFT$(name$, 3)', track_undo=False)
|
|
1517
|
+
_, out2 = capture(t2.cmd_run)
|
|
1518
|
+
self.assertIn('hel', out2)
|
|
1519
|
+
|
|
1520
|
+
|
|
1435
1521
|
if __name__ == '__main__':
|
|
1436
1522
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
1437
1523
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|