qubasic 0.1.0__py3-none-any.whl

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 (48) hide show
  1. qbasic.py +113 -0
  2. qbasic_core/__init__.py +80 -0
  3. qbasic_core/__main__.py +6 -0
  4. qbasic_core/analysis.py +179 -0
  5. qbasic_core/backend.py +76 -0
  6. qbasic_core/classic.py +419 -0
  7. qbasic_core/control_flow.py +576 -0
  8. qbasic_core/debug.py +327 -0
  9. qbasic_core/demos.py +356 -0
  10. qbasic_core/display.py +274 -0
  11. qbasic_core/engine.py +126 -0
  12. qbasic_core/engine_state.py +109 -0
  13. qbasic_core/errors.py +37 -0
  14. qbasic_core/exec_context.py +24 -0
  15. qbasic_core/executor.py +861 -0
  16. qbasic_core/expression.py +228 -0
  17. qbasic_core/file_io.py +457 -0
  18. qbasic_core/gates.py +284 -0
  19. qbasic_core/help_text.py +167 -0
  20. qbasic_core/io_protocol.py +33 -0
  21. qbasic_core/locc.py +10 -0
  22. qbasic_core/locc_commands.py +221 -0
  23. qbasic_core/locc_display.py +61 -0
  24. qbasic_core/locc_engine.py +195 -0
  25. qbasic_core/locc_execution.py +389 -0
  26. qbasic_core/memory.py +369 -0
  27. qbasic_core/mock_backend.py +66 -0
  28. qbasic_core/noise_mixin.py +96 -0
  29. qbasic_core/parser.py +564 -0
  30. qbasic_core/patterns.py +186 -0
  31. qbasic_core/profiler.py +156 -0
  32. qbasic_core/program_mgmt.py +369 -0
  33. qbasic_core/protocol.py +77 -0
  34. qbasic_core/py.typed +0 -0
  35. qbasic_core/scope.py +74 -0
  36. qbasic_core/screen.py +115 -0
  37. qbasic_core/state_display.py +60 -0
  38. qbasic_core/statements.py +387 -0
  39. qbasic_core/strings.py +107 -0
  40. qbasic_core/subs.py +261 -0
  41. qbasic_core/sweep.py +82 -0
  42. qbasic_core/terminal.py +1697 -0
  43. qubasic-0.1.0.dist-info/METADATA +736 -0
  44. qubasic-0.1.0.dist-info/RECORD +48 -0
  45. qubasic-0.1.0.dist-info/WHEEL +5 -0
  46. qubasic-0.1.0.dist-info/entry_points.txt +2 -0
  47. qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. qubasic-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,576 @@
1
+ """Control flow helpers extracted from QBasicTerminal.
2
+
3
+ Requires: TerminalProtocol (see qbasic_core.protocol).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Any, Callable
10
+
11
+ from qbasic_core.engine import (
12
+ ExecResult, ExecOutcome,
13
+ RE_LET_ARRAY, RE_LET_VAR, RE_PRINT,
14
+ RE_GOTO, RE_GOSUB, RE_FOR, RE_NEXT, RE_WHILE, RE_IF_THEN,
15
+ )
16
+ from qbasic_core.parser import parse_stmt
17
+ from qbasic_core.statements import (
18
+ RawStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt, WendStmt,
19
+ LetArrayStmt, LetStmt, PrintStmt, GotoStmt, GosubStmt,
20
+ ForStmt, NextStmt, WhileStmt, IfThenStmt,
21
+ DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
22
+ SelectCaseStmt, CaseStmt, EndSelectStmt,
23
+ DoStmt, LoopStmt, ExitStmt,
24
+ SwapStmt, DefFnStmt, OptionBaseStmt,
25
+ SubStmt, EndSubStmt, FunctionStmt, EndFunctionStmt, CallStmt,
26
+ LocalStmt, StaticStmt, SharedStmt,
27
+ OnErrorStmt, ResumeStmt, ErrorStmt, AssertStmt, StopStmt,
28
+ OnMeasureStmt, OnTimerStmt,
29
+ )
30
+
31
+
32
+ class ControlFlowMixin:
33
+ """Mixin providing control flow helpers for QBasicTerminal.
34
+
35
+ Requires: TerminalProtocol — uses self.program, self.variables,
36
+ self.arrays, self.locc_mode, self.locc, self._gosub_stack,
37
+ self._eval_with_vars(), self._eval_condition(), self._substitute_vars().
38
+ """
39
+
40
+ # ── Control flow helpers (decomposed from _exec_control_flow) ────
41
+ #
42
+ # Each _cf_* method accepts (self, stmt: str, ..., *, parsed=None).
43
+ # When parsed is provided the method uses the typed fields directly;
44
+ # when it is None the method falls back to regex matching on the raw
45
+ # string. The raw-string path is retained for backward compatibility
46
+ # but is no longer exercised by the main execution pipeline (callers
47
+ # now always supply a parsed Stmt via _exec_control_flow).
48
+ # Deferred cleanup: the raw-string fallback can be removed once all
49
+ # external call sites are confirmed to pass parsed objects.
50
+
51
+ def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
52
+ *, parsed: LetArrayStmt | None = None) -> tuple[bool, ExecOutcome] | None:
53
+ if parsed is None:
54
+ m = RE_LET_ARRAY.match(stmt)
55
+ if not m:
56
+ return None
57
+ name, idx_expr, val_expr = m.group(1), m.group(2), m.group(3)
58
+ else:
59
+ name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
60
+ idx = int(self._eval_with_vars(idx_expr, run_vars))
61
+ val = self._eval_with_vars(val_expr, run_vars)
62
+ if name not in self.arrays:
63
+ self.arrays[name] = [0.0] * (idx + 1)
64
+ while idx >= len(self.arrays[name]):
65
+ self.arrays[name].append(0.0)
66
+ self.arrays[name][idx] = val
67
+ return True, ExecResult.ADVANCE
68
+
69
+ def _cf_let_var(self, stmt: str, run_vars: dict[str, Any],
70
+ *, parsed: LetStmt | None = None) -> tuple[bool, ExecOutcome] | None:
71
+ if parsed is None:
72
+ m = RE_LET_VAR.match(stmt)
73
+ if not m:
74
+ return None
75
+ name, expr = m.group(1), m.group(2)
76
+ else:
77
+ name, expr = parsed.name, parsed.expr
78
+ val = self._eval_with_vars(expr, run_vars)
79
+ run_vars[name] = val
80
+ self.variables[name] = val
81
+ return True, ExecResult.ADVANCE
82
+
83
+ def _cf_print(self, stmt: str, run_vars: dict[str, Any],
84
+ *, parsed: PrintStmt | None = None) -> tuple[bool, ExecOutcome] | None:
85
+ if parsed is None:
86
+ m = RE_PRINT.match(stmt)
87
+ if not m:
88
+ return None
89
+ raw_expr = m.group(1)
90
+ else:
91
+ raw_expr = parsed.expr
92
+ text = self._substitute_vars(raw_expr.strip(), run_vars)
93
+ # Determine trailing separator: ; suppresses newline, , advances to tab
94
+ suppress_newline = raw_expr.rstrip().endswith(';')
95
+ tab_advance = raw_expr.rstrip().endswith(',')
96
+ if suppress_newline:
97
+ text = text.rstrip().removesuffix(';').rstrip()
98
+ elif tab_advance:
99
+ text = text.rstrip().removesuffix(',').rstrip()
100
+ # Evaluate SPC(n) and TAB(n) inline
101
+ def _replace_spc(m_spc):
102
+ n = int(self._eval_with_vars(m_spc.group(1), run_vars))
103
+ return ' ' * max(0, n)
104
+ def _replace_tab(m_tab):
105
+ n = int(self._eval_with_vars(m_tab.group(1), run_vars))
106
+ return ' ' * max(0, n)
107
+ text = re.sub(r'\bSPC\s*\(([^)]+)\)', _replace_spc, text, flags=re.IGNORECASE)
108
+ text = re.sub(r'\bTAB\s*\(([^)]+)\)', _replace_tab, text, flags=re.IGNORECASE)
109
+ # Evaluate the expression
110
+ if (text.startswith('"') and text.endswith('"')) or \
111
+ (text.startswith("'") and text.endswith("'")):
112
+ output = text[1:-1]
113
+ else:
114
+ try:
115
+ ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else dict(run_vars) if not isinstance(run_vars, dict) else run_vars
116
+ result = self._safe_eval(text, extra_ns=ns)
117
+ output = str(result)
118
+ except Exception:
119
+ output = text
120
+ # Output with separator behavior
121
+ if suppress_newline:
122
+ self.io.write(output)
123
+ elif tab_advance:
124
+ col = len(output) % 14
125
+ padding = 14 - col if col > 0 else 14
126
+ self.io.write(output + ' ' * padding)
127
+ else:
128
+ self.io.writeln(output)
129
+ return True, ExecResult.ADVANCE
130
+
131
+ def _cf_goto(self, stmt: str, sorted_lines: list[int],
132
+ *, parsed: GotoStmt | None = None) -> tuple[bool, int] | None:
133
+ if parsed is None:
134
+ m = RE_GOTO.match(stmt)
135
+ if not m:
136
+ return None
137
+ target = int(m.group(1))
138
+ else:
139
+ target = parsed.target
140
+ for idx, ln in enumerate(sorted_lines):
141
+ if ln == target:
142
+ return True, idx
143
+ raise RuntimeError(f"GOTO {target}: LINE NOT FOUND")
144
+
145
+ def _cf_gosub(self, stmt: str, sorted_lines: list[int], ip: int,
146
+ *, parsed: GosubStmt | None = None) -> tuple[bool, int] | None:
147
+ if parsed is None:
148
+ m = RE_GOSUB.match(stmt)
149
+ if not m:
150
+ return None
151
+ target = int(m.group(1))
152
+ else:
153
+ target = parsed.target
154
+ self._gosub_stack.append(ip + 1)
155
+ for idx, ln in enumerate(sorted_lines):
156
+ if ln == target:
157
+ return True, idx
158
+ raise RuntimeError(f"GOSUB {target}: LINE NOT FOUND")
159
+
160
+ def _cf_for(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]], ip: int,
161
+ *, parsed: ForStmt | None = None) -> tuple[bool, ExecOutcome] | None:
162
+ if parsed is None:
163
+ m = RE_FOR.match(stmt)
164
+ if not m:
165
+ return None
166
+ var = m.group(1)
167
+ start_expr, end_expr, step_expr = m.group(2), m.group(3), m.group(4)
168
+ else:
169
+ var = parsed.var
170
+ start_expr, end_expr, step_expr = parsed.start_expr, parsed.end_expr, parsed.step_expr
171
+ start = self._eval_with_vars(start_expr, run_vars)
172
+ end = self._eval_with_vars(end_expr, run_vars)
173
+ step = self._eval_with_vars(step_expr, run_vars) if step_expr else 1
174
+ try:
175
+ if start == int(start): start = int(start)
176
+ except (OverflowError, ValueError):
177
+ pass
178
+ try:
179
+ if end == int(end): end = int(end)
180
+ except (OverflowError, ValueError):
181
+ pass
182
+ try:
183
+ if isinstance(step, float) and step == int(step): step = int(step)
184
+ except (OverflowError, ValueError):
185
+ pass
186
+ run_vars[var] = start
187
+ self.variables[var] = start
188
+ loop_stack.append({'var': var, 'current': start, 'end': end,
189
+ 'step': step, 'return_ip': ip})
190
+ return True, ExecResult.ADVANCE
191
+
192
+ def _cf_next(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
193
+ *, parsed: NextStmt | None = None) -> tuple[bool, ExecOutcome] | None:
194
+ if parsed is None:
195
+ m = RE_NEXT.match(stmt)
196
+ if not m:
197
+ return None
198
+ var = m.group(1)
199
+ else:
200
+ var = parsed.var
201
+ if not loop_stack or loop_stack[-1].get('var') != var:
202
+ if loop_stack:
203
+ expected = loop_stack[-1].get('var', '?')
204
+ raise RuntimeError(f"NEXT {var} does not match current FOR {expected}")
205
+ raise RuntimeError(f"NEXT {var} without matching FOR")
206
+ loop = loop_stack[-1]
207
+ loop['current'] += loop['step']
208
+ if (loop['step'] > 0 and loop['current'] <= loop['end']) or \
209
+ (loop['step'] < 0 and loop['current'] >= loop['end']):
210
+ run_vars[var] = loop['current']
211
+ self.variables[var] = loop['current']
212
+ return True, loop['return_ip'] + 1
213
+ else:
214
+ loop_stack.pop()
215
+ return True, ExecResult.ADVANCE
216
+
217
+ def _find_matching_wend(self, sorted_lines: list[int], ip: int) -> int:
218
+ """Find the ip after the WEND matching the WHILE at ip.
219
+
220
+ Scans forward with proper nesting depth tracking. Returns the ip
221
+ index past the matching WEND. Raises with the WHILE line number
222
+ for clear diagnostics.
223
+ """
224
+ depth = 1
225
+ scan = ip + 1
226
+ while scan < len(sorted_lines):
227
+ s = self.program[sorted_lines[scan]].strip().upper()
228
+ if s.startswith('WHILE '):
229
+ depth += 1
230
+ elif s == 'WEND':
231
+ depth -= 1
232
+ if depth == 0:
233
+ return scan + 1
234
+ scan += 1
235
+ line_num = sorted_lines[ip]
236
+ raise RuntimeError(f"WHILE at line {line_num} has no matching WEND")
237
+
238
+ def _cf_while(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
239
+ sorted_lines: list[int], ip: int,
240
+ *, parsed: WhileStmt | None = None) -> tuple[bool, ExecOutcome] | None:
241
+ if parsed is None:
242
+ m = RE_WHILE.match(stmt)
243
+ if not m:
244
+ return None
245
+ cond = m.group(1).strip()
246
+ else:
247
+ cond = parsed.condition
248
+ if self._eval_condition(cond, run_vars):
249
+ loop_stack.append({'type': 'while', 'cond': cond, 'return_ip': ip})
250
+ return True, ExecResult.ADVANCE
251
+ else:
252
+ return True, self._find_matching_wend(sorted_lines, ip)
253
+
254
+ def _cf_wend(self, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
255
+ sorted_lines: list[int] | None = None, ip: int | None = None) -> tuple[bool, ExecOutcome] | None:
256
+ if not loop_stack or loop_stack[-1].get('type') != 'while':
257
+ ctx = f" at line {sorted_lines[ip]}" if sorted_lines and ip is not None else ""
258
+ raise RuntimeError(f"WEND{ctx} without matching WHILE")
259
+ loop = loop_stack[-1]
260
+ if self._eval_condition(loop['cond'], run_vars):
261
+ return True, loop['return_ip']
262
+ else:
263
+ loop_stack.pop()
264
+ return True, ExecResult.ADVANCE
265
+
266
+ def _cf_if_then(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
267
+ sorted_lines: list[int], ip: int,
268
+ exec_fn: Callable[..., Any],
269
+ *, parsed: IfThenStmt | None = None) -> tuple[bool, ExecOutcome] | None:
270
+ if parsed is None:
271
+ m = RE_IF_THEN.match(stmt)
272
+ if not m:
273
+ return None
274
+ cond_str = m.group(1).strip()
275
+ then_clause = m.group(2).strip()
276
+ else_clause = m.group(3).strip() if m.group(3) else None
277
+ else:
278
+ cond_str = parsed.condition
279
+ then_clause = parsed.then_clause
280
+ else_clause = parsed.else_clause
281
+ cond_vars = run_vars
282
+ if self.locc_mode and self.locc:
283
+ cond_vars = {**run_vars, **self.locc.classical}
284
+ result = ExecResult.ADVANCE
285
+ if self._eval_condition(cond_str, cond_vars):
286
+ if then_clause:
287
+ r = exec_fn(then_clause, loop_stack, sorted_lines, ip, run_vars)
288
+ if r is not None and r is not ExecResult.ADVANCE:
289
+ result = r
290
+ elif else_clause:
291
+ r = exec_fn(else_clause, loop_stack, sorted_lines, ip, run_vars)
292
+ if r is not None and r is not ExecResult.ADVANCE:
293
+ result = r
294
+ return True, result
295
+
296
+ # ── Type-based dispatch table ────────────────────────────────────
297
+ # Maps parsed Stmt types to handler functions. Each handler
298
+ # receives (self, stmt, parsed, loop_stack, sorted_lines, ip,
299
+ # run_vars, exec_fn) and returns (handled: bool, result). Built
300
+ # once as a class variable so the dict lookup cost is paid
301
+ # per-call, not per-class.
302
+
303
+ @staticmethod
304
+ def _d_rem(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
305
+ return True, ExecResult.ADVANCE
306
+
307
+ _d_measure = _d_rem # same behavior
308
+
309
+ @staticmethod
310
+ def _d_end(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
311
+ return True, ExecResult.END
312
+
313
+ @staticmethod
314
+ def _d_return(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
315
+ if not self._gosub_stack:
316
+ raise RuntimeError("RETURN WITHOUT GOSUB")
317
+ return True, self._gosub_stack.pop()
318
+
319
+ @staticmethod
320
+ def _d_wend(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
321
+ return self._cf_wend(run_vars, loop_stack, sorted_lines, ip)
322
+
323
+ @staticmethod
324
+ def _d_let_array(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
325
+ r = self._cf_let_array(stmt, run_vars, parsed=parsed)
326
+ return r if r is not None else (False, None)
327
+
328
+ @staticmethod
329
+ def _d_let_var(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
330
+ r = self._cf_let_var(stmt, run_vars, parsed=parsed)
331
+ return r if r is not None else (False, None)
332
+
333
+ @staticmethod
334
+ def _d_print(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
335
+ r = self._cf_print(stmt, run_vars, parsed=parsed)
336
+ return r if r is not None else (False, None)
337
+
338
+ @staticmethod
339
+ def _d_goto(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
340
+ r = self._cf_goto(stmt, sorted_lines, parsed=parsed)
341
+ return r if r is not None else (False, None)
342
+
343
+ @staticmethod
344
+ def _d_gosub(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
345
+ r = self._cf_gosub(stmt, sorted_lines, ip, parsed=parsed)
346
+ return r if r is not None else (False, None)
347
+
348
+ @staticmethod
349
+ def _d_for(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
350
+ r = self._cf_for(stmt, run_vars, loop_stack, ip, parsed=parsed)
351
+ return r if r is not None else (False, None)
352
+
353
+ @staticmethod
354
+ def _d_next(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
355
+ r = self._cf_next(stmt, run_vars, loop_stack, parsed=parsed)
356
+ return r if r is not None else (False, None)
357
+
358
+ @staticmethod
359
+ def _d_while(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
360
+ r = self._cf_while(stmt, run_vars, loop_stack, sorted_lines, ip, parsed=parsed)
361
+ return r if r is not None else (False, None)
362
+
363
+ @staticmethod
364
+ def _d_if_then(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
365
+ r = self._cf_if_then(stmt, run_vars, loop_stack, sorted_lines, ip, exec_fn, parsed=parsed)
366
+ return r if r is not None else (False, None)
367
+
368
+ @staticmethod
369
+ def _d_data(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
370
+ r = self._cf_data(stmt, parsed=parsed)
371
+ return r if r is not None else (False, None)
372
+
373
+ @staticmethod
374
+ def _d_read(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
375
+ r = self._cf_read(stmt, run_vars, parsed=parsed)
376
+ return r if r is not None else (False, None)
377
+
378
+ @staticmethod
379
+ def _d_on_goto(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
380
+ r = self._cf_on_goto(stmt, run_vars, sorted_lines, parsed=parsed)
381
+ return r if r is not None else (False, None)
382
+
383
+ @staticmethod
384
+ def _d_on_gosub(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
385
+ r = self._cf_on_gosub(stmt, run_vars, sorted_lines, ip, parsed=parsed)
386
+ return r if r is not None else (False, None)
387
+
388
+ @staticmethod
389
+ def _d_select_case(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
390
+ r = self._cf_select_case(stmt, run_vars, sorted_lines, ip, parsed=parsed)
391
+ return r if r is not None else (False, None)
392
+
393
+ @staticmethod
394
+ def _d_case(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
395
+ r = self._cf_case(stmt, sorted_lines, ip, parsed=parsed)
396
+ return r if r is not None else (False, None)
397
+
398
+ @staticmethod
399
+ def _d_end_select(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
400
+ r = self._cf_end_select(stmt, parsed=parsed)
401
+ return r if r is not None else (False, None)
402
+
403
+ @staticmethod
404
+ def _d_do(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
405
+ r = self._cf_do(stmt, run_vars, loop_stack, sorted_lines, ip, parsed=parsed)
406
+ return r if r is not None else (False, None)
407
+
408
+ @staticmethod
409
+ def _d_loop(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
410
+ r = self._cf_loop(stmt, run_vars, loop_stack, sorted_lines, ip, parsed=parsed)
411
+ return r if r is not None else (False, None)
412
+
413
+ @staticmethod
414
+ def _d_exit(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
415
+ r = self._cf_exit(stmt, loop_stack, sorted_lines, ip, parsed=parsed)
416
+ return r if r is not None else (False, None)
417
+
418
+ @staticmethod
419
+ def _d_swap(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
420
+ r = self._cf_swap(stmt, run_vars, parsed=parsed)
421
+ return r if r is not None else (False, None)
422
+
423
+ @staticmethod
424
+ def _d_def_fn(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
425
+ r = self._cf_def_fn(stmt, run_vars, parsed=parsed)
426
+ return r if r is not None else (False, None)
427
+
428
+ @staticmethod
429
+ def _d_option_base(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
430
+ r = self._cf_option_base(stmt, parsed=parsed)
431
+ return r if r is not None else (False, None)
432
+
433
+ @staticmethod
434
+ def _d_sub(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
435
+ r = self._cf_sub(stmt, sorted_lines, ip, parsed=parsed)
436
+ return r if r is not None else (False, None)
437
+
438
+ @staticmethod
439
+ def _d_end_sub(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
440
+ r = self._cf_end_sub(stmt, parsed=parsed)
441
+ return r if r is not None else (False, None)
442
+
443
+ @staticmethod
444
+ def _d_function(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
445
+ r = self._cf_function(stmt, sorted_lines, ip, parsed=parsed)
446
+ return r if r is not None else (False, None)
447
+
448
+ @staticmethod
449
+ def _d_end_function(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
450
+ r = self._cf_end_function(stmt, parsed=parsed)
451
+ return r if r is not None else (False, None)
452
+
453
+ @staticmethod
454
+ def _d_call(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
455
+ r = self._cf_call(stmt, run_vars, sorted_lines, ip, parsed=parsed)
456
+ return r if r is not None else (False, None)
457
+
458
+ @staticmethod
459
+ def _d_local(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
460
+ r = self._cf_local(stmt, run_vars, parsed=parsed)
461
+ return r if r is not None else (False, None)
462
+
463
+ @staticmethod
464
+ def _d_static(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
465
+ r = self._cf_static(stmt, run_vars, parsed=parsed)
466
+ return r if r is not None else (False, None)
467
+
468
+ @staticmethod
469
+ def _d_shared(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
470
+ r = self._cf_shared(stmt, run_vars, parsed=parsed)
471
+ return r if r is not None else (False, None)
472
+
473
+ @staticmethod
474
+ def _d_on_error(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
475
+ r = self._cf_on_error(stmt, parsed=parsed)
476
+ return r if r is not None else (False, None)
477
+
478
+ @staticmethod
479
+ def _d_resume(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
480
+ r = self._cf_resume(stmt, sorted_lines, parsed=parsed)
481
+ return r if r is not None else (False, None)
482
+
483
+ @staticmethod
484
+ def _d_error(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
485
+ r = self._cf_error(stmt, parsed=parsed)
486
+ return r if r is not None else (False, None)
487
+
488
+ @staticmethod
489
+ def _d_assert(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
490
+ r = self._cf_assert(stmt, run_vars, parsed=parsed)
491
+ return r if r is not None else (False, None)
492
+
493
+ @staticmethod
494
+ def _d_stop(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
495
+ r = self._cf_stop(stmt, sorted_lines, ip, parsed=parsed)
496
+ return r if r is not None else (False, None)
497
+
498
+ @staticmethod
499
+ def _d_on_measure(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
500
+ r = self._cf_on_measure(stmt, parsed=parsed)
501
+ return r if r is not None else (False, None)
502
+
503
+ @staticmethod
504
+ def _d_on_timer(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn):
505
+ r = self._cf_on_timer(stmt, parsed=parsed)
506
+ return r if r is not None else (False, None)
507
+
508
+ _CF_DISPATCH: dict[type, Callable] = {
509
+ RemStmt: _d_rem,
510
+ MeasureStmt: _d_measure,
511
+ EndStmt: _d_end,
512
+ ReturnStmt: _d_return,
513
+ WendStmt: _d_wend,
514
+ LetArrayStmt: _d_let_array,
515
+ LetStmt: _d_let_var,
516
+ PrintStmt: _d_print,
517
+ GotoStmt: _d_goto,
518
+ GosubStmt: _d_gosub,
519
+ ForStmt: _d_for,
520
+ NextStmt: _d_next,
521
+ WhileStmt: _d_while,
522
+ IfThenStmt: _d_if_then,
523
+ DataStmt: _d_data,
524
+ ReadStmt: _d_read,
525
+ OnGotoStmt: _d_on_goto,
526
+ OnGosubStmt: _d_on_gosub,
527
+ SelectCaseStmt: _d_select_case,
528
+ CaseStmt: _d_case,
529
+ EndSelectStmt: _d_end_select,
530
+ DoStmt: _d_do,
531
+ LoopStmt: _d_loop,
532
+ ExitStmt: _d_exit,
533
+ SwapStmt: _d_swap,
534
+ DefFnStmt: _d_def_fn,
535
+ OptionBaseStmt: _d_option_base,
536
+ SubStmt: _d_sub,
537
+ EndSubStmt: _d_end_sub,
538
+ FunctionStmt: _d_function,
539
+ EndFunctionStmt: _d_end_function,
540
+ CallStmt: _d_call,
541
+ LocalStmt: _d_local,
542
+ StaticStmt: _d_static,
543
+ SharedStmt: _d_shared,
544
+ OnErrorStmt: _d_on_error,
545
+ ResumeStmt: _d_resume,
546
+ ErrorStmt: _d_error,
547
+ AssertStmt: _d_assert,
548
+ StopStmt: _d_stop,
549
+ OnMeasureStmt: _d_on_measure,
550
+ OnTimerStmt: _d_on_timer,
551
+ }
552
+
553
+ def _exec_control_flow(
554
+ self, stmt: str, loop_stack: list[dict[str, Any]],
555
+ sorted_lines: list[int], ip: int, run_vars: dict[str, Any],
556
+ exec_fn: Callable[..., Any],
557
+ *, parsed=None,
558
+ ) -> tuple[bool, ExecOutcome | None]:
559
+ """Shared control flow for both Qiskit and LOCC execution paths.
560
+ Returns (handled, result) — if handled is True, result is the return value.
561
+ exec_fn is the recursive line executor for IF/multi-statement dispatch.
562
+
563
+ Dispatches via dict lookup on the parsed Stmt type (O(1)) instead
564
+ of a linear chain of regex-matching _cf_* calls.
565
+
566
+ If *parsed* is provided, the parse_stmt call is skipped (avoids
567
+ redundant parsing when the caller has already parsed the statement).
568
+ """
569
+ if parsed is None:
570
+ parsed = parse_stmt(stmt)
571
+ handler = self._CF_DISPATCH.get(type(parsed))
572
+ if handler is not None:
573
+ return handler(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn)
574
+
575
+ # RawStmt or unmapped type — not handled by control flow
576
+ return False, None