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.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- qubasic-0.1.0.dist-info/top_level.txt +2 -0
qbasic_core/patterns.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Pre-compiled regex patterns for the QBASIC parser."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
6
|
+
# Pre-compiled regexes
|
|
7
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
RE_LINE_NUM = re.compile(r'^(\d+)\s*(.*)')
|
|
10
|
+
RE_DEF_SINGLE = re.compile(r'(\w+)(?:\(([^)]*)\))?\s*=\s*(.*)')
|
|
11
|
+
RE_DEF_BEGIN = re.compile(r'DEF\s+BEGIN\s+(\w+)(?:\(([^)]*)\))?', re.IGNORECASE)
|
|
12
|
+
RE_REG_INDEX = re.compile(r'(\w+)\[(\d+)\]')
|
|
13
|
+
RE_AT_REG = re.compile(r'@([A-Z])\s+', re.IGNORECASE)
|
|
14
|
+
RE_AT_REG_LINE = re.compile(r'@([A-Z])\s+(.*)', re.IGNORECASE)
|
|
15
|
+
RE_SEND = re.compile(r'SEND\s+([A-Z])\s+(\S+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
16
|
+
RE_SHARE = re.compile(r'SHARE\s+([A-Z])\s+(\d+)\s*,?\s*([A-Z])\s+(\d+)', re.IGNORECASE)
|
|
17
|
+
RE_MEAS = re.compile(r'MEAS\s+(\S+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
18
|
+
RE_RESET = re.compile(r'RESET\s+(\S+)', re.IGNORECASE)
|
|
19
|
+
RE_UNITARY = re.compile(r'UNITARY\s+(\w+)\s*=\s*(\[.+\])', re.IGNORECASE)
|
|
20
|
+
RE_DIM = re.compile(r'DIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
|
|
21
|
+
RE_REDIM = re.compile(r'REDIM\s+(\w+)\((\d+)\)', re.IGNORECASE)
|
|
22
|
+
RE_ERASE = re.compile(r'ERASE\s+(\w+)', re.IGNORECASE)
|
|
23
|
+
RE_GET = re.compile(r'GET\s+(\w+\$?)', re.IGNORECASE)
|
|
24
|
+
RE_INPUT = re.compile(r'INPUT\s+(?:"([^"]*)"\s*,\s*)?(\w+)', re.IGNORECASE)
|
|
25
|
+
RE_CTRL = re.compile(r'CTRL\s+(\w+)\s+(.*)', re.IGNORECASE)
|
|
26
|
+
RE_INV = re.compile(r'INV\s+(\w+)\s+(.*)', re.IGNORECASE)
|
|
27
|
+
RE_LET_ARRAY = re.compile(r'LET\s+(\w+)\((.+?)\)\s*=\s*(.*)', re.IGNORECASE)
|
|
28
|
+
RE_LET_VAR = re.compile(r'LET\s+(\w+)\s*=\s*(.*)', re.IGNORECASE)
|
|
29
|
+
RE_PRINT = re.compile(r'PRINT\s+(.*)', re.IGNORECASE)
|
|
30
|
+
RE_GOTO = re.compile(r'GOTO\s+(\d+)\s*$', re.IGNORECASE)
|
|
31
|
+
RE_GOSUB = re.compile(r'GOSUB\s+(\d+)\s*$', re.IGNORECASE)
|
|
32
|
+
RE_FOR = re.compile(
|
|
33
|
+
r'FOR\s+(\w+)\s*=\s*(.+?)\s+TO\s+(.+?)(?:\s+STEP\s+(.+))?\s*$', re.IGNORECASE)
|
|
34
|
+
RE_NEXT = re.compile(r'NEXT\s+(\w+)\s*$', re.IGNORECASE)
|
|
35
|
+
RE_WHILE = re.compile(r'WHILE\s+(.+)$', re.IGNORECASE)
|
|
36
|
+
RE_IF_THEN = re.compile(
|
|
37
|
+
r'IF\s+(.+?)\s+THEN\s+(.*?)(?:\s+ELSE\s+(.*))?$', re.IGNORECASE)
|
|
38
|
+
RE_GOTO_GOSUB_TARGET = re.compile(r'(GOTO|GOSUB)\s+(\d+)', re.IGNORECASE)
|
|
39
|
+
RE_MEASURE_BASIS = re.compile(
|
|
40
|
+
r'MEASURE_(X|Y|Z)\s+(\S+)', re.IGNORECASE)
|
|
41
|
+
RE_SYNDROME = re.compile(
|
|
42
|
+
r'SYNDROME\s+(.*)', re.IGNORECASE)
|
|
43
|
+
|
|
44
|
+
# ── Classic BASIC, memory, SUB/FUNCTION, debug ──────────────────────
|
|
45
|
+
|
|
46
|
+
RE_DATA = re.compile(r'DATA\s+(.*)', re.IGNORECASE)
|
|
47
|
+
RE_READ = re.compile(r'READ\s+(.*)', re.IGNORECASE)
|
|
48
|
+
RE_ON_GOTO = re.compile(r'ON\s+(.+?)\s+GOTO\s+([\d\s,]+)', re.IGNORECASE)
|
|
49
|
+
RE_ON_GOSUB = re.compile(r'ON\s+(.+?)\s+GOSUB\s+([\d\s,]+)', re.IGNORECASE)
|
|
50
|
+
RE_SELECT_CASE = re.compile(r'SELECT\s+CASE\s+(.*)', re.IGNORECASE)
|
|
51
|
+
RE_CASE = re.compile(r'CASE\s+(.*)', re.IGNORECASE)
|
|
52
|
+
RE_DO = re.compile(r'DO(?:\s+(WHILE|UNTIL)\s+(.+))?\s*$', re.IGNORECASE)
|
|
53
|
+
RE_LOOP_STMT = re.compile(r'LOOP(?:\s+(WHILE|UNTIL)\s+(.+))?\s*$', re.IGNORECASE)
|
|
54
|
+
RE_EXIT = re.compile(r'EXIT\s+(FOR|WHILE|DO|SUB|FUNCTION)\s*$', re.IGNORECASE)
|
|
55
|
+
RE_SUB = re.compile(r'SUB\s+(\w+)(?:\(([^)]*)\))?\s*$', re.IGNORECASE)
|
|
56
|
+
RE_END_SUB = re.compile(r'END\s+SUB\s*$', re.IGNORECASE)
|
|
57
|
+
RE_FUNCTION = re.compile(r'FUNCTION\s+(\w+)(?:\(([^)]*)\))?\s*$', re.IGNORECASE)
|
|
58
|
+
RE_END_FUNCTION = re.compile(r'END\s+FUNCTION\s*$', re.IGNORECASE)
|
|
59
|
+
RE_CALL = re.compile(r'CALL\s+(\w+)(?:\(([^)]*)\))?\s*$', re.IGNORECASE)
|
|
60
|
+
RE_LOCAL = re.compile(r'LOCAL\s+(.*)', re.IGNORECASE)
|
|
61
|
+
RE_STATIC_DECL = re.compile(r'STATIC\s+(.*)', re.IGNORECASE)
|
|
62
|
+
RE_SHARED = re.compile(r'SHARED\s+(.*)', re.IGNORECASE)
|
|
63
|
+
RE_ON_ERROR = re.compile(r'ON\s+ERROR\s+GOTO\s+(\d+)', re.IGNORECASE)
|
|
64
|
+
RE_RESUME = re.compile(r'RESUME(?:\s+(.+))?\s*$', re.IGNORECASE)
|
|
65
|
+
RE_ERROR_STMT = re.compile(r'ERROR\s+(\d+)', re.IGNORECASE)
|
|
66
|
+
RE_ASSERT = re.compile(r'ASSERT\s+(.*)', re.IGNORECASE)
|
|
67
|
+
RE_SWAP = re.compile(r'SWAP\s+(\w+\$?)\s*,\s*(\w+\$?)', re.IGNORECASE)
|
|
68
|
+
RE_POKE = re.compile(r'POKE\s+(.+?)\s*,\s*(.+)', re.IGNORECASE)
|
|
69
|
+
RE_SYS = re.compile(r'SYS\s+(.+)', re.IGNORECASE)
|
|
70
|
+
RE_OPEN = re.compile(
|
|
71
|
+
r'OPEN\s+"?([^"]+)"?\s+FOR\s+(INPUT|OUTPUT|APPEND|RANDOM)\s+AS\s+#?(\d+)'
|
|
72
|
+
r'(?:\s+ENCODING\s+"?([^"]*)"?)?',
|
|
73
|
+
re.IGNORECASE)
|
|
74
|
+
RE_CLOSE = re.compile(r'CLOSE\s+#?(\d+)', re.IGNORECASE)
|
|
75
|
+
RE_PRINT_FILE = re.compile(r'PRINT\s+#(\d+)\s*,\s*(.*)', re.IGNORECASE)
|
|
76
|
+
RE_INPUT_FILE = re.compile(r'INPUT\s+#(\d+)\s*,\s*(\w+\$?)', re.IGNORECASE)
|
|
77
|
+
RE_LINE_INPUT = re.compile(
|
|
78
|
+
r'LINE\s+INPUT\s+(?:"([^"]*)"\s*,\s*)?(\w+\$?)', re.IGNORECASE)
|
|
79
|
+
RE_OPTION_BASE = re.compile(r'OPTION\s+BASE\s+([01])', re.IGNORECASE)
|
|
80
|
+
RE_IMPORT = re.compile(r'IMPORT\s+"?([^"]+)"?', re.IGNORECASE)
|
|
81
|
+
RE_SAVE_EXPECT = re.compile(r'SAVE_EXPECT\s+(\w+)\s+([\d\s,]+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
82
|
+
RE_SAVE_PROBS = re.compile(r'SAVE_PROBS\s+([\d\s,]+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
83
|
+
RE_SAVE_AMPS = re.compile(r'SAVE_AMPS\s+([\d\s,]+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
84
|
+
RE_SET_STATE = re.compile(r'SET_STATE\s+(.*)', re.IGNORECASE)
|
|
85
|
+
RE_TYPE_BEGIN = re.compile(r'TYPE\s+(\w+)', re.IGNORECASE)
|
|
86
|
+
RE_TYPE_FIELD = re.compile(r'(\w+)\s+AS\s+(INTEGER|FLOAT|STRING|QUBIT)', re.IGNORECASE)
|
|
87
|
+
RE_END_TYPE = re.compile(r'END\s+TYPE', re.IGNORECASE)
|
|
88
|
+
RE_DIM_TYPE = re.compile(r'DIM\s+(\w+)\s+AS\s+(\w+)', re.IGNORECASE)
|
|
89
|
+
RE_CHAIN = re.compile(r'CHAIN\s+"?([^"]+)"?', re.IGNORECASE)
|
|
90
|
+
RE_MERGE = re.compile(r'MERGE\s+"?([^"]+)"?', re.IGNORECASE)
|
|
91
|
+
RE_DEF_FN = re.compile(
|
|
92
|
+
r'DEF\s+FN\s*(\w+)\s*\(([^)]*)\)\s*=\s*(.*)', re.IGNORECASE)
|
|
93
|
+
RE_PRINT_USING = re.compile(
|
|
94
|
+
r'PRINT\s+USING\s+"([^"]+)"\s*;\s*(.*)', re.IGNORECASE)
|
|
95
|
+
RE_COLOR = re.compile(r'COLOR\s+(\w+)(?:\s*,\s*(\w+))?', re.IGNORECASE)
|
|
96
|
+
RE_LOCATE = re.compile(r'LOCATE\s+(\d+)\s*,\s*(\d+)', re.IGNORECASE)
|
|
97
|
+
RE_SCREEN = re.compile(r'SCREEN\s+(\d+)', re.IGNORECASE)
|
|
98
|
+
RE_LPRINT = re.compile(r'LPRINT\s+(.*)', re.IGNORECASE)
|
|
99
|
+
RE_ON_MEASURE = re.compile(r'ON\s+MEASURE\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
100
|
+
RE_ON_TIMER = re.compile(r'ON\s+TIMER\s*\((\d+)\)\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
101
|
+
RE_DIM_MULTI = re.compile(r'DIM\s+(\w+)\((\d+(?:\s*,\s*\d+)*)\)', re.IGNORECASE)
|
|
102
|
+
RE_LET_STR = re.compile(r'LET\s+(\w+\$)\s*=\s*(.*)', re.IGNORECASE)
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"RE_LINE_NUM",
|
|
106
|
+
"RE_DEF_SINGLE",
|
|
107
|
+
"RE_DEF_BEGIN",
|
|
108
|
+
"RE_REG_INDEX",
|
|
109
|
+
"RE_AT_REG",
|
|
110
|
+
"RE_AT_REG_LINE",
|
|
111
|
+
"RE_SEND",
|
|
112
|
+
"RE_SHARE",
|
|
113
|
+
"RE_MEAS",
|
|
114
|
+
"RE_RESET",
|
|
115
|
+
"RE_UNITARY",
|
|
116
|
+
"RE_DIM",
|
|
117
|
+
"RE_REDIM",
|
|
118
|
+
"RE_ERASE",
|
|
119
|
+
"RE_GET",
|
|
120
|
+
"RE_INPUT",
|
|
121
|
+
"RE_CTRL",
|
|
122
|
+
"RE_INV",
|
|
123
|
+
"RE_LET_ARRAY",
|
|
124
|
+
"RE_LET_VAR",
|
|
125
|
+
"RE_PRINT",
|
|
126
|
+
"RE_GOTO",
|
|
127
|
+
"RE_GOSUB",
|
|
128
|
+
"RE_FOR",
|
|
129
|
+
"RE_NEXT",
|
|
130
|
+
"RE_WHILE",
|
|
131
|
+
"RE_IF_THEN",
|
|
132
|
+
"RE_GOTO_GOSUB_TARGET",
|
|
133
|
+
"RE_MEASURE_BASIS",
|
|
134
|
+
"RE_SYNDROME",
|
|
135
|
+
"RE_DATA",
|
|
136
|
+
"RE_READ",
|
|
137
|
+
"RE_ON_GOTO",
|
|
138
|
+
"RE_ON_GOSUB",
|
|
139
|
+
"RE_SELECT_CASE",
|
|
140
|
+
"RE_CASE",
|
|
141
|
+
"RE_DO",
|
|
142
|
+
"RE_LOOP_STMT",
|
|
143
|
+
"RE_EXIT",
|
|
144
|
+
"RE_SUB",
|
|
145
|
+
"RE_END_SUB",
|
|
146
|
+
"RE_FUNCTION",
|
|
147
|
+
"RE_END_FUNCTION",
|
|
148
|
+
"RE_CALL",
|
|
149
|
+
"RE_LOCAL",
|
|
150
|
+
"RE_STATIC_DECL",
|
|
151
|
+
"RE_SHARED",
|
|
152
|
+
"RE_ON_ERROR",
|
|
153
|
+
"RE_RESUME",
|
|
154
|
+
"RE_ERROR_STMT",
|
|
155
|
+
"RE_ASSERT",
|
|
156
|
+
"RE_SWAP",
|
|
157
|
+
"RE_POKE",
|
|
158
|
+
"RE_SYS",
|
|
159
|
+
"RE_OPEN",
|
|
160
|
+
"RE_CLOSE",
|
|
161
|
+
"RE_PRINT_FILE",
|
|
162
|
+
"RE_INPUT_FILE",
|
|
163
|
+
"RE_LINE_INPUT",
|
|
164
|
+
"RE_OPTION_BASE",
|
|
165
|
+
"RE_IMPORT",
|
|
166
|
+
"RE_SAVE_EXPECT",
|
|
167
|
+
"RE_SAVE_PROBS",
|
|
168
|
+
"RE_SAVE_AMPS",
|
|
169
|
+
"RE_SET_STATE",
|
|
170
|
+
"RE_TYPE_BEGIN",
|
|
171
|
+
"RE_TYPE_FIELD",
|
|
172
|
+
"RE_END_TYPE",
|
|
173
|
+
"RE_DIM_TYPE",
|
|
174
|
+
"RE_CHAIN",
|
|
175
|
+
"RE_MERGE",
|
|
176
|
+
"RE_DEF_FN",
|
|
177
|
+
"RE_PRINT_USING",
|
|
178
|
+
"RE_COLOR",
|
|
179
|
+
"RE_LOCATE",
|
|
180
|
+
"RE_SCREEN",
|
|
181
|
+
"RE_LPRINT",
|
|
182
|
+
"RE_ON_MEASURE",
|
|
183
|
+
"RE_ON_TIMER",
|
|
184
|
+
"RE_DIM_MULTI",
|
|
185
|
+
"RE_LET_STR",
|
|
186
|
+
]
|
qbasic_core/profiler.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""QBASIC profiler — profile mode, gate count, depth tracking, statistics accumulator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
MAX_STATS_RUNS = 10000
|
|
8
|
+
import math
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _NullIOPort:
|
|
13
|
+
def write(self, text): pass
|
|
14
|
+
def writeln(self, text): pass
|
|
15
|
+
def read_line(self, prompt): return ''
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProfilerMixin:
|
|
19
|
+
"""Profiling and statistics for QBasicTerminal.
|
|
20
|
+
|
|
21
|
+
Requires: TerminalProtocol — uses self.program, self.last_counts,
|
|
22
|
+
self.shots, self.cmd_run().
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def _init_profiler(self) -> None:
|
|
26
|
+
self._profile_mode: bool = False
|
|
27
|
+
self._profile_data: dict[int, dict[str, float]] = {}
|
|
28
|
+
self._profile_start: float = 0.0
|
|
29
|
+
self._depth_counter: int = 0
|
|
30
|
+
self._gate_counter: int = 0
|
|
31
|
+
self._stats_runs: list[dict[str, int]] = []
|
|
32
|
+
|
|
33
|
+
# ── Profile mode ───────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def cmd_profile(self, rest: str = '') -> None:
|
|
36
|
+
"""PROFILE [ON|OFF|SHOW] — toggle profiling or show results."""
|
|
37
|
+
arg = rest.strip().upper()
|
|
38
|
+
if arg == 'ON':
|
|
39
|
+
self._profile_mode = True
|
|
40
|
+
self._profile_data.clear()
|
|
41
|
+
self.io.writeln("PROFILE ON")
|
|
42
|
+
elif arg == 'OFF':
|
|
43
|
+
self._profile_mode = False
|
|
44
|
+
self.io.writeln("PROFILE OFF")
|
|
45
|
+
elif arg == 'SHOW' or not arg:
|
|
46
|
+
self._show_profile()
|
|
47
|
+
else:
|
|
48
|
+
self.io.writeln("?USAGE: PROFILE [ON|OFF|SHOW]")
|
|
49
|
+
|
|
50
|
+
def _profile_line_start(self, line_num: int) -> None:
|
|
51
|
+
if self._profile_mode:
|
|
52
|
+
self._profile_start = time.perf_counter()
|
|
53
|
+
|
|
54
|
+
def _profile_line_end(self, line_num: int, gates: int = 0) -> None:
|
|
55
|
+
if self._profile_mode:
|
|
56
|
+
dt = (time.perf_counter() - self._profile_start) * 1000 # ms
|
|
57
|
+
if line_num not in self._profile_data:
|
|
58
|
+
self._profile_data[line_num] = {'time_ms': 0.0, 'calls': 0, 'gates': 0}
|
|
59
|
+
entry = self._profile_data[line_num]
|
|
60
|
+
entry['time_ms'] += dt
|
|
61
|
+
entry['calls'] += 1
|
|
62
|
+
entry['gates'] += gates
|
|
63
|
+
|
|
64
|
+
def _show_profile(self) -> None:
|
|
65
|
+
if not self._profile_data:
|
|
66
|
+
self.io.writeln(" No profile data (PROFILE ON, then RUN)")
|
|
67
|
+
return
|
|
68
|
+
self.io.writeln("\n Profile Results:")
|
|
69
|
+
self.io.writeln(f" {'Line':>6} {'Time(ms)':>10} {'Calls':>6} {'Gates':>6} Source")
|
|
70
|
+
total_time = sum(d['time_ms'] for d in self._profile_data.values())
|
|
71
|
+
for ln in sorted(self._profile_data.keys()):
|
|
72
|
+
d = self._profile_data[ln]
|
|
73
|
+
src = self.program.get(ln, '')[:40]
|
|
74
|
+
pct = 100 * d['time_ms'] / total_time if total_time > 0 else 0
|
|
75
|
+
self.io.writeln(f" {ln:>6} {d['time_ms']:>9.2f} {d['calls']:>6} {d['gates']:>6} {src}")
|
|
76
|
+
self.io.writeln(f"\n Total: {total_time:.2f} ms")
|
|
77
|
+
self.io.writeln('')
|
|
78
|
+
|
|
79
|
+
# ── Gate/depth tracking ────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def _track_gate(self) -> None:
|
|
82
|
+
self._gate_counter += 1
|
|
83
|
+
|
|
84
|
+
def _track_depth(self, depth: int) -> None:
|
|
85
|
+
self._depth_counter = max(self._depth_counter, depth)
|
|
86
|
+
|
|
87
|
+
# ── Statistics accumulator ─────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def cmd_stats(self, rest: str = '') -> None:
|
|
90
|
+
"""STATS [N|SHOW|CLEAR] — multi-run statistics accumulator."""
|
|
91
|
+
arg = rest.strip().upper()
|
|
92
|
+
if arg == 'CLEAR':
|
|
93
|
+
self._stats_runs.clear()
|
|
94
|
+
self.io.writeln("STATS CLEARED")
|
|
95
|
+
return
|
|
96
|
+
if arg == 'SHOW' or not arg:
|
|
97
|
+
self._show_stats()
|
|
98
|
+
return
|
|
99
|
+
# STATS N — run N trials
|
|
100
|
+
try:
|
|
101
|
+
n = int(arg)
|
|
102
|
+
except ValueError:
|
|
103
|
+
self.io.writeln("?USAGE: STATS [N|SHOW|CLEAR]")
|
|
104
|
+
return
|
|
105
|
+
if n < 1:
|
|
106
|
+
self.io.writeln("?STATS needs at least 1 run")
|
|
107
|
+
return
|
|
108
|
+
self.io.writeln(f"\nRunning {n} trials...")
|
|
109
|
+
for trial in range(n):
|
|
110
|
+
old_io = self.io
|
|
111
|
+
self.io = _NullIOPort()
|
|
112
|
+
try:
|
|
113
|
+
self.cmd_run()
|
|
114
|
+
finally:
|
|
115
|
+
self.io = old_io
|
|
116
|
+
if self.last_counts:
|
|
117
|
+
if len(self._stats_runs) >= MAX_STATS_RUNS:
|
|
118
|
+
self.io.writeln(f"?STATS: run limit ({MAX_STATS_RUNS}) reached, stopping collection")
|
|
119
|
+
break
|
|
120
|
+
self._stats_runs.append(dict(self.last_counts))
|
|
121
|
+
if n > 10 and (trial + 1) % (n // 10) == 0:
|
|
122
|
+
self.io.write(f" {100 * (trial + 1) // n}%..." + '\r')
|
|
123
|
+
if n > 10:
|
|
124
|
+
self.io.write(" " * 30 + '\r')
|
|
125
|
+
self.io.writeln(f"Collected {len(self._stats_runs)} runs ({n} trials)")
|
|
126
|
+
|
|
127
|
+
def _show_stats(self) -> None:
|
|
128
|
+
if not self._stats_runs:
|
|
129
|
+
self.io.writeln(" No statistics collected (STATS N to run N trials)")
|
|
130
|
+
return
|
|
131
|
+
n = len(self._stats_runs)
|
|
132
|
+
# Aggregate: count how often each state appears across runs
|
|
133
|
+
state_totals: dict[str, list[int]] = {}
|
|
134
|
+
for run in self._stats_runs:
|
|
135
|
+
total = sum(run.values())
|
|
136
|
+
for state, count in run.items():
|
|
137
|
+
if state not in state_totals:
|
|
138
|
+
state_totals[state] = []
|
|
139
|
+
state_totals[state].append(count)
|
|
140
|
+
# States not seen in this run get 0
|
|
141
|
+
for state in state_totals:
|
|
142
|
+
if state not in run:
|
|
143
|
+
state_totals[state].append(0)
|
|
144
|
+
# Pad lists to same length
|
|
145
|
+
for state in state_totals:
|
|
146
|
+
while len(state_totals[state]) < n:
|
|
147
|
+
state_totals[state].append(0)
|
|
148
|
+
self.io.writeln(f"\n Statistics over {n} runs:")
|
|
149
|
+
self.io.writeln(f" {'State':>10} {'Mean':>8} {'StdDev':>8} {'Min':>6} {'Max':>6}")
|
|
150
|
+
for state in sorted(state_totals.keys()):
|
|
151
|
+
vals = state_totals[state]
|
|
152
|
+
mean = sum(vals) / len(vals)
|
|
153
|
+
variance = sum((v - mean) ** 2 for v in vals) / len(vals)
|
|
154
|
+
std = math.sqrt(variance)
|
|
155
|
+
self.io.writeln(f" |{state}\u27E9 {mean:>8.1f} {std:>8.2f} {min(vals):>6} {max(vals):>6}")
|
|
156
|
+
self.io.writeln('')
|