qubasic 0.10.0__tar.gz → 0.10.1__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.10.0 → qubasic-0.10.1}/CHANGELOG.md +11 -0
- {qubasic-0.10.0/qubasic.egg-info → qubasic-0.10.1}/PKG-INFO +2 -2
- {qubasic-0.10.0 → qubasic-0.10.1}/README.md +1 -1
- {qubasic-0.10.0 → qubasic-0.10.1}/pyproject.toml +1 -1
- {qubasic-0.10.0 → qubasic-0.10.1/qubasic.egg-info}/PKG-INFO +2 -2
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/__init__.py +1 -1
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/control_flow.py +22 -8
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/expression.py +58 -7
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/strings.py +12 -14
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/terminal.py +15 -5
- {qubasic-0.10.0 → qubasic-0.10.1}/tests/test_qubasic.py +68 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/LICENSE +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/MANIFEST.in +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/examples/bell.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/examples/grover3.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/examples/locc_teleport.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/examples/sweep_rx.qb +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/__main__.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/algos2.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/analysis.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/backend.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/classic.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/cli.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/debug.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/demos.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/display.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/engine.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/engine_state.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/errors.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/executor.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/file_io.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/gates.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/help_text.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/memory.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/parser.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/patterns.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/profiler.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/protocol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qec.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qol.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qudits.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/resources.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/scope.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/screen.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/state_display.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/statements.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/subs.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/sweep.py +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/setup.cfg +0 -0
- {qubasic-0.10.0 → qubasic-0.10.1}/tests/test_features.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Expression, string, and PRINT fixes in the classic-BASIC layer. The quantum
|
|
6
|
+
engine is unchanged.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- Math functions resolve in any case: `SQRT`, `SIN`, `ABS`, `INT`, ... now work as well as their lowercase spellings, matching the rest of the language. Runtime and string functions (`RND`, `LEFT$`, ...) are likewise case-insensitive.
|
|
10
|
+
- `AND`, `OR`, `NOT`, `XOR`, and `<>` work in every expression context (`LET`, `PRINT`, gate parameters), not only in `IF` conditions. `AND`/`OR`/`XOR` are bitwise on integers (`6 AND 3` = 2) while preserving operator precedence below comparison, so `IF a > b AND c > d` still groups correctly; `NOT` is logical.
|
|
11
|
+
- String variables are fully usable: `LET s$ = "foo" + "bar"`, `LET t$ = LEFT$(s$, 3)`, and other computed strings store their real value in both immediate and program mode, instead of being mistaken for a quoted literal or forced through `float()`.
|
|
12
|
+
- `PRINT` surfaces evaluation errors instead of silently printing the unevaluated source text. Assigning a string to a numeric variable reports a `TYPE MISMATCH` rather than crashing on a float conversion.
|
|
13
|
+
|
|
3
14
|
## 0.10.0 (2026-06-19)
|
|
4
15
|
|
|
5
16
|
Completes the frontier set: fault-tolerant QEC extras and a compilation/resources
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -242,7 +242,7 @@ CLEAR x Remove a variable
|
|
|
242
242
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
243
243
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
244
244
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
245
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
245
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
246
246
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
247
247
|
|
|
248
248
|
## Arrays
|
|
@@ -209,7 +209,7 @@ CLEAR x Remove a variable
|
|
|
209
209
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
210
210
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
211
211
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
212
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
212
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
213
213
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
214
214
|
|
|
215
215
|
## Arrays
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -242,7 +242,7 @@ CLEAR x Remove a variable
|
|
|
242
242
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
243
243
|
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
244
244
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
245
|
-
Bitwise: `AND`, `OR`, `XOR
|
|
245
|
+
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
246
246
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
247
247
|
|
|
248
248
|
## Arrays
|
|
@@ -12,7 +12,7 @@ from qubasic_core.engine import ExecResult, ExecOutcome
|
|
|
12
12
|
from qubasic_core.parser import parse_stmt
|
|
13
13
|
from qubasic_core.statements import (
|
|
14
14
|
RawStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt, WendStmt,
|
|
15
|
-
LetArrayStmt, LetStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
15
|
+
LetArrayStmt, LetStmt, LetStrStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
16
16
|
ForStmt, NextStmt, WhileStmt, IfThenStmt,
|
|
17
17
|
DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
|
|
18
18
|
SelectCaseStmt, CaseStmt, EndSelectStmt, ElseStmt, EndIfStmt,
|
|
@@ -106,7 +106,20 @@ class ControlFlowMixin:
|
|
|
106
106
|
def _cf_let_var(self, stmt: str, run_vars: dict[str, Any],
|
|
107
107
|
parsed: LetStmt) -> tuple[bool, ExecOutcome]:
|
|
108
108
|
name, expr = parsed.name, parsed.expr
|
|
109
|
-
|
|
109
|
+
raw = self._safe_eval(expr, extra_ns=run_vars)
|
|
110
|
+
if isinstance(raw, str):
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
f"TYPE MISMATCH: '{name}' is numeric; use '{name}$' for strings")
|
|
113
|
+
val = float(raw)
|
|
114
|
+
run_vars[name] = val
|
|
115
|
+
self.variables[name] = val
|
|
116
|
+
return True, ExecResult.ADVANCE
|
|
117
|
+
|
|
118
|
+
def _cf_let_str(self, stmt: str, run_vars: dict[str, Any],
|
|
119
|
+
parsed: LetStrStmt) -> tuple[bool, ExecOutcome]:
|
|
120
|
+
"""LET v$ = <expr> — assign a string (or number) to a string variable."""
|
|
121
|
+
name, expr = parsed.name, parsed.expr
|
|
122
|
+
val = self._eval_string_expr(expr, run_vars)
|
|
110
123
|
run_vars[name] = val
|
|
111
124
|
self.variables[name] = val
|
|
112
125
|
return True, ExecResult.ADVANCE
|
|
@@ -167,12 +180,12 @@ class ControlFlowMixin:
|
|
|
167
180
|
text = re.sub(r'\bTAB\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
|
|
168
181
|
if not text.strip():
|
|
169
182
|
return text # standalone SPC/TAB -> whitespace
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
|
|
184
|
+
run_vars if isinstance(run_vars, dict) else dict(run_vars))
|
|
185
|
+
# Surface evaluation errors instead of silently printing the raw source
|
|
186
|
+
# text (which used to turn PRINT SQRT(9) into the literal "SQRT(9)" and
|
|
187
|
+
# an undefined variable into its own name).
|
|
188
|
+
return str(self._safe_eval(text, extra_ns=ns))
|
|
176
189
|
|
|
177
190
|
def _cf_print(self, stmt: str, run_vars: dict[str, Any],
|
|
178
191
|
parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
|
|
@@ -400,6 +413,7 @@ class ControlFlowMixin:
|
|
|
400
413
|
WendStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_wend(rv, ls, sl, ip),
|
|
401
414
|
LetArrayStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_array(st, rv, p),
|
|
402
415
|
LetStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_var(st, rv, p),
|
|
416
|
+
LetStrStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_str(st, rv, p),
|
|
403
417
|
PrintStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_print(st, rv, p),
|
|
404
418
|
GotoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_goto(st, sl, p),
|
|
405
419
|
GosubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_gosub(st, sl, ip, p),
|
|
@@ -94,6 +94,36 @@ def _rewrite_logical_outside_strings(cond: str) -> str:
|
|
|
94
94
|
return ''.join(parts)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _as_int(x: Any) -> int:
|
|
98
|
+
"""Coerce a BASIC value to int for bitwise AND/OR/XOR.
|
|
99
|
+
|
|
100
|
+
QBasic rounds non-integers; truth values and non-numerics fold to 1/0 so
|
|
101
|
+
``(a > 0) AND (b > 0)`` works the same as ``6 AND 3``.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(x, bool):
|
|
104
|
+
return int(x)
|
|
105
|
+
if isinstance(x, int):
|
|
106
|
+
return x
|
|
107
|
+
if isinstance(x, float):
|
|
108
|
+
return int(round(x))
|
|
109
|
+
return 1 if x else 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _basic_and(a: Any, b: Any) -> int:
|
|
113
|
+
"""BASIC AND — bitwise on integers, and correct for truth values
|
|
114
|
+
(1 & 1 == 1, 1 & 0 == 0). Parsed from ``and`` so precedence stays below
|
|
115
|
+
comparison: ``a > b AND c > d`` groups as ``(a>b) AND (c>d)``."""
|
|
116
|
+
return _as_int(a) & _as_int(b)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _basic_or(a: Any, b: Any) -> int:
|
|
120
|
+
return _as_int(a) | _as_int(b)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _basic_xor(a: Any, b: Any) -> int:
|
|
124
|
+
return _as_int(a) ^ _as_int(b)
|
|
125
|
+
|
|
126
|
+
|
|
97
127
|
class ExpressionMixin:
|
|
98
128
|
"""AST-based safe expression evaluation. No eval().
|
|
99
129
|
|
|
@@ -126,11 +156,14 @@ class ExpressionMixin:
|
|
|
126
156
|
ast.Eq: operator.eq, ast.NotEq: operator.ne,
|
|
127
157
|
ast.Lt: operator.lt, ast.LtE: operator.le,
|
|
128
158
|
ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ast.
|
|
133
|
-
ast.
|
|
159
|
+
# AND/OR/XOR are BASIC's bitwise-logical operators (6 AND 3 == 2),
|
|
160
|
+
# robust to float operands; NOT stays logical so IF NOT flag works
|
|
161
|
+
# with 0/1 truth values.
|
|
162
|
+
ast.And: _basic_and,
|
|
163
|
+
ast.Or: _basic_or,
|
|
164
|
+
ast.BitAnd: _basic_and,
|
|
165
|
+
ast.BitOr: _basic_or,
|
|
166
|
+
ast.BitXor: _basic_xor,
|
|
134
167
|
ast.Invert: operator.invert,
|
|
135
168
|
}
|
|
136
169
|
|
|
@@ -237,6 +270,13 @@ class ExpressionMixin:
|
|
|
237
270
|
ns['FRE'] = lambda x=0: psutil.virtual_memory().available
|
|
238
271
|
except ImportError:
|
|
239
272
|
ns['FRE'] = lambda x=0: 0
|
|
273
|
+
# BASIC is case-insensitive for built-in functions and constants, so
|
|
274
|
+
# register upper- and lower-case aliases for every builtin (SQRT and
|
|
275
|
+
# sqrt, RND and rnd, LEFT$ and left$). Variables are merged with their
|
|
276
|
+
# own case in _safe_eval and shadow these.
|
|
277
|
+
for _name in list(ns.keys()):
|
|
278
|
+
ns.setdefault(_name.upper(), ns[_name])
|
|
279
|
+
ns.setdefault(_name.lower(), ns[_name])
|
|
240
280
|
self._base_ns = ns
|
|
241
281
|
return ns
|
|
242
282
|
|
|
@@ -301,9 +341,17 @@ class ExpressionMixin:
|
|
|
301
341
|
expr_str = str(expr).strip()
|
|
302
342
|
# Normalize FN prefix: "FN square(x)" -> "square(x)"
|
|
303
343
|
expr_str = re.sub(r'\bFN\s+(\w+)\s*\(', r'\1(', expr_str, flags=re.IGNORECASE)
|
|
344
|
+
# Rewrite BASIC logical/relational operators (AND, OR, NOT, XOR, <>) to
|
|
345
|
+
# the Python forms the AST understands, only outside quoted strings.
|
|
346
|
+
# Applied to every expression (LET, PRINT, gate params), not just IF
|
|
347
|
+
# conditions, so the documented operators work everywhere.
|
|
348
|
+
expr_str = _rewrite_logical_outside_strings(expr_str)
|
|
304
349
|
# Rewrite numeric literals (&H, &B, $hex addresses) and the string
|
|
305
350
|
# sigil, each only outside quoted string literals.
|
|
306
351
|
expr_str = _replace_dollar_outside_strings(expr_str)
|
|
352
|
+
# The operator rewrite can pad a leading token (NOT x -> " not x"); strip
|
|
353
|
+
# so ast.parse(mode='eval') does not reject it as an unexpected indent.
|
|
354
|
+
expr_str = expr_str.strip()
|
|
307
355
|
if not expr_str:
|
|
308
356
|
raise ValueError("EMPTY EXPRESSION")
|
|
309
357
|
try:
|
|
@@ -351,8 +399,11 @@ class ExpressionMixin:
|
|
|
351
399
|
return float(self._safe_eval(expr, extra_ns=run_vars))
|
|
352
400
|
|
|
353
401
|
def _eval_condition(self, cond: str, run_vars: dict[str, Any]) -> bool:
|
|
354
|
-
"""Evaluate a boolean condition.
|
|
355
|
-
|
|
402
|
+
"""Evaluate a boolean condition.
|
|
403
|
+
|
|
404
|
+
The operator rewrite (AND/OR/NOT/XOR/<>) now happens inside _safe_eval,
|
|
405
|
+
so conditions and ordinary expressions share one consistent path.
|
|
406
|
+
"""
|
|
356
407
|
return bool(self._safe_eval(cond, extra_ns=run_vars))
|
|
357
408
|
|
|
358
409
|
def _run_timer(self) -> float:
|
|
@@ -84,21 +84,19 @@ class StringMixin:
|
|
|
84
84
|
ns[k] = v
|
|
85
85
|
return ns
|
|
86
86
|
|
|
87
|
-
def _eval_string_expr(self, expr: str) -> str | float:
|
|
88
|
-
"""Evaluate an expression that
|
|
87
|
+
def _eval_string_expr(self, expr: str, run_vars: dict[str, Any] | None = None) -> str | float:
|
|
88
|
+
"""Evaluate an expression that may return a string or a number.
|
|
89
|
+
|
|
90
|
+
Uses the shared AST evaluator so concatenation (``"a" + "b"``), string
|
|
91
|
+
functions (``LEFT$(s$, 3)``), and string variables resolve to their real
|
|
92
|
+
values. A multi-part expression like ``"a" + "b"`` is no longer mistaken
|
|
93
|
+
for a single quoted literal, and a string result is no longer forced
|
|
94
|
+
through float(). Errors surface instead of silently returning the source.
|
|
95
|
+
"""
|
|
89
96
|
expr = expr.strip()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return expr[1:-1]
|
|
94
|
-
# String variable
|
|
95
|
-
if expr in self.variables and isinstance(self.variables[expr], str):
|
|
96
|
-
return self.variables[expr]
|
|
97
|
-
# Try numeric
|
|
98
|
-
try:
|
|
99
|
-
return self.eval_expr(expr)
|
|
100
|
-
except Exception:
|
|
101
|
-
return expr
|
|
97
|
+
if run_vars is not None:
|
|
98
|
+
return self._safe_eval(expr, extra_ns=run_vars)
|
|
99
|
+
return self._safe_eval(expr)
|
|
102
100
|
|
|
103
101
|
def cmd_let_str(self, name: str, expr: str) -> None:
|
|
104
102
|
"""Assign a string value to a string variable."""
|
|
@@ -1044,9 +1044,10 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1044
1044
|
|
|
1045
1045
|
def cmd_let(self, rest: str) -> None:
|
|
1046
1046
|
"""LET <var> = <expr> — assign a computed value to a variable.
|
|
1047
|
-
Supports record fields (LET p.x = 3.14)
|
|
1048
|
-
(LET a(0) = PI, LET m(i, j) = x), matching the
|
|
1049
|
-
|
|
1047
|
+
Supports string variables (LET s$ = "hi"), record fields (LET p.x = 3.14)
|
|
1048
|
+
and array elements (LET a(0) = PI, LET m(i, j) = x), matching the
|
|
1049
|
+
in-program LET."""
|
|
1050
|
+
from qubasic_core.patterns import RE_LET_ARRAY, RE_LET_STR
|
|
1050
1051
|
am = RE_LET_ARRAY.match(f"LET {rest}")
|
|
1051
1052
|
if am:
|
|
1052
1053
|
from qubasic_core.statements import LetArrayStmt
|
|
@@ -1056,12 +1057,21 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1056
1057
|
self._cf_let_array(f"LET {rest}", self.variables, parsed)
|
|
1057
1058
|
self.io.writeln(f"{name}({idx_expr.strip()}) = {self.eval_expr(val_expr)}")
|
|
1058
1059
|
return
|
|
1060
|
+
sm = RE_LET_STR.match(f"LET {rest}")
|
|
1061
|
+
if sm:
|
|
1062
|
+
self.cmd_let_str(sm.group(1), sm.group(2))
|
|
1063
|
+
return
|
|
1059
1064
|
m = re.match(r'(\w+(?:\.\w+)?)\s*=\s*(.*)', rest)
|
|
1060
1065
|
if not m:
|
|
1061
1066
|
self.io.writeln("?USAGE: LET <var> = <expr>")
|
|
1062
1067
|
return
|
|
1063
1068
|
name = m.group(1)
|
|
1064
|
-
|
|
1069
|
+
raw = self._safe_eval(m.group(2))
|
|
1070
|
+
if isinstance(raw, str):
|
|
1071
|
+
self.io.writeln(
|
|
1072
|
+
f"?TYPE MISMATCH: '{name}' is numeric — use '{name}$' for strings")
|
|
1073
|
+
return
|
|
1074
|
+
val = float(raw)
|
|
1065
1075
|
self.variables[name] = val
|
|
1066
1076
|
if '.' in name: # mirror into the record dict for LIST VARS
|
|
1067
1077
|
base, field = name.split('.', 1)
|
|
@@ -2097,7 +2107,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
2097
2107
|
if not m:
|
|
2098
2108
|
return False
|
|
2099
2109
|
name = m.group(1)
|
|
2100
|
-
val = self._eval_string_expr(m.group(2))
|
|
2110
|
+
val = self._eval_string_expr(m.group(2), run_vars)
|
|
2101
2111
|
run_vars[name] = val
|
|
2102
2112
|
self.variables[name] = val
|
|
2103
2113
|
return True
|
|
@@ -1948,6 +1948,74 @@ class TestResources(unittest.TestCase):
|
|
|
1948
1948
|
self.assertIsNotNone(t.last_counts)
|
|
1949
1949
|
|
|
1950
1950
|
|
|
1951
|
+
class TestExpressionStringRegressions(unittest.TestCase):
|
|
1952
|
+
"""Regression coverage for the expression/string/PRINT fixes:
|
|
1953
|
+
case-insensitive functions, bitwise/logical operators in every expression
|
|
1954
|
+
context, string-variable assignment, and PRINT surfacing errors."""
|
|
1955
|
+
|
|
1956
|
+
def setUp(self):
|
|
1957
|
+
self.t = QBasicTerminal()
|
|
1958
|
+
self.t.num_qubits = 1
|
|
1959
|
+
|
|
1960
|
+
def test_math_functions_case_insensitive(self):
|
|
1961
|
+
# Uppercase now resolves as well as lowercase, and they agree.
|
|
1962
|
+
self.assertAlmostEqual(self.t.eval_expr('SQRT(2)'), math.sqrt(2))
|
|
1963
|
+
self.assertAlmostEqual(self.t.eval_expr('SQRT(2)'), self.t.eval_expr('sqrt(2)'))
|
|
1964
|
+
self.assertAlmostEqual(self.t.eval_expr('SIN(0)'), 0.0)
|
|
1965
|
+
self.assertEqual(self.t.eval_expr('ABS(-5)'), 5.0)
|
|
1966
|
+
self.assertEqual(self.t.eval_expr('INT(-3.2)'), self.t.eval_expr('int(-3.2)'))
|
|
1967
|
+
|
|
1968
|
+
def test_rnd_case_insensitive(self):
|
|
1969
|
+
for expr in ('RND(1)', 'rnd(1)'):
|
|
1970
|
+
v = self.t.eval_expr(expr)
|
|
1971
|
+
self.assertTrue(0.0 <= v < 1.0)
|
|
1972
|
+
|
|
1973
|
+
def test_bitwise_logical_operators_in_expressions(self):
|
|
1974
|
+
# AND/OR/XOR work in ordinary expressions (not just IF) and are bitwise.
|
|
1975
|
+
self.assertEqual(self.t.eval_expr('6 AND 3'), 2.0)
|
|
1976
|
+
self.assertEqual(self.t.eval_expr('5 OR 2'), 7.0)
|
|
1977
|
+
self.assertEqual(self.t.eval_expr('5 XOR 3'), 6.0)
|
|
1978
|
+
# <> works as an expression operator.
|
|
1979
|
+
self.assertTrue(self.t._safe_eval('3 <> 4'))
|
|
1980
|
+
self.assertFalse(self.t._safe_eval('3 <> 3'))
|
|
1981
|
+
|
|
1982
|
+
def test_logical_operator_precedence_preserved(self):
|
|
1983
|
+
# AND must bind below comparison: a > b AND c > d groups correctly.
|
|
1984
|
+
self.assertTrue(self.t._eval_condition('3 > 2 AND 5 > 1', {}))
|
|
1985
|
+
self.assertFalse(self.t._eval_condition('3 > 5 AND 5 > 1', {}))
|
|
1986
|
+
self.assertTrue(self.t._eval_condition('1 > 5 OR 5 > 1', {}))
|
|
1987
|
+
self.assertTrue(self.t._eval_condition('NOT 0', {}))
|
|
1988
|
+
self.assertFalse(self.t._eval_condition('NOT 1', {}))
|
|
1989
|
+
|
|
1990
|
+
def test_string_assignment_program_mode(self):
|
|
1991
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
1992
|
+
t.program = {10: 'LET s$ = "foo" + "bar"',
|
|
1993
|
+
20: 'LET t$ = LEFT$("hello", 3)',
|
|
1994
|
+
30: 'LET u$ = MID$("hello", 2)'}
|
|
1995
|
+
capture(t.cmd_run)
|
|
1996
|
+
self.assertEqual(t.variables['s$'], 'foobar')
|
|
1997
|
+
self.assertEqual(t.variables['t$'], 'hel')
|
|
1998
|
+
self.assertEqual(t.variables['u$'], 'ello')
|
|
1999
|
+
|
|
2000
|
+
def test_string_assignment_immediate_mode(self):
|
|
2001
|
+
capture(self.t.cmd_let, 's$ = "hi"')
|
|
2002
|
+
self.assertEqual(self.t.variables['s$'], 'hi')
|
|
2003
|
+
capture(self.t.cmd_let, 'g$ = "foo" + "bar"')
|
|
2004
|
+
self.assertEqual(self.t.variables['g$'], 'foobar')
|
|
2005
|
+
|
|
2006
|
+
def test_numeric_var_rejects_string(self):
|
|
2007
|
+
_, out = capture(self.t.cmd_let, 'n = "hi"')
|
|
2008
|
+
self.assertIn('TYPE MISMATCH', out)
|
|
2009
|
+
self.assertNotIn('n', self.t.variables)
|
|
2010
|
+
|
|
2011
|
+
def test_print_surfaces_errors_no_masking(self):
|
|
2012
|
+
# A valid (now case-insensitive) call evaluates instead of printing source.
|
|
2013
|
+
self.assertEqual(self.t._eval_print_item('SQRT(9)', {}), '3.0')
|
|
2014
|
+
# A genuine error is raised, not silently printed as raw text.
|
|
2015
|
+
with self.assertRaises(Exception):
|
|
2016
|
+
self.t._eval_print_item('GARBAGEFUNC(3)', {})
|
|
2017
|
+
|
|
2018
|
+
|
|
1951
2019
|
if __name__ == '__main__':
|
|
1952
2020
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
1953
2021
|
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
|
|
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
|