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.
Files changed (70) hide show
  1. {qubasic-0.10.0 → qubasic-0.10.1}/CHANGELOG.md +11 -0
  2. {qubasic-0.10.0/qubasic.egg-info → qubasic-0.10.1}/PKG-INFO +2 -2
  3. {qubasic-0.10.0 → qubasic-0.10.1}/README.md +1 -1
  4. {qubasic-0.10.0 → qubasic-0.10.1}/pyproject.toml +1 -1
  5. {qubasic-0.10.0 → qubasic-0.10.1/qubasic.egg-info}/PKG-INFO +2 -2
  6. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/control_flow.py +22 -8
  8. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/expression.py +58 -7
  9. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/strings.py +12 -14
  10. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/terminal.py +15 -5
  11. {qubasic-0.10.0 → qubasic-0.10.1}/tests/test_qubasic.py +68 -0
  12. {qubasic-0.10.0 → qubasic-0.10.1}/LICENSE +0 -0
  13. {qubasic-0.10.0 → qubasic-0.10.1}/MANIFEST.in +0 -0
  14. {qubasic-0.10.0 → qubasic-0.10.1}/examples/bell.qb +0 -0
  15. {qubasic-0.10.0 → qubasic-0.10.1}/examples/grover3.qb +0 -0
  16. {qubasic-0.10.0 → qubasic-0.10.1}/examples/locc_teleport.qb +0 -0
  17. {qubasic-0.10.0 → qubasic-0.10.1}/examples/sweep_rx.qb +0 -0
  18. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/SOURCES.txt +0 -0
  19. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/dependency_links.txt +0 -0
  20. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/entry_points.txt +0 -0
  21. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/requires.txt +0 -0
  22. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic.egg-info/top_level.txt +0 -0
  23. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/__main__.py +0 -0
  24. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/algorithms.py +0 -0
  25. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/algos2.py +0 -0
  26. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/analysis.py +0 -0
  27. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/backend.py +0 -0
  28. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/benchmarking.py +0 -0
  29. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/bosonic.py +0 -0
  30. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/classic.py +0 -0
  31. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/cli.py +0 -0
  32. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/debug.py +0 -0
  33. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/demos.py +0 -0
  34. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/display.py +0 -0
  35. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/dynamics.py +0 -0
  36. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/engine.py +0 -0
  37. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/engine_state.py +0 -0
  38. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/errors.py +0 -0
  39. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/exec_context.py +0 -0
  40. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/executor.py +0 -0
  41. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/file_io.py +0 -0
  42. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/gates.py +0 -0
  43. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/help_text.py +0 -0
  44. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/io_protocol.py +0 -0
  45. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc.py +0 -0
  46. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_commands.py +0 -0
  47. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_display.py +0 -0
  48. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_engine.py +0 -0
  49. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/locc_execution.py +0 -0
  50. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/memory.py +0 -0
  51. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/mock_backend.py +0 -0
  52. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/noise_mixin.py +0 -0
  53. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/parser.py +0 -0
  54. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/patterns.py +0 -0
  55. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/pauliprop.py +0 -0
  56. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/profiler.py +0 -0
  57. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/program_mgmt.py +0 -0
  58. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/protocol.py +0 -0
  59. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qec.py +0 -0
  60. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qol.py +0 -0
  61. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/qudits.py +0 -0
  62. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/resources.py +0 -0
  63. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/scope.py +0 -0
  64. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/screen.py +0 -0
  65. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/state_display.py +0 -0
  66. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/statements.py +0 -0
  67. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/subs.py +0 -0
  68. {qubasic-0.10.0 → qubasic-0.10.1}/qubasic_core/sweep.py +0 -0
  69. {qubasic-0.10.0 → qubasic-0.10.1}/setup.cfg +0 -0
  70. {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.0
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`, `NOT` (on integers)
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`, `NOT` (on integers)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.10.0"
7
+ version = "0.10.1"
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.10.0
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`, `NOT` (on integers)
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
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.10.0'
31
+ __version__ = '0.10.1'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -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
- val = self._eval_with_vars(expr, run_vars)
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
- try:
171
- ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
172
- run_vars if isinstance(run_vars, dict) else dict(run_vars))
173
- return str(self._safe_eval(text, extra_ns=ns))
174
- except Exception:
175
- return text
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
- ast.And: lambda a, b: a and b,
130
- ast.Or: lambda a, b: a or b,
131
- ast.BitAnd: operator.and_,
132
- ast.BitOr: operator.or_,
133
- ast.BitXor: operator.xor,
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
- cond = _rewrite_logical_outside_strings(cond)
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 might return a string."""
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
- # Quoted string literal
91
- if (expr.startswith('"') and expr.endswith('"')) or \
92
- (expr.startswith("'") and expr.endswith("'")):
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) and array elements
1048
- (LET a(0) = PI, LET m(i, j) = x), matching the in-program LET."""
1049
- from qubasic_core.patterns import RE_LET_ARRAY
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
- val = self.eval_expr(m.group(2))
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