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
qbasic_core/parser.py ADDED
@@ -0,0 +1,564 @@
1
+ """QBASIC parser — converts raw statement strings into typed Stmt objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from qbasic_core.engine import (
8
+ GATE_TABLE, GATE_ALIASES,
9
+ RE_LET_ARRAY, RE_LET_VAR, RE_PRINT,
10
+ RE_GOTO, RE_GOSUB, RE_FOR, RE_NEXT, RE_WHILE, RE_IF_THEN,
11
+ RE_MEAS, RE_RESET, RE_SEND, RE_SHARE, RE_AT_REG_LINE, RE_AT_REG,
12
+ RE_DATA, RE_READ, RE_ON_GOTO, RE_ON_GOSUB,
13
+ RE_SELECT_CASE, RE_CASE, RE_DO, RE_LOOP_STMT, RE_EXIT,
14
+ RE_SUB, RE_END_SUB, RE_FUNCTION, RE_END_FUNCTION, RE_CALL,
15
+ RE_LOCAL, RE_STATIC_DECL, RE_SHARED,
16
+ RE_ON_ERROR, RE_RESUME, RE_ERROR_STMT, RE_ASSERT,
17
+ RE_SWAP, RE_DEF_FN, RE_OPTION_BASE,
18
+ RE_POKE, RE_SYS, RE_UNITARY, RE_DIM, RE_REDIM, RE_ERASE,
19
+ RE_GET, RE_INPUT, RE_LINE_INPUT,
20
+ RE_PRINT_USING, RE_OPEN, RE_CLOSE, RE_PRINT_FILE, RE_INPUT_FILE,
21
+ RE_LPRINT, RE_SCREEN, RE_COLOR, RE_LOCATE,
22
+ RE_ON_MEASURE, RE_ON_TIMER, RE_IMPORT, RE_CHAIN, RE_MERGE,
23
+ RE_LET_STR, RE_DIM_MULTI, RE_MEASURE_BASIS, RE_SYNDROME,
24
+ )
25
+ from qbasic_core.statements import (
26
+ Stmt, RawStmt, GateStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt,
27
+ BarrierStmt, WendStmt, RestoreStmt, EndSelectStmt, EndSubStmt,
28
+ EndFunctionStmt, StopStmt,
29
+ GotoStmt, GosubStmt, ForStmt, NextStmt, WhileStmt, IfThenStmt,
30
+ DoStmt, LoopStmt, ExitStmt,
31
+ OnGotoStmt, OnGosubStmt, SelectCaseStmt, CaseStmt,
32
+ CallStmt, SubStmt, FunctionStmt,
33
+ OnErrorStmt, ResumeStmt, ErrorStmt, AssertStmt,
34
+ SwapStmt, DefFnStmt, OptionBaseStmt,
35
+ OnMeasureStmt, OnTimerStmt, DataStmt, ReadStmt,
36
+ LocalStmt, StaticStmt, SharedStmt,
37
+ LetStmt, LetArrayStmt, LetStrStmt, PrintStmt, PrintUsingStmt,
38
+ InputStmt, LineInputStmt, GetStmt,
39
+ DimStmt, RedimStmt, EraseStmt,
40
+ PokeStmt, SysStmt, UnitaryStmt,
41
+ OpenStmt, CloseStmt, PrintFileStmt, InputFileStmt, LprintStmt,
42
+ ScreenStmt, ColorStmt, LocateStmt, ImportStmt, ChainStmt, MergeStmt,
43
+ MeasStmt, ResetStmt, MeasureBasisStmt, SyndromeStmt,
44
+ SendStmt, ShareStmt, AtRegStmt,
45
+ CompoundStmt,
46
+ )
47
+
48
+
49
+ # ═══════════════════════════════════════════════════════════════════════
50
+ # Keyword dispatch handlers
51
+ # ═══════════════════════════════════════════════════════════════════════
52
+
53
+ def _handle_goto(text, raw):
54
+ m = RE_GOTO.match(text)
55
+ if m:
56
+ return GotoStmt(raw=raw, target=int(m.group(1)))
57
+ return None
58
+
59
+ def _handle_gosub(text, raw):
60
+ m = RE_GOSUB.match(text)
61
+ if m:
62
+ return GosubStmt(raw=raw, target=int(m.group(1)))
63
+ return None
64
+
65
+ def _handle_for(text, raw):
66
+ m = RE_FOR.match(text)
67
+ if m:
68
+ return ForStmt(raw=raw, var=m.group(1),
69
+ start_expr=m.group(2), end_expr=m.group(3),
70
+ step_expr=m.group(4))
71
+ return None
72
+
73
+ def _handle_next(text, raw):
74
+ m = RE_NEXT.match(text)
75
+ if m:
76
+ return NextStmt(raw=raw, var=m.group(1))
77
+ return None
78
+
79
+ def _handle_while(text, raw):
80
+ m = RE_WHILE.match(text)
81
+ if m:
82
+ return WhileStmt(raw=raw, condition=m.group(1).strip())
83
+ return None
84
+
85
+ def _handle_if(text, raw):
86
+ m = RE_IF_THEN.match(text)
87
+ if m:
88
+ return IfThenStmt(raw=raw, condition=m.group(1).strip(),
89
+ then_clause=m.group(2).strip(),
90
+ else_clause=m.group(3).strip() if m.group(3) else None)
91
+ return None
92
+
93
+ def _handle_do(text, raw):
94
+ m = RE_DO.match(text)
95
+ if m:
96
+ return DoStmt(raw=raw, kind=m.group(1), condition=m.group(2))
97
+ return None
98
+
99
+ def _handle_loop(text, raw):
100
+ m = RE_LOOP_STMT.match(text)
101
+ if m:
102
+ return LoopStmt(raw=raw, kind=m.group(1), condition=m.group(2))
103
+ return None
104
+
105
+ def _handle_exit(text, raw):
106
+ m = RE_EXIT.match(text)
107
+ if m:
108
+ return ExitStmt(raw=raw, target=m.group(1).upper())
109
+ return None
110
+
111
+ def _handle_on(text, raw):
112
+ m = RE_ON_MEASURE.match(text)
113
+ if m:
114
+ return OnMeasureStmt(raw=raw, target=int(m.group(1)))
115
+ m = RE_ON_TIMER.match(text)
116
+ if m:
117
+ return OnTimerStmt(raw=raw, interval=m.group(1), target=int(m.group(2)))
118
+ m = RE_ON_ERROR.match(text)
119
+ if m:
120
+ return OnErrorStmt(raw=raw, target=int(m.group(1)))
121
+ m = RE_ON_GOTO.match(text)
122
+ if m:
123
+ targets = tuple(int(t.strip()) for t in m.group(2).split(',') if t.strip())
124
+ return OnGotoStmt(raw=raw, expr=m.group(1).strip(), targets=targets)
125
+ m = RE_ON_GOSUB.match(text)
126
+ if m:
127
+ targets = tuple(int(t.strip()) for t in m.group(2).split(',') if t.strip())
128
+ return OnGosubStmt(raw=raw, expr=m.group(1).strip(), targets=targets)
129
+ return None
130
+
131
+ def _handle_select(text, raw):
132
+ m = RE_SELECT_CASE.match(text)
133
+ if m:
134
+ return SelectCaseStmt(raw=raw, expr=m.group(1).strip())
135
+ return None
136
+
137
+ def _handle_case(text, raw):
138
+ m = RE_CASE.match(text)
139
+ if m:
140
+ return CaseStmt(raw=raw, value=m.group(1).strip())
141
+ return None
142
+
143
+ def _handle_sub(text, raw):
144
+ m = RE_SUB.match(text)
145
+ if m:
146
+ return SubStmt(raw=raw, name=m.group(1).upper(),
147
+ params=m.group(2) or '')
148
+ return None
149
+
150
+ def _handle_function(text, raw):
151
+ m = RE_FUNCTION.match(text)
152
+ if m:
153
+ return FunctionStmt(raw=raw, name=m.group(1).upper(),
154
+ params=m.group(2) or '')
155
+ return None
156
+
157
+ def _handle_call(text, raw):
158
+ m = RE_CALL.match(text)
159
+ if m:
160
+ return CallStmt(raw=raw, name=m.group(1).upper(),
161
+ args=m.group(2) or '')
162
+ return None
163
+
164
+ def _handle_local(text, raw):
165
+ m = RE_LOCAL.match(text)
166
+ if m:
167
+ return LocalStmt(raw=raw, var_list=m.group(1))
168
+ return None
169
+
170
+ def _handle_static(text, raw):
171
+ m = RE_STATIC_DECL.match(text)
172
+ if m:
173
+ return StaticStmt(raw=raw, var_list=m.group(1))
174
+ return None
175
+
176
+ def _handle_shared(text, raw):
177
+ m = RE_SHARED.match(text)
178
+ if m:
179
+ return SharedStmt(raw=raw, var_list=m.group(1))
180
+ return None
181
+
182
+ def _handle_resume(text, raw):
183
+ m = RE_RESUME.match(text)
184
+ if m:
185
+ return ResumeStmt(raw=raw, arg=m.group(1))
186
+ return None
187
+
188
+ def _handle_error(text, raw):
189
+ m = RE_ERROR_STMT.match(text)
190
+ if m:
191
+ return ErrorStmt(raw=raw, code=int(m.group(1)))
192
+ return None
193
+
194
+ def _handle_assert(text, raw):
195
+ m = RE_ASSERT.match(text)
196
+ if m:
197
+ return AssertStmt(raw=raw, condition=m.group(1).strip())
198
+ return None
199
+
200
+ def _handle_swap(text, raw):
201
+ m = RE_SWAP.match(text)
202
+ if m:
203
+ return SwapStmt(raw=raw, a=m.group(1), b=m.group(2))
204
+ return None
205
+
206
+ def _handle_def(text, raw):
207
+ m = RE_DEF_FN.match(text)
208
+ if m:
209
+ return DefFnStmt(raw=raw, name=m.group(1),
210
+ params=m.group(2), body=m.group(3))
211
+ return None
212
+
213
+ def _handle_option(text, raw):
214
+ m = RE_OPTION_BASE.match(text)
215
+ if m:
216
+ return OptionBaseStmt(raw=raw, base=int(m.group(1)))
217
+ return None
218
+
219
+ def _handle_data(text, raw):
220
+ m = RE_DATA.match(text)
221
+ if m:
222
+ return DataStmt(raw=raw, values=m.group(1))
223
+ return None
224
+
225
+ def _handle_read(text, raw):
226
+ m = RE_READ.match(text)
227
+ if m:
228
+ return ReadStmt(raw=raw, var_list=m.group(1))
229
+ return None
230
+
231
+ def _handle_let(text, raw):
232
+ m = RE_LET_ARRAY.match(text)
233
+ if m:
234
+ return LetArrayStmt(raw=raw, name=m.group(1),
235
+ index_expr=m.group(2), value_expr=m.group(3))
236
+ m = RE_LET_STR.match(text)
237
+ if m:
238
+ return LetStrStmt(raw=raw, name=m.group(1), expr=m.group(2))
239
+ m = RE_LET_VAR.match(text)
240
+ if m:
241
+ return LetStmt(raw=raw, name=m.group(1), expr=m.group(2))
242
+ return None
243
+
244
+ def _handle_print(text, raw):
245
+ m = RE_PRINT_USING.match(text)
246
+ if m:
247
+ return PrintUsingStmt(raw=raw, fmt=m.group(1), values=m.group(2))
248
+ m = RE_PRINT_FILE.match(text)
249
+ if m:
250
+ return PrintFileStmt(raw=raw, handle=int(m.group(1)), data=m.group(2))
251
+ m = RE_PRINT.match(text)
252
+ if m:
253
+ return PrintStmt(raw=raw, expr=m.group(1).strip())
254
+ return None
255
+
256
+ def _handle_line(text, raw):
257
+ m = RE_LINE_INPUT.match(text)
258
+ if m:
259
+ return LineInputStmt(raw=raw, prompt=m.group(1), var=m.group(2))
260
+ return None
261
+
262
+ def _handle_get(text, raw):
263
+ m = RE_GET.match(text)
264
+ if m:
265
+ return GetStmt(raw=raw, var=m.group(1))
266
+ return None
267
+
268
+ def _handle_input(text, raw):
269
+ m = RE_INPUT_FILE.match(text)
270
+ if m:
271
+ return InputFileStmt(raw=raw, handle=int(m.group(1)), var=m.group(2))
272
+ m = RE_INPUT.match(text)
273
+ if m:
274
+ return InputStmt(raw=raw, prompt=m.group(1), var=m.group(2))
275
+ return None
276
+
277
+ def _handle_lprint(text, raw):
278
+ m = RE_LPRINT.match(text)
279
+ if m:
280
+ return LprintStmt(raw=raw, expr=m.group(1))
281
+ return None
282
+
283
+ def _handle_poke(text, raw):
284
+ m = RE_POKE.match(text)
285
+ if m:
286
+ return PokeStmt(raw=raw, addr_expr=m.group(1), value_expr=m.group(2))
287
+ return None
288
+
289
+ def _handle_sys(text, raw):
290
+ m = RE_SYS.match(text)
291
+ if m:
292
+ return SysStmt(raw=raw, arg=m.group(1))
293
+ return None
294
+
295
+ def _handle_unitary(text, raw):
296
+ m = RE_UNITARY.match(text)
297
+ if m:
298
+ return UnitaryStmt(raw=raw, name=m.group(1), matrix=m.group(2))
299
+ return None
300
+
301
+ def _handle_redim(text, raw):
302
+ m = RE_REDIM.match(text)
303
+ if m:
304
+ return RedimStmt(raw=raw, name=m.group(1), size=m.group(2))
305
+ return None
306
+
307
+ def _handle_erase(text, raw):
308
+ m = RE_ERASE.match(text)
309
+ if m:
310
+ return EraseStmt(raw=raw, name=m.group(1))
311
+ return None
312
+
313
+ def _handle_dim(text, raw):
314
+ m = RE_DIM_MULTI.match(text)
315
+ if m:
316
+ return DimStmt(raw=raw, name=m.group(1), size=m.group(2))
317
+ m = RE_DIM.match(text)
318
+ if m:
319
+ return DimStmt(raw=raw, name=m.group(1), size=m.group(2))
320
+ return None
321
+
322
+ def _handle_open(text, raw):
323
+ m = RE_OPEN.match(text)
324
+ if m:
325
+ return OpenStmt(raw=raw, path=m.group(1).strip(),
326
+ mode=m.group(2).upper(), handle=int(m.group(3)),
327
+ encoding=m.group(4).strip() if m.group(4) else None)
328
+ return None
329
+
330
+ def _handle_close(text, raw):
331
+ m = RE_CLOSE.match(text)
332
+ if m:
333
+ return CloseStmt(raw=raw, handle=int(m.group(1)))
334
+ return None
335
+
336
+ def _handle_import(text, raw):
337
+ m = RE_IMPORT.match(text)
338
+ if m:
339
+ return ImportStmt(raw=raw, path=m.group(1).strip())
340
+ return None
341
+
342
+ def _handle_chain(text, raw):
343
+ m = RE_CHAIN.match(text)
344
+ if m:
345
+ return ChainStmt(raw=raw, path=m.group(1).strip())
346
+ return None
347
+
348
+ def _handle_merge(text, raw):
349
+ m = RE_MERGE.match(text)
350
+ if m:
351
+ return MergeStmt(raw=raw, path=m.group(1).strip())
352
+ return None
353
+
354
+ def _handle_screen(text, raw):
355
+ m = RE_SCREEN.match(text)
356
+ if m:
357
+ return ScreenStmt(raw=raw, mode=m.group(1))
358
+ return None
359
+
360
+ def _handle_color(text, raw):
361
+ m = RE_COLOR.match(text)
362
+ if m:
363
+ return ColorStmt(raw=raw, fg=m.group(1), bg=m.group(2))
364
+ return None
365
+
366
+ def _handle_locate(text, raw):
367
+ m = RE_LOCATE.match(text)
368
+ if m:
369
+ return LocateStmt(raw=raw, row=m.group(1), col=m.group(2))
370
+ return None
371
+
372
+ def _handle_measure_basis(text, raw):
373
+ m = RE_MEASURE_BASIS.match(text)
374
+ if m:
375
+ return MeasureBasisStmt(raw=raw, basis=m.group(1).upper(),
376
+ qubit_expr=m.group(2))
377
+ return None
378
+
379
+ def _handle_syndrome(text, raw):
380
+ m = RE_SYNDROME.match(text)
381
+ if m:
382
+ return SyndromeStmt(raw=raw, rest=m.group(1).strip())
383
+ return None
384
+
385
+ def _handle_meas(text, raw):
386
+ m = RE_MEAS.match(text)
387
+ if m:
388
+ return MeasStmt(raw=raw, qubit_expr=m.group(1), var=m.group(2))
389
+ return None
390
+
391
+ def _handle_reset(text, raw):
392
+ m = RE_RESET.match(text)
393
+ if m:
394
+ return ResetStmt(raw=raw, qubit_expr=m.group(1))
395
+ return None
396
+
397
+ def _handle_send(text, raw):
398
+ m = RE_SEND.match(text)
399
+ if m:
400
+ return SendStmt(raw=raw, reg=m.group(1).upper(),
401
+ qubit_expr=m.group(2), var=m.group(3))
402
+ return None
403
+
404
+ def _handle_share(text, raw):
405
+ m = RE_SHARE.match(text)
406
+ if m:
407
+ return ShareStmt(raw=raw, reg1=m.group(1).upper(), q1=int(m.group(2)),
408
+ reg2=m.group(3).upper(), q2=int(m.group(4)))
409
+ return None
410
+
411
+
412
+ # ═══════════════════════════════════════════════════════════════════════
413
+ # First-word dispatch table
414
+ # ═══════════════════════════════════════════════════════════════════════
415
+
416
+ _KEYWORD_PARSERS = {
417
+ 'GOTO': _handle_goto,
418
+ 'GOSUB': _handle_gosub,
419
+ 'FOR': _handle_for,
420
+ 'NEXT': _handle_next,
421
+ 'WHILE': _handle_while,
422
+ 'IF': _handle_if,
423
+ 'DO': _handle_do,
424
+ 'LOOP': _handle_loop,
425
+ 'EXIT': _handle_exit,
426
+ 'ON': _handle_on,
427
+ 'SELECT': _handle_select,
428
+ 'CASE': _handle_case,
429
+ 'SUB': _handle_sub,
430
+ 'FUNCTION': _handle_function,
431
+ 'CALL': _handle_call,
432
+ 'LOCAL': _handle_local,
433
+ 'STATIC': _handle_static,
434
+ 'SHARED': _handle_shared,
435
+ 'RESUME': _handle_resume,
436
+ 'ERROR': _handle_error,
437
+ 'ASSERT': _handle_assert,
438
+ 'SWAP': _handle_swap,
439
+ 'DEF': _handle_def,
440
+ 'OPTION': _handle_option,
441
+ 'DATA': _handle_data,
442
+ 'READ': _handle_read,
443
+ 'LET': _handle_let,
444
+ 'PRINT': _handle_print,
445
+ 'LINE': _handle_line,
446
+ 'GET': _handle_get,
447
+ 'INPUT': _handle_input,
448
+ 'LPRINT': _handle_lprint,
449
+ 'POKE': _handle_poke,
450
+ 'SYS': _handle_sys,
451
+ 'UNITARY': _handle_unitary,
452
+ 'REDIM': _handle_redim,
453
+ 'ERASE': _handle_erase,
454
+ 'DIM': _handle_dim,
455
+ 'OPEN': _handle_open,
456
+ 'CLOSE': _handle_close,
457
+ 'IMPORT': _handle_import,
458
+ 'CHAIN': _handle_chain,
459
+ 'MERGE': _handle_merge,
460
+ 'SCREEN': _handle_screen,
461
+ 'COLOR': _handle_color,
462
+ 'LOCATE': _handle_locate,
463
+ 'MEASURE_X': _handle_measure_basis,
464
+ 'MEASURE_Y': _handle_measure_basis,
465
+ 'MEASURE_Z': _handle_measure_basis,
466
+ 'SYNDROME': _handle_syndrome,
467
+ 'MEAS': _handle_meas,
468
+ 'RESET': _handle_reset,
469
+ 'SEND': _handle_send,
470
+ 'SHARE': _handle_share,
471
+ }
472
+
473
+
474
+ def _split_colon_stmts(stmt: str) -> list[str]:
475
+ """Split colon-separated statements, inheriting @register prefixes."""
476
+ parts = []
477
+ last_reg = None
478
+ for sub in stmt.split(':'):
479
+ sub = sub.strip()
480
+ if not sub:
481
+ continue
482
+ m_reg = RE_AT_REG.match(sub)
483
+ if m_reg:
484
+ last_reg = m_reg.group(1).upper()
485
+ elif last_reg and not sub.upper().startswith((
486
+ 'SEND', 'IF ', 'REM', 'FOR', 'NEXT',
487
+ 'SHARE', 'MEASURE', '@')):
488
+ sub = f"@{last_reg} {sub}"
489
+ parts.append(sub)
490
+ return parts
491
+
492
+
493
+ def parse_stmt(raw: str) -> Stmt:
494
+ """Parse a raw statement string into a typed Stmt.
495
+
496
+ Returns RawStmt only for gate applications and truly unrecognized input.
497
+ """
498
+ text = raw.strip()
499
+ if not text:
500
+ return RawStmt(raw=raw)
501
+
502
+ upper = text.upper()
503
+
504
+ # ── Terminals ──────────────────────────────────────────────────
505
+ if upper.startswith('REM') or upper.startswith("'"):
506
+ return RemStmt(raw=raw)
507
+ if upper == 'MEASURE':
508
+ return MeasureStmt(raw=raw)
509
+ if upper == 'END':
510
+ return EndStmt(raw=raw)
511
+ if upper == 'RETURN':
512
+ return ReturnStmt(raw=raw)
513
+ if upper == 'BARRIER':
514
+ return BarrierStmt(raw=raw)
515
+ if upper == 'WEND':
516
+ return WendStmt(raw=raw)
517
+ if upper == 'RESTORE':
518
+ return RestoreStmt(raw=raw)
519
+ if upper == 'END SELECT':
520
+ return EndSelectStmt(raw=raw)
521
+ if upper == 'STOP':
522
+ return StopStmt(raw=raw)
523
+
524
+ # ── END SUB / END FUNCTION ────────────────────────────────────
525
+ m = RE_END_SUB.match(text)
526
+ if m:
527
+ return EndSubStmt(raw=raw)
528
+ m = RE_END_FUNCTION.match(text)
529
+ if m:
530
+ return EndFunctionStmt(raw=raw)
531
+
532
+ # ── First-word dispatch ───────────────────────────────────────
533
+ first_word = text.split(None, 1)[0].upper()
534
+ handler = _KEYWORD_PARSERS.get(first_word)
535
+ if handler is not None:
536
+ result = handler(text, raw)
537
+ if result is not None:
538
+ return result
539
+
540
+ # ── @REG lines ────────────────────────────────────────────────
541
+ if text.startswith('@'):
542
+ m = RE_AT_REG_LINE.match(text)
543
+ if m:
544
+ return AtRegStmt(raw=raw, reg=m.group(1).upper(),
545
+ inner=m.group(2).strip())
546
+
547
+ # ── Compound (colon-separated) ────────────────────────────────
548
+ if ':' in text:
549
+ parts = _split_colon_stmts(text)
550
+ if len(parts) > 1:
551
+ return CompoundStmt(raw=raw, parts=tuple(parts))
552
+
553
+ # ── Gate application ────────────────────────────────────────────
554
+ canonical = GATE_ALIASES.get(first_word, first_word)
555
+ if canonical in GATE_TABLE:
556
+ rest_args = text.split(None, 1)[1].strip() if ' ' in text.strip() else ''
557
+ if ',' in rest_args:
558
+ args = tuple(a.strip() for a in rest_args.split(',') if a.strip())
559
+ else:
560
+ args = tuple(a.strip() for a in rest_args.split() if a.strip())
561
+ return GateStmt(raw=raw, name=canonical, args=args)
562
+
563
+ # ── Fallback (subroutine calls, truly unrecognized) ─────────────
564
+ return RawStmt(raw=raw)