stackvm 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.
- stackvm/__init__.py +3 -0
- stackvm/__main__.py +173 -0
- stackvm/_api.py +1407 -0
- stackvm/_ui.py +1036 -0
- stackvm/_util.py +269 -0
- stackvm/py.typed +0 -0
- stackvm/resources/icon.ico +0 -0
- stackvm-0.1.0.dist-info/METADATA +21 -0
- stackvm-0.1.0.dist-info/RECORD +10 -0
- stackvm-0.1.0.dist-info/WHEEL +4 -0
stackvm/_ui.py
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import itertools as it
|
|
5
|
+
import platform
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from concurrent.futures import Future
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import cache, partial
|
|
10
|
+
from importlib import metadata, resources
|
|
11
|
+
from io import IOBase, StringIO
|
|
12
|
+
from random import Random
|
|
13
|
+
from types import CoroutineType, NoneType, SimpleNamespace
|
|
14
|
+
from typing import Any, ClassVar, cast
|
|
15
|
+
from weakref import WeakKeyDictionary
|
|
16
|
+
|
|
17
|
+
import flet as ft
|
|
18
|
+
|
|
19
|
+
from stackvm._api import Executor, StackMachine
|
|
20
|
+
from stackvm._util import Instruction as VMInstruction
|
|
21
|
+
from stackvm._util import StackItem, VMFrame
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cache
|
|
25
|
+
def monospace(size: int = 24) -> ft.TextStyle:
|
|
26
|
+
return ft.TextStyle(
|
|
27
|
+
size=size,
|
|
28
|
+
font_family="JetBrains Mono",
|
|
29
|
+
font_family_fallback=["Fira Code", "Consolas", "Inconsolata", "SF Mono", "Monospace"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
COLORS: dict[type, ft.Colors] = {
|
|
34
|
+
NoneType: ft.Colors.GREY_600,
|
|
35
|
+
bool: ft.Colors.BLUE_600,
|
|
36
|
+
int: ft.Colors.GREEN_600,
|
|
37
|
+
float: ft.Colors.ORANGE_600,
|
|
38
|
+
str: ft.Colors.PURPLE_600,
|
|
39
|
+
bytes: ft.Colors.BROWN_600,
|
|
40
|
+
slice: ft.Colors.CYAN_600,
|
|
41
|
+
list: ft.Colors.RED_600,
|
|
42
|
+
dict: ft.Colors.YELLOW_600,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def loop(
|
|
47
|
+
page: ft.Page,
|
|
48
|
+
executor: Executor,
|
|
49
|
+
pause: asyncio.Event,
|
|
50
|
+
send: asyncio.Queue[tuple[bool, Any]],
|
|
51
|
+
receive: asyncio.Queue[float],
|
|
52
|
+
) -> object:
|
|
53
|
+
await send.put((False, -1))
|
|
54
|
+
delay = await receive.get()
|
|
55
|
+
await asyncio.sleep(delay)
|
|
56
|
+
while True:
|
|
57
|
+
await pause.wait()
|
|
58
|
+
current = executor.state.frames[-1].current
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
returned, value = await asyncio.create_task(asyncio.to_thread(executor.step))
|
|
62
|
+
except BaseException as e: # noqa: BLE001
|
|
63
|
+
await send.put((True, (current, e)))
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if returned:
|
|
67
|
+
await send.put((True, (current, value)))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
await send.put((False, current))
|
|
71
|
+
delay = await receive.get()
|
|
72
|
+
|
|
73
|
+
if pause.is_set(): # spam clicking step is okay
|
|
74
|
+
await asyncio.sleep(delay)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
ITEM_HEIGHT = 50
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def item_height(obj: object) -> float:
|
|
81
|
+
if isinstance(obj, list):
|
|
82
|
+
return ITEM_HEIGHT + sum(item_height(i) for i in obj)
|
|
83
|
+
|
|
84
|
+
if isinstance(obj, dict):
|
|
85
|
+
return ITEM_HEIGHT + sum(max(item_height(k), item_height(v)) for k, v in obj.items())
|
|
86
|
+
|
|
87
|
+
return ITEM_HEIGHT
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@ft.control
|
|
91
|
+
class Item(ft.Container):
|
|
92
|
+
@property
|
|
93
|
+
def item(self) -> object:
|
|
94
|
+
return self.data
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def app(self) -> App:
|
|
98
|
+
try:
|
|
99
|
+
return self.page.data
|
|
100
|
+
except RuntimeError:
|
|
101
|
+
return ft.context.page.data
|
|
102
|
+
|
|
103
|
+
def init(self):
|
|
104
|
+
obj = self.item
|
|
105
|
+
color = COLORS.get(type(obj), ft.Colors.PINK_600)
|
|
106
|
+
|
|
107
|
+
self.bgcolor = color
|
|
108
|
+
self.border = ft.Border.all(2, ft.Colors.BLACK)
|
|
109
|
+
|
|
110
|
+
self.height = item_height(obj)
|
|
111
|
+
self.border_radius = ITEM_HEIGHT / 5
|
|
112
|
+
|
|
113
|
+
def before_update(self):
|
|
114
|
+
obj = self.item
|
|
115
|
+
size = 24
|
|
116
|
+
|
|
117
|
+
if isinstance(obj, IOBase):
|
|
118
|
+
obj_repr = f"File: {getattr(obj, 'name', '<unknown>')}"
|
|
119
|
+
elif isinstance(obj, list):
|
|
120
|
+
obj_repr = "List: []" if not obj else "List:"
|
|
121
|
+
elif isinstance(obj, dict):
|
|
122
|
+
obj_repr = "Dictionary: {}" if not obj else "Dictionary:"
|
|
123
|
+
else:
|
|
124
|
+
obj_repr = repr(obj)
|
|
125
|
+
|
|
126
|
+
text = ft.Row(
|
|
127
|
+
[
|
|
128
|
+
ft.Text(
|
|
129
|
+
obj_repr,
|
|
130
|
+
style=monospace(size),
|
|
131
|
+
text_align=ft.TextAlign.CENTER,
|
|
132
|
+
align=ft.Alignment.CENTER,
|
|
133
|
+
color=ft.Colors.BLACK if isinstance(obj, dict) else ft.Colors.WHITE,
|
|
134
|
+
)
|
|
135
|
+
],
|
|
136
|
+
scroll=ft.ScrollMode.AUTO,
|
|
137
|
+
spacing=0,
|
|
138
|
+
alignment=ft.MainAxisAlignment.CENTER,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if obj and isinstance(obj, list):
|
|
142
|
+
children = [Item(data=item) for item in obj]
|
|
143
|
+
self.content = ft.Column(
|
|
144
|
+
cast(
|
|
145
|
+
list[ft.Control],
|
|
146
|
+
[
|
|
147
|
+
text,
|
|
148
|
+
ft.Container(
|
|
149
|
+
ft.Column(cast(list[ft.Control], children), spacing=0),
|
|
150
|
+
padding=ft.Padding.symmetric(horizontal=4),
|
|
151
|
+
),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
elif obj and isinstance(obj, dict):
|
|
156
|
+
children = [
|
|
157
|
+
ft.Row([Item(data=key, expand=1), Item(data=value, expand=1)], spacing=0) for key, value in obj.items()
|
|
158
|
+
]
|
|
159
|
+
self.content = ft.Column(
|
|
160
|
+
cast(
|
|
161
|
+
list[ft.Control],
|
|
162
|
+
[
|
|
163
|
+
text,
|
|
164
|
+
ft.Container(
|
|
165
|
+
ft.Column(cast(list[ft.Control], children), spacing=0),
|
|
166
|
+
padding=ft.Padding.symmetric(horizontal=5),
|
|
167
|
+
),
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
self.content = text
|
|
173
|
+
|
|
174
|
+
def build(self) -> None:
|
|
175
|
+
self.set_animation()
|
|
176
|
+
|
|
177
|
+
def set_animation(self) -> None:
|
|
178
|
+
self.animate = ft.Animation(int((self.app.state.delay * 1000) // 2), ft.AnimationCurve.EASE_IN_OUT)
|
|
179
|
+
self.animate_size = ft.Animation(int((self.app.state.delay * 1000) // 2), ft.AnimationCurve.EASE_IN_OUT)
|
|
180
|
+
self.animate_offset = ft.Animation(int((self.app.state.delay * 1000) // 2), ft.AnimationCurve.EASE_IN_OUT)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@ft.control
|
|
184
|
+
class Instruction(ft.Container):
|
|
185
|
+
@property
|
|
186
|
+
def index(self) -> VMInstruction:
|
|
187
|
+
return self.data.index
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def instruction(self) -> VMInstruction:
|
|
191
|
+
return self.data.instruction
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def current(self) -> bool:
|
|
195
|
+
return self.data.current
|
|
196
|
+
|
|
197
|
+
@current.setter
|
|
198
|
+
def current(self, value: bool) -> None:
|
|
199
|
+
self.data.current = value
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def text(self) -> str:
|
|
203
|
+
return self.data.text
|
|
204
|
+
|
|
205
|
+
@text.setter
|
|
206
|
+
def text(self, value: str) -> None:
|
|
207
|
+
self.data.text = value
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def app(self) -> App:
|
|
211
|
+
try:
|
|
212
|
+
return self.page.data
|
|
213
|
+
except RuntimeError:
|
|
214
|
+
return ft.context.page.data
|
|
215
|
+
|
|
216
|
+
def init(self):
|
|
217
|
+
self.data = SimpleNamespace(index=self.data[0], instruction=self.data[1], current=False)
|
|
218
|
+
|
|
219
|
+
instr = self.instruction
|
|
220
|
+
color = ft.Colors.WHITE
|
|
221
|
+
|
|
222
|
+
size = 24
|
|
223
|
+
|
|
224
|
+
self.text = f"{self.index}: {instr}"
|
|
225
|
+
|
|
226
|
+
control: list[ft.Control] = [
|
|
227
|
+
ft.Text(
|
|
228
|
+
self.text,
|
|
229
|
+
style=monospace(size),
|
|
230
|
+
text_align=ft.TextAlign.CENTER,
|
|
231
|
+
align=ft.Alignment.CENTER,
|
|
232
|
+
)
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
self.content = ft.Row(
|
|
236
|
+
control,
|
|
237
|
+
scroll=ft.ScrollMode.AUTO,
|
|
238
|
+
spacing=0,
|
|
239
|
+
alignment=ft.MainAxisAlignment.CENTER,
|
|
240
|
+
)
|
|
241
|
+
self.bgcolor = color
|
|
242
|
+
self.border = ft.Border.all(2, ft.Colors.BLACK)
|
|
243
|
+
|
|
244
|
+
self.height = ITEM_HEIGHT
|
|
245
|
+
self.border_radius = ITEM_HEIGHT / 5
|
|
246
|
+
|
|
247
|
+
def before_update(self):
|
|
248
|
+
control = cast(ft.Text, cast(ft.Row, self.content).controls[0])
|
|
249
|
+
if self.current:
|
|
250
|
+
control.value = f"▶️ {self.text} ◀️"
|
|
251
|
+
else:
|
|
252
|
+
control.value = self.text
|
|
253
|
+
|
|
254
|
+
if self.current:
|
|
255
|
+
control.color = ft.Colors.WHITE
|
|
256
|
+
else:
|
|
257
|
+
control.color = ft.Colors.BLACK
|
|
258
|
+
|
|
259
|
+
self.animate = ft.Animation(int((self.app.state.delay * 1000) // 2), ft.AnimationCurve.EASE_IN_OUT)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@ft.control
|
|
263
|
+
class Stack(ft.Column):
|
|
264
|
+
@property
|
|
265
|
+
def items(self) -> list[StackItem]:
|
|
266
|
+
return self.data.items
|
|
267
|
+
|
|
268
|
+
@items.setter
|
|
269
|
+
def items(self, items: list[StackItem]) -> None:
|
|
270
|
+
self.data.last_items = self.items
|
|
271
|
+
self.data.items = list(items)
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def positions(self) -> dict[StackItem, float]:
|
|
275
|
+
return self.data.positions
|
|
276
|
+
|
|
277
|
+
@positions.setter
|
|
278
|
+
def positions(self, positions: dict[StackItem, float]) -> None:
|
|
279
|
+
self.data.positions = dict(positions.items())
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def last_items(self) -> list[StackItem]:
|
|
283
|
+
return self.data.last_items
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def item_controls(self) -> WeakKeyDictionary[StackItem, Item]:
|
|
287
|
+
return self.data.item_controls
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def app(self) -> App:
|
|
291
|
+
try:
|
|
292
|
+
return self.page.data
|
|
293
|
+
except RuntimeError:
|
|
294
|
+
return ft.context.page.data
|
|
295
|
+
|
|
296
|
+
def init(self):
|
|
297
|
+
self.expand = 1
|
|
298
|
+
self.scroll = ft.ScrollMode.AUTO
|
|
299
|
+
|
|
300
|
+
self.data = SimpleNamespace(items=[], last_items=[], positions={}, item_controls=WeakKeyDictionary())
|
|
301
|
+
|
|
302
|
+
def before_update(self):
|
|
303
|
+
old_items = list(reversed(self.last_items))
|
|
304
|
+
new_items = list(reversed(self.items))
|
|
305
|
+
|
|
306
|
+
old_positions = self.positions
|
|
307
|
+
|
|
308
|
+
for item in new_items:
|
|
309
|
+
if item not in self.item_controls:
|
|
310
|
+
self.item_controls[item] = Item(key=id(item), data=item.obj)
|
|
311
|
+
|
|
312
|
+
leaving = set(old_items) - set(new_items)
|
|
313
|
+
entering = set(new_items) - set(old_items)
|
|
314
|
+
|
|
315
|
+
old_controls = [
|
|
316
|
+
(item, self.item_controls[item]) for item in old_items + [i for i in new_items if i in entering]
|
|
317
|
+
]
|
|
318
|
+
new_controls: list[tuple[StackItem, Item]] = [(item, self.item_controls[item]) for item in new_items]
|
|
319
|
+
|
|
320
|
+
temporary_positions = dict(
|
|
321
|
+
zip(
|
|
322
|
+
(item for item, _ in old_controls),
|
|
323
|
+
it.accumulate(
|
|
324
|
+
old_controls,
|
|
325
|
+
lambda prev, entry: prev + item_height(entry[0].obj) + self.spacing,
|
|
326
|
+
initial=0,
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
new_positions = dict(
|
|
331
|
+
zip(
|
|
332
|
+
new_items,
|
|
333
|
+
it.accumulate(
|
|
334
|
+
new_items,
|
|
335
|
+
lambda prev, item: prev + item_height(item.obj) + self.spacing,
|
|
336
|
+
initial=0,
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if old_positions == new_positions:
|
|
342
|
+
# currently impossible for a mutable object to be mutated without affecting the stack
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
self.positions = new_positions
|
|
346
|
+
|
|
347
|
+
positions: list[tuple[Item, tuple[tuple[float, float], tuple[float, float]]]] = []
|
|
348
|
+
for item, control in old_controls:
|
|
349
|
+
if item in leaving:
|
|
350
|
+
end = (-2, 0)
|
|
351
|
+
else:
|
|
352
|
+
height = item_height(item.obj)
|
|
353
|
+
diff = new_positions[item] - temporary_positions[item]
|
|
354
|
+
end = (0, diff / height)
|
|
355
|
+
|
|
356
|
+
start = (-2, end[1]) if item in entering else (0, 0)
|
|
357
|
+
|
|
358
|
+
positions.append((control, (start, end)))
|
|
359
|
+
|
|
360
|
+
async def animate():
|
|
361
|
+
await asyncio.sleep(1 / 30)
|
|
362
|
+
for item, control in new_controls:
|
|
363
|
+
control.height = item_height(item.obj)
|
|
364
|
+
control.update()
|
|
365
|
+
for control, (_, end) in positions:
|
|
366
|
+
control.offset = ft.Offset(*end)
|
|
367
|
+
control.update()
|
|
368
|
+
await asyncio.sleep(self.app.state.delay / 2)
|
|
369
|
+
self.controls = [entry[1] for entry in new_controls]
|
|
370
|
+
for control in cast(list[Item], self.controls):
|
|
371
|
+
control.offset = ft.Offset(0, 0)
|
|
372
|
+
control.animate_offset = None
|
|
373
|
+
self.data.last_items = list(self.items)
|
|
374
|
+
self.update()
|
|
375
|
+
|
|
376
|
+
self.controls = [entry[1] for entry in old_controls]
|
|
377
|
+
|
|
378
|
+
for control, (start, _) in positions:
|
|
379
|
+
control.offset = ft.Offset(*start)
|
|
380
|
+
control.set_animation()
|
|
381
|
+
|
|
382
|
+
self.app.create_task(animate)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@ft.control
|
|
386
|
+
class Instructions(ft.Column):
|
|
387
|
+
@property
|
|
388
|
+
def instructions(self) -> tuple[VMInstruction, ...]:
|
|
389
|
+
return self.data.instructions
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def current(self) -> int:
|
|
393
|
+
return self.data.current
|
|
394
|
+
|
|
395
|
+
@current.setter
|
|
396
|
+
def current(self, value: int) -> None:
|
|
397
|
+
self.data.prev = self.current
|
|
398
|
+
self.data.current = value
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def next(self) -> int:
|
|
402
|
+
return self.data.next
|
|
403
|
+
|
|
404
|
+
@next.setter
|
|
405
|
+
def next(self, value: int) -> None:
|
|
406
|
+
self.data.next = value
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def suspended(self) -> list[int]:
|
|
410
|
+
return self.data.suspended
|
|
411
|
+
|
|
412
|
+
@suspended.setter
|
|
413
|
+
def suspended(self, value: list[int]) -> None:
|
|
414
|
+
self.data.suspended = value
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def viewport_height(self) -> float:
|
|
418
|
+
return self.data.viewport_height
|
|
419
|
+
|
|
420
|
+
@viewport_height.setter
|
|
421
|
+
def viewport_height(self, value: float) -> None:
|
|
422
|
+
self.data.viewport_height = value
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def prev(self) -> int:
|
|
426
|
+
return self.data.prev
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def app(self) -> App:
|
|
430
|
+
try:
|
|
431
|
+
return self.page.data
|
|
432
|
+
except RuntimeError:
|
|
433
|
+
return ft.context.page.data
|
|
434
|
+
|
|
435
|
+
def init(self):
|
|
436
|
+
self.expand = 1
|
|
437
|
+
self.scroll = ft.ScrollMode.AUTO
|
|
438
|
+
|
|
439
|
+
self.data.current = 0
|
|
440
|
+
self.data.next = 0
|
|
441
|
+
self.data.prev = 0
|
|
442
|
+
self.data.suspended = []
|
|
443
|
+
self.data.viewport_height = 0
|
|
444
|
+
|
|
445
|
+
self.controls = [
|
|
446
|
+
Instruction(data=(index, instr), key=ft.ScrollKey(f"instr_{index}"))
|
|
447
|
+
for index, instr in enumerate(self.instructions)
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
self.offsets = list(
|
|
451
|
+
it.accumulate(
|
|
452
|
+
self.controls,
|
|
453
|
+
lambda prev, control: prev + cast(float, cast(Instruction, control).height) + self.spacing,
|
|
454
|
+
initial=0,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def on_size_change(event: ft.LayoutSizeChangeEvent) -> None:
|
|
459
|
+
self.viewport_height = event.height
|
|
460
|
+
|
|
461
|
+
self.on_size_change = on_size_change
|
|
462
|
+
|
|
463
|
+
def before_update(self) -> None:
|
|
464
|
+
if 0 <= self.prev < len(self.controls):
|
|
465
|
+
prev = self.prev
|
|
466
|
+
control = cast(Instruction, self.controls[prev])
|
|
467
|
+
control.current = False
|
|
468
|
+
control.bgcolor = ft.Colors.WHITE
|
|
469
|
+
|
|
470
|
+
if 0 <= self.next <= len(self.controls):
|
|
471
|
+
control = cast(Instruction, self.controls[self.next])
|
|
472
|
+
control.current = False
|
|
473
|
+
control.bgcolor = ft.Colors.GREEN_200
|
|
474
|
+
|
|
475
|
+
for suspended in self.suspended:
|
|
476
|
+
if 0 <= suspended <= len(self.controls):
|
|
477
|
+
control = cast(Instruction, self.controls[suspended])
|
|
478
|
+
control.current = False
|
|
479
|
+
control.bgcolor = ft.Colors.GREY_400
|
|
480
|
+
|
|
481
|
+
if 0 <= self.current < len(self.controls):
|
|
482
|
+
current = self.current
|
|
483
|
+
control = cast(Instruction, self.controls[current])
|
|
484
|
+
control.current = True
|
|
485
|
+
control.bgcolor = ft.Colors.GREEN_800
|
|
486
|
+
|
|
487
|
+
async def scroll(current: int = current):
|
|
488
|
+
height = self.viewport_height
|
|
489
|
+
await self.scroll_to(
|
|
490
|
+
max(self.offsets[current] - height / 2 + (0 if control.height is None else control.height / 2), 0),
|
|
491
|
+
duration=int(self.app.state.delay * 1000 // 2),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
self.app.create_task(scroll)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@ft.control
|
|
498
|
+
class Frame(ft.Container):
|
|
499
|
+
@property
|
|
500
|
+
def frame(self) -> VMFrame:
|
|
501
|
+
return self.data.frame
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def active(self) -> bool:
|
|
505
|
+
return self.data.active
|
|
506
|
+
|
|
507
|
+
@active.setter
|
|
508
|
+
def active(self, value: bool) -> None:
|
|
509
|
+
self.data.active = value
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def app(self) -> App:
|
|
513
|
+
try:
|
|
514
|
+
return self.page.data
|
|
515
|
+
except RuntimeError:
|
|
516
|
+
return ft.context.page.data
|
|
517
|
+
|
|
518
|
+
def init(self):
|
|
519
|
+
self.border = ft.Border.all(2, ft.Colors.BLACK)
|
|
520
|
+
|
|
521
|
+
self.border_radius = ITEM_HEIGHT / 5
|
|
522
|
+
|
|
523
|
+
self.expand = True
|
|
524
|
+
|
|
525
|
+
self.data = SimpleNamespace(frame=self.data[0], active=self.data[1])
|
|
526
|
+
|
|
527
|
+
self.set_animation()
|
|
528
|
+
|
|
529
|
+
def before_update(self):
|
|
530
|
+
self.set_animation()
|
|
531
|
+
|
|
532
|
+
color = ft.Colors.GREEN_200 if self.active else ft.Colors.GREY_200
|
|
533
|
+
grid_color = ft.Colors.GREEN_400 if self.active else ft.Colors.GREY_400
|
|
534
|
+
|
|
535
|
+
self.bgcolor = color
|
|
536
|
+
|
|
537
|
+
frame = self.frame
|
|
538
|
+
|
|
539
|
+
size = 24
|
|
540
|
+
|
|
541
|
+
text = f"Current: {frame.prev}\nNext: {frame.current}"
|
|
542
|
+
|
|
543
|
+
content = ft.Column(
|
|
544
|
+
[
|
|
545
|
+
ft.Text(
|
|
546
|
+
text,
|
|
547
|
+
style=monospace(size),
|
|
548
|
+
expand=True,
|
|
549
|
+
text_align=ft.TextAlign.CENTER,
|
|
550
|
+
)
|
|
551
|
+
],
|
|
552
|
+
margin=ft.Margin.all(5),
|
|
553
|
+
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if frame.env:
|
|
557
|
+
grid = ft.Container(
|
|
558
|
+
ft.DataTable(
|
|
559
|
+
columns=[
|
|
560
|
+
ft.DataColumn(label=ft.Text("Variable", style=monospace(size))),
|
|
561
|
+
ft.DataColumn(label=ft.Text("Value", style=monospace(size))),
|
|
562
|
+
],
|
|
563
|
+
rows=[
|
|
564
|
+
ft.DataRow(
|
|
565
|
+
cells=[
|
|
566
|
+
ft.DataCell(
|
|
567
|
+
ft.Text(
|
|
568
|
+
spans=[ft.TextSpan(str(key))]
|
|
569
|
+
+ (
|
|
570
|
+
[ft.TextSpan(" (global)", style=monospace(size // 2))]
|
|
571
|
+
if key in frame.env.maps[1] and key not in frame.env.maps[0]
|
|
572
|
+
else []
|
|
573
|
+
),
|
|
574
|
+
style=monospace(size),
|
|
575
|
+
)
|
|
576
|
+
),
|
|
577
|
+
ft.DataCell(
|
|
578
|
+
ft.Row(
|
|
579
|
+
[
|
|
580
|
+
ft.Text(
|
|
581
|
+
repr(value),
|
|
582
|
+
style=monospace(size),
|
|
583
|
+
no_wrap=True,
|
|
584
|
+
)
|
|
585
|
+
],
|
|
586
|
+
scroll=ft.ScrollMode.AUTO,
|
|
587
|
+
),
|
|
588
|
+
),
|
|
589
|
+
]
|
|
590
|
+
)
|
|
591
|
+
for key, value in sorted(frame.env.items())
|
|
592
|
+
],
|
|
593
|
+
bgcolor=grid_color,
|
|
594
|
+
border=ft.Border.all(2, ft.Colors.BLACK),
|
|
595
|
+
border_radius=ITEM_HEIGHT / 5,
|
|
596
|
+
horizontal_lines=ft.BorderSide(1, ft.Colors.BLACK),
|
|
597
|
+
vertical_lines=ft.BorderSide(1, ft.Colors.BLACK),
|
|
598
|
+
horizontal_margin=4,
|
|
599
|
+
column_spacing=8,
|
|
600
|
+
),
|
|
601
|
+
theme=ft.Theme(divider_theme=ft.DividerTheme(color=ft.Colors.BLACK, thickness=1)),
|
|
602
|
+
)
|
|
603
|
+
content.controls.append(grid)
|
|
604
|
+
|
|
605
|
+
self.content = content
|
|
606
|
+
|
|
607
|
+
def set_animation(self) -> None:
|
|
608
|
+
pass
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@ft.control
|
|
612
|
+
class CallStack(ft.Column):
|
|
613
|
+
@property
|
|
614
|
+
def executor(self) -> Executor:
|
|
615
|
+
return self.data.executor
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def frames(self) -> list[VMFrame]:
|
|
619
|
+
return self.executor.state.frames
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def frame_controls(self) -> dict[VMFrame, Frame]:
|
|
623
|
+
return self.data.frame_controls
|
|
624
|
+
|
|
625
|
+
@frame_controls.setter
|
|
626
|
+
def frame_controls(self, value: dict[VMFrame, Frame]) -> None:
|
|
627
|
+
self.data.frame_controls = value
|
|
628
|
+
|
|
629
|
+
def init(self):
|
|
630
|
+
self.expand = 2
|
|
631
|
+
self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH
|
|
632
|
+
self.scroll = ft.ScrollMode.AUTO
|
|
633
|
+
|
|
634
|
+
self.frame_controls = {}
|
|
635
|
+
|
|
636
|
+
def before_update(self):
|
|
637
|
+
controls: list[ft.Control] = []
|
|
638
|
+
|
|
639
|
+
frames = list(reversed(self.frames))
|
|
640
|
+
|
|
641
|
+
if not frames:
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
if frames[0] in self.frame_controls:
|
|
645
|
+
control = self.frame_controls[frames[0]]
|
|
646
|
+
control.active = True
|
|
647
|
+
controls.append(control)
|
|
648
|
+
else:
|
|
649
|
+
control = Frame(data=(frames[0], True))
|
|
650
|
+
self.frame_controls[frames[0]] = control
|
|
651
|
+
controls.append(control)
|
|
652
|
+
|
|
653
|
+
for frame in frames[1:]:
|
|
654
|
+
control = self.frame_controls[frame]
|
|
655
|
+
control.active = False
|
|
656
|
+
controls.append(control)
|
|
657
|
+
|
|
658
|
+
for frame in self.frame_controls.keys() - set(frames):
|
|
659
|
+
del self.frame_controls[frame]
|
|
660
|
+
|
|
661
|
+
self.controls = controls
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@ft.control
|
|
665
|
+
class Console(ft.Container):
|
|
666
|
+
@property
|
|
667
|
+
def stdout(self) -> StringIO:
|
|
668
|
+
return self.data
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def text(self) -> ft.Text:
|
|
672
|
+
return cast(ft.Text, cast(ft.Column, self.content).controls[0])
|
|
673
|
+
|
|
674
|
+
def init(self) -> None:
|
|
675
|
+
self.bgcolor = ft.Colors.GREEN_900
|
|
676
|
+
|
|
677
|
+
self.border = ft.Border.all(2, ft.Colors.BLACK)
|
|
678
|
+
self.border_radius = ITEM_HEIGHT // 5
|
|
679
|
+
|
|
680
|
+
self.expand = 1
|
|
681
|
+
|
|
682
|
+
self.content = ft.Column(
|
|
683
|
+
[
|
|
684
|
+
ft.Text(
|
|
685
|
+
style=monospace(16),
|
|
686
|
+
color=ft.Colors.WHITE,
|
|
687
|
+
margin=ft.Margin.all(5),
|
|
688
|
+
selectable=True,
|
|
689
|
+
)
|
|
690
|
+
],
|
|
691
|
+
scroll=ft.ScrollMode.AUTO,
|
|
692
|
+
auto_scroll=True,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def before_update(self) -> None:
|
|
696
|
+
self.text.value = self.stdout.getvalue()
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def ErrorDialog(page: ft.Page, exception: BaseException) -> ft.AlertDialog:
|
|
700
|
+
def close() -> None:
|
|
701
|
+
page.pop_dialog()
|
|
702
|
+
|
|
703
|
+
return ft.AlertDialog(
|
|
704
|
+
modal=True,
|
|
705
|
+
title=ft.Text("Exception raised during execution"),
|
|
706
|
+
content=ft.Text(f"{type(exception).__name__}: {exception}", style=monospace(14), selectable=True),
|
|
707
|
+
actions=[ft.TextButton("OK", on_click=close)],
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def ReturnDialog(page: ft.Page, obj: object) -> ft.AlertDialog:
|
|
712
|
+
def close() -> None:
|
|
713
|
+
page.pop_dialog()
|
|
714
|
+
|
|
715
|
+
return ft.AlertDialog(
|
|
716
|
+
modal=True,
|
|
717
|
+
title=ft.Text("Program finished successfully"),
|
|
718
|
+
content=ft.Text(
|
|
719
|
+
spans=[ft.TextSpan("Returned: "), ft.TextSpan(repr(obj), style=monospace(14))], selectable=True
|
|
720
|
+
),
|
|
721
|
+
actions=[ft.TextButton("OK", on_click=close)],
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
async def input_dialog(page: ft.Page, stdout: StringIO) -> str:
|
|
726
|
+
future: asyncio.Future[str] = asyncio.Future()
|
|
727
|
+
|
|
728
|
+
def submit(event: ft.Event[Any]):
|
|
729
|
+
value = text_input.value or ""
|
|
730
|
+
print(value, file=stdout)
|
|
731
|
+
page.pop_dialog()
|
|
732
|
+
if not future.done():
|
|
733
|
+
future.set_result(value)
|
|
734
|
+
|
|
735
|
+
text_input = ft.TextField(autofocus=True, on_submit=submit)
|
|
736
|
+
|
|
737
|
+
dialog = ft.AlertDialog(
|
|
738
|
+
modal=True,
|
|
739
|
+
title=ft.Text("Input requested"),
|
|
740
|
+
content=text_input,
|
|
741
|
+
actions=cast(list[ft.Control], [ft.TextButton("Submit", on_click=submit)]),
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
page.show_dialog(dialog)
|
|
745
|
+
|
|
746
|
+
return await future
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def get_input_from_dialog(page: ft.Page, stdout: StringIO) -> str:
|
|
750
|
+
result = asyncio.run_coroutine_threadsafe(input_dialog(page, stdout), page.loop)
|
|
751
|
+
return result.result()
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@ft.control
|
|
755
|
+
class SpeedSlider(ft.Slider):
|
|
756
|
+
delays: ClassVar[tuple[float, ...]] = (1.25, 1.0, 0.75, 0.5, 0.25)
|
|
757
|
+
|
|
758
|
+
@property
|
|
759
|
+
def app(self) -> App:
|
|
760
|
+
try:
|
|
761
|
+
return self.page.data
|
|
762
|
+
except RuntimeError:
|
|
763
|
+
return ft.context.page.data
|
|
764
|
+
|
|
765
|
+
def init(self):
|
|
766
|
+
self.value = 2
|
|
767
|
+
self.min = 0
|
|
768
|
+
self.max = 4
|
|
769
|
+
self.divisions = 4
|
|
770
|
+
|
|
771
|
+
self.on_change = self.handle_change
|
|
772
|
+
|
|
773
|
+
self.expand = True
|
|
774
|
+
|
|
775
|
+
self.set_label()
|
|
776
|
+
|
|
777
|
+
def set_label(self) -> None:
|
|
778
|
+
value = 2 if self.value is None else int(self.value)
|
|
779
|
+
self.label = ["Slowest", "Slow", "Medium", "Fast", "Fastest"][value]
|
|
780
|
+
|
|
781
|
+
def handle_change(self, event: ft.Event[ft.Slider]):
|
|
782
|
+
value = 2 if self.value is None else int(self.value)
|
|
783
|
+
self.set_label()
|
|
784
|
+
|
|
785
|
+
self.app.state.delay = self.delays[value]
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@ft.control
|
|
789
|
+
class PauseButton(ft.IconButton):
|
|
790
|
+
@property
|
|
791
|
+
def pause(self) -> asyncio.Event:
|
|
792
|
+
return self.data
|
|
793
|
+
|
|
794
|
+
def init(self) -> None:
|
|
795
|
+
self.icon = ft.Icons.PAUSE
|
|
796
|
+
|
|
797
|
+
def on_click(event: ft.Event[ft.IconButton]):
|
|
798
|
+
if self.pause.is_set():
|
|
799
|
+
self.pause.clear()
|
|
800
|
+
self.icon = ft.Icons.PLAY_ARROW
|
|
801
|
+
else:
|
|
802
|
+
self.pause.set()
|
|
803
|
+
self.icon = ft.Icons.PAUSE
|
|
804
|
+
|
|
805
|
+
self.on_click = on_click
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@ft.control
|
|
809
|
+
class StepButton(ft.IconButton):
|
|
810
|
+
@property
|
|
811
|
+
def pause(self) -> asyncio.Event:
|
|
812
|
+
return self.data
|
|
813
|
+
|
|
814
|
+
def init(self) -> None:
|
|
815
|
+
self.icon = ft.Icons.SKIP_NEXT
|
|
816
|
+
|
|
817
|
+
async def on_click(event: ft.Event[ft.IconButton]):
|
|
818
|
+
if not self.pause.is_set():
|
|
819
|
+
self.pause.set()
|
|
820
|
+
await asyncio.sleep(0)
|
|
821
|
+
self.pause.clear()
|
|
822
|
+
|
|
823
|
+
self.on_click = on_click
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@dataclass
|
|
827
|
+
class AppState:
|
|
828
|
+
delay: float = 0.75
|
|
829
|
+
paused: bool = False
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class App:
|
|
833
|
+
page: ft.Page
|
|
834
|
+
machine: StackMachine
|
|
835
|
+
executor: Executor
|
|
836
|
+
|
|
837
|
+
speed_control: ft.Row
|
|
838
|
+
stack: Stack
|
|
839
|
+
instructions: Instructions
|
|
840
|
+
call_stack: CallStack
|
|
841
|
+
console: Console
|
|
842
|
+
|
|
843
|
+
stdout: StringIO
|
|
844
|
+
|
|
845
|
+
pause: asyncio.Event
|
|
846
|
+
send: asyncio.Queue[float]
|
|
847
|
+
receive: asyncio.Queue[tuple[bool, Any]]
|
|
848
|
+
|
|
849
|
+
state: AppState
|
|
850
|
+
|
|
851
|
+
tasks: set[Future]
|
|
852
|
+
|
|
853
|
+
result: Future[Any]
|
|
854
|
+
|
|
855
|
+
closing: bool
|
|
856
|
+
|
|
857
|
+
def __init__(
|
|
858
|
+
self,
|
|
859
|
+
machine: StackMachine,
|
|
860
|
+
page: ft.Page,
|
|
861
|
+
*,
|
|
862
|
+
args: tuple[Any, ...],
|
|
863
|
+
seed: int | str | Random | None = None,
|
|
864
|
+
result: Future[Any],
|
|
865
|
+
) -> None:
|
|
866
|
+
self.tasks = set()
|
|
867
|
+
|
|
868
|
+
self.state = AppState()
|
|
869
|
+
|
|
870
|
+
self.page = page
|
|
871
|
+
page.data = self
|
|
872
|
+
|
|
873
|
+
page.title = "StackVM"
|
|
874
|
+
|
|
875
|
+
if platform.system() == "Windows":
|
|
876
|
+
icon = resources.files("stackvm").joinpath("resources", "icon.ico")
|
|
877
|
+
|
|
878
|
+
with resources.as_file(icon) as path:
|
|
879
|
+
page.window.icon = str(path)
|
|
880
|
+
|
|
881
|
+
self.machine = machine
|
|
882
|
+
|
|
883
|
+
self.stdout = StringIO()
|
|
884
|
+
self.executor = machine.ui_executor(
|
|
885
|
+
*args, seed=seed, stdin=partial(get_input_from_dialog, page, self.stdout), stdout=self.stdout
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
self.pause = asyncio.Event()
|
|
889
|
+
self.pause.set()
|
|
890
|
+
|
|
891
|
+
self.send = asyncio.Queue()
|
|
892
|
+
self.receive = asyncio.Queue()
|
|
893
|
+
|
|
894
|
+
self.speed_control = ft.Row(
|
|
895
|
+
[
|
|
896
|
+
ft.Text("Speed:", size=18),
|
|
897
|
+
SpeedSlider(0),
|
|
898
|
+
PauseButton(data=self.pause),
|
|
899
|
+
StepButton(data=self.pause),
|
|
900
|
+
ft.IconButton(ft.Icons.STOP, icon_color=ft.Colors.RED, on_click=page.window.close),
|
|
901
|
+
],
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
self.stack = Stack()
|
|
905
|
+
self.instructions = Instructions(data=SimpleNamespace(instructions=self.executor.instructions))
|
|
906
|
+
self.call_stack = CallStack(data=SimpleNamespace(executor=self.executor))
|
|
907
|
+
self.console = Console(data=self.stdout)
|
|
908
|
+
|
|
909
|
+
page.theme = ft.Theme(color_scheme=ft.ColorScheme(primary=ft.Colors.GREEN))
|
|
910
|
+
|
|
911
|
+
self.closing = False
|
|
912
|
+
|
|
913
|
+
self.result = result
|
|
914
|
+
|
|
915
|
+
name = metadata.metadata("stackvm").get("Name", "???")
|
|
916
|
+
version = metadata.version("stackvm")
|
|
917
|
+
author = metadata.metadata("stackvm").get("Author", "???")
|
|
918
|
+
self.page.add(
|
|
919
|
+
ft.Column(
|
|
920
|
+
[
|
|
921
|
+
ft.Row(
|
|
922
|
+
[
|
|
923
|
+
ft.Column(
|
|
924
|
+
[ft.Text("Operand Stack", style=monospace(36))],
|
|
925
|
+
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
|
926
|
+
expand=1,
|
|
927
|
+
),
|
|
928
|
+
ft.Column(
|
|
929
|
+
[ft.Text("Instructions", style=monospace(36))],
|
|
930
|
+
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
|
931
|
+
expand=1,
|
|
932
|
+
),
|
|
933
|
+
ft.Column(
|
|
934
|
+
[ft.Text("Console", style=monospace(36))],
|
|
935
|
+
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
|
936
|
+
expand=1,
|
|
937
|
+
),
|
|
938
|
+
]
|
|
939
|
+
),
|
|
940
|
+
ft.Row(
|
|
941
|
+
[
|
|
942
|
+
self.stack,
|
|
943
|
+
self.instructions,
|
|
944
|
+
ft.Column(
|
|
945
|
+
[
|
|
946
|
+
self.console,
|
|
947
|
+
ft.Text("Call Stack", style=monospace(36), text_align=ft.TextAlign.CENTER),
|
|
948
|
+
self.call_stack,
|
|
949
|
+
],
|
|
950
|
+
expand=1,
|
|
951
|
+
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
|
952
|
+
),
|
|
953
|
+
],
|
|
954
|
+
expand=True,
|
|
955
|
+
vertical_alignment=ft.CrossAxisAlignment.START,
|
|
956
|
+
),
|
|
957
|
+
self.speed_control,
|
|
958
|
+
ft.Row(
|
|
959
|
+
[ft.Text(f"{name} v{version} by {author}", style=monospace(12), color=ft.Colors.GREY_500)],
|
|
960
|
+
alignment=ft.MainAxisAlignment.END,
|
|
961
|
+
),
|
|
962
|
+
],
|
|
963
|
+
expand=True,
|
|
964
|
+
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
self.create_task(loop, page, self.executor, self.pause, self.receive, self.send)
|
|
969
|
+
self.create_task(self.updateloop)
|
|
970
|
+
|
|
971
|
+
async def on_window_event(event: ft.WindowEvent) -> None:
|
|
972
|
+
if event.type == ft.WindowEventType.CLOSE:
|
|
973
|
+
if self.closing:
|
|
974
|
+
return
|
|
975
|
+
self.closing = True
|
|
976
|
+
|
|
977
|
+
for task in list(self.tasks):
|
|
978
|
+
task.cancel()
|
|
979
|
+
|
|
980
|
+
await asyncio.gather(*map(asyncio.wrap_future, self.tasks), return_exceptions=True)
|
|
981
|
+
|
|
982
|
+
await page.window.destroy()
|
|
983
|
+
|
|
984
|
+
page.window.prevent_close = True
|
|
985
|
+
page.window.on_event = on_window_event
|
|
986
|
+
|
|
987
|
+
async def updateloop(self) -> None:
|
|
988
|
+
executor = self.executor
|
|
989
|
+
while True:
|
|
990
|
+
returned, value = await self.receive.get()
|
|
991
|
+
|
|
992
|
+
if returned:
|
|
993
|
+
current, value = value
|
|
994
|
+
else:
|
|
995
|
+
current = value
|
|
996
|
+
|
|
997
|
+
if executor.state.frames:
|
|
998
|
+
self.stack.items = executor.state.frames[-1].stack
|
|
999
|
+
self.stack.update()
|
|
1000
|
+
|
|
1001
|
+
self.instructions.current = current
|
|
1002
|
+
if executor.state.frames:
|
|
1003
|
+
self.instructions.next = executor.state.frames[-1].current
|
|
1004
|
+
self.instructions.suspended = [frame.current for frame in executor.state.frames[-2::-1]]
|
|
1005
|
+
self.instructions.update()
|
|
1006
|
+
|
|
1007
|
+
self.call_stack.update()
|
|
1008
|
+
self.console.update()
|
|
1009
|
+
|
|
1010
|
+
if returned:
|
|
1011
|
+
if isinstance(value, BaseException):
|
|
1012
|
+
self.page.show_dialog(ErrorDialog(self.page, value))
|
|
1013
|
+
else:
|
|
1014
|
+
self.page.show_dialog(ReturnDialog(self.page, value))
|
|
1015
|
+
self.result.set_result(value)
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
await self.send.put(self.state.delay)
|
|
1019
|
+
|
|
1020
|
+
def create_task[**P, T](
|
|
1021
|
+
self, coroutine: Callable[P, CoroutineType[Any, Any, T]], /, *args: P.args, **kwargs: P.kwargs
|
|
1022
|
+
) -> Future[T] | None:
|
|
1023
|
+
if self.closing:
|
|
1024
|
+
return None
|
|
1025
|
+
|
|
1026
|
+
future = self.page.run_task(coroutine, *args, **kwargs)
|
|
1027
|
+
self.tasks.add(future)
|
|
1028
|
+
future.add_done_callback(self.tasks.discard)
|
|
1029
|
+
|
|
1030
|
+
return future
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def execute(machine: StackMachine, *args: None | int | bool | str, seed: int | str | Random | None = None):
|
|
1034
|
+
result: Future[Any] = Future()
|
|
1035
|
+
ft.run(partial(App, machine, args=args, seed=seed, result=result))
|
|
1036
|
+
return result.result()
|