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/_api.py
ADDED
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import itertools as it
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Callable, Generator
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from random import Random
|
|
12
|
+
from types import NoneType
|
|
13
|
+
from typing import Any, BinaryIO, Literal, Self, TextIO, assert_never, cast
|
|
14
|
+
|
|
15
|
+
from stackvm._util import (
|
|
16
|
+
FILE_MODES,
|
|
17
|
+
TYPES,
|
|
18
|
+
InstructedError,
|
|
19
|
+
Instruction,
|
|
20
|
+
OpCode,
|
|
21
|
+
PopPeek,
|
|
22
|
+
ResolvedInstruction,
|
|
23
|
+
StackItem,
|
|
24
|
+
StackUnderflowError,
|
|
25
|
+
VMFrame,
|
|
26
|
+
VMState,
|
|
27
|
+
unzigzag,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if sys.version_info < (3, 15):
|
|
31
|
+
from stackvm._util import sentinel
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def process_instruction(
|
|
35
|
+
state: VMState,
|
|
36
|
+
instructions: tuple[ResolvedInstruction, ...],
|
|
37
|
+
push: Callable[[list[Any], Any], None],
|
|
38
|
+
pop: PopPeek,
|
|
39
|
+
peek: PopPeek,
|
|
40
|
+
rng: Random,
|
|
41
|
+
stdin: Callable[[], str],
|
|
42
|
+
stdout: TextIO,
|
|
43
|
+
) -> tuple[bool, Any]:
|
|
44
|
+
frame = state.frames[-1]
|
|
45
|
+
current, stack, env, prev = frame.current, frame.stack, frame.env, frame.prev
|
|
46
|
+
|
|
47
|
+
if current >= len(instructions):
|
|
48
|
+
return True, None
|
|
49
|
+
|
|
50
|
+
value: Any
|
|
51
|
+
returned, value, to = False, None, current + 1
|
|
52
|
+
|
|
53
|
+
instr = instructions[current]
|
|
54
|
+
opcode, arg, arg2 = instr.opcode, instr.arg, instr.arg2
|
|
55
|
+
try:
|
|
56
|
+
match opcode:
|
|
57
|
+
case OpCode.NOOP:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
case OpCode.NONE:
|
|
61
|
+
push(stack, None)
|
|
62
|
+
case OpCode.INT:
|
|
63
|
+
push(stack, arg)
|
|
64
|
+
case OpCode.CHR:
|
|
65
|
+
if arg == -1:
|
|
66
|
+
push(stack, "")
|
|
67
|
+
else:
|
|
68
|
+
push(stack, chr(arg))
|
|
69
|
+
case OpCode.BOOL:
|
|
70
|
+
push(stack, bool(arg))
|
|
71
|
+
|
|
72
|
+
case OpCode.TO_INT:
|
|
73
|
+
push(stack, int(pop(stack)))
|
|
74
|
+
case OpCode.TO_FLOAT:
|
|
75
|
+
push(stack, float(pop(stack)))
|
|
76
|
+
case OpCode.TO_CHR:
|
|
77
|
+
push(stack, chr(pop(stack)))
|
|
78
|
+
case OpCode.TO_STR:
|
|
79
|
+
push(stack, str(pop(stack)))
|
|
80
|
+
case OpCode.TO_BOOL:
|
|
81
|
+
push(stack, bool(pop(stack)))
|
|
82
|
+
case OpCode.TO_BYTES:
|
|
83
|
+
push(stack, bytes(pop(stack)))
|
|
84
|
+
|
|
85
|
+
case OpCode.ORD:
|
|
86
|
+
push(stack, ord(pop(stack)))
|
|
87
|
+
case OpCode.SLICE:
|
|
88
|
+
step, stop, start = (pop(stack), pop(stack), pop(stack))
|
|
89
|
+
push(stack, slice(start, stop, step))
|
|
90
|
+
case OpCode.TYPE:
|
|
91
|
+
obj = pop(stack)
|
|
92
|
+
for index, cls in enumerate(TYPES):
|
|
93
|
+
if isinstance(obj, cls):
|
|
94
|
+
push(stack, index)
|
|
95
|
+
break
|
|
96
|
+
else:
|
|
97
|
+
raise RuntimeError(f"Unrecognized type {type(obj).__name__}")
|
|
98
|
+
|
|
99
|
+
case OpCode.LOAD:
|
|
100
|
+
try:
|
|
101
|
+
push(stack, env[arg])
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise UnboundLocalError(f"Variable {arg} is unbound") from None
|
|
104
|
+
case OpCode.STORE:
|
|
105
|
+
frame.store(arg, pop(stack))
|
|
106
|
+
case OpCode.GLOBAL:
|
|
107
|
+
if arg in env.maps[0]:
|
|
108
|
+
# perhaps odd, but this is what Python raises when you use a global before the global keyword
|
|
109
|
+
raise SyntaxError(f"Variable {arg} was already assigned as a local variable when declared global")
|
|
110
|
+
frame.global_vars.add(arg)
|
|
111
|
+
|
|
112
|
+
case OpCode.COPY:
|
|
113
|
+
item = peek(stack, -(arg + 1) if arg < 0 else -arg)
|
|
114
|
+
push(stack, item)
|
|
115
|
+
case OpCode.SWAP:
|
|
116
|
+
loc = -(arg + 1) if arg < 0 else -arg
|
|
117
|
+
try:
|
|
118
|
+
stack[loc], stack[-1] = stack[-1], stack[loc]
|
|
119
|
+
except IndexError:
|
|
120
|
+
raise StackUnderflowError
|
|
121
|
+
case OpCode.ROLL:
|
|
122
|
+
try:
|
|
123
|
+
item = stack.pop(-(arg + 1) if arg < 0 else -arg)
|
|
124
|
+
except IndexError:
|
|
125
|
+
raise StackUnderflowError
|
|
126
|
+
stack.append(item)
|
|
127
|
+
case OpCode.POP:
|
|
128
|
+
pop(stack)
|
|
129
|
+
|
|
130
|
+
case OpCode.ADD:
|
|
131
|
+
addend, augend = pop(stack), pop(stack)
|
|
132
|
+
push(stack, augend + addend)
|
|
133
|
+
case OpCode.SUB:
|
|
134
|
+
subtrahend, minuend = pop(stack), pop(stack)
|
|
135
|
+
push(stack, minuend - subtrahend)
|
|
136
|
+
case OpCode.MUL:
|
|
137
|
+
multiplier, multiplicand = pop(stack), pop(stack)
|
|
138
|
+
push(stack, multiplicand * multiplier)
|
|
139
|
+
case OpCode.DIV:
|
|
140
|
+
divisor, dividend = pop(stack), pop(stack)
|
|
141
|
+
push(stack, dividend / divisor)
|
|
142
|
+
case OpCode.FLOORDIV:
|
|
143
|
+
divisor, dividend = pop(stack), pop(stack)
|
|
144
|
+
push(stack, dividend // divisor)
|
|
145
|
+
case OpCode.MOD:
|
|
146
|
+
divisor, dividend = pop(stack), pop(stack)
|
|
147
|
+
if isinstance(dividend, str) and isinstance(divisor, list):
|
|
148
|
+
# deliberate choice to make string formatting possible without adding tuples or a format instruction
|
|
149
|
+
divisor = tuple(divisor)
|
|
150
|
+
push(stack, dividend % divisor)
|
|
151
|
+
case OpCode.POW:
|
|
152
|
+
exponent, base = pop(stack), pop(stack)
|
|
153
|
+
if (
|
|
154
|
+
isinstance(base, (int | float))
|
|
155
|
+
and base < 0
|
|
156
|
+
and isinstance(exponent, float)
|
|
157
|
+
and not exponent.is_integer()
|
|
158
|
+
):
|
|
159
|
+
raise ArithmeticError("Cannot raise a negative number to a non-integral power")
|
|
160
|
+
push(stack, base**exponent)
|
|
161
|
+
case OpCode.AND:
|
|
162
|
+
right, left = pop(stack), pop(stack)
|
|
163
|
+
push(stack, left & right)
|
|
164
|
+
case OpCode.OR:
|
|
165
|
+
right, left = pop(stack), pop(stack)
|
|
166
|
+
push(stack, left | right)
|
|
167
|
+
case OpCode.XOR:
|
|
168
|
+
right, left = pop(stack), pop(stack)
|
|
169
|
+
push(stack, left ^ right)
|
|
170
|
+
case OpCode.LSHIFT:
|
|
171
|
+
right, left = pop(stack), pop(stack)
|
|
172
|
+
push(stack, left << right)
|
|
173
|
+
case OpCode.RSHIFT:
|
|
174
|
+
right, left = pop(stack), pop(stack)
|
|
175
|
+
push(stack, left >> right)
|
|
176
|
+
|
|
177
|
+
case OpCode.INVERT:
|
|
178
|
+
push(stack, ~pop(stack))
|
|
179
|
+
case OpCode.NEG:
|
|
180
|
+
push(stack, -pop(stack))
|
|
181
|
+
case OpCode.ABS:
|
|
182
|
+
push(stack, abs(pop(stack)))
|
|
183
|
+
case OpCode.NOT:
|
|
184
|
+
push(stack, not pop(stack))
|
|
185
|
+
|
|
186
|
+
case OpCode.EQ:
|
|
187
|
+
right, left = pop(stack), pop(stack)
|
|
188
|
+
push(stack, left == right)
|
|
189
|
+
case OpCode.NE:
|
|
190
|
+
right, left = pop(stack), pop(stack)
|
|
191
|
+
push(stack, left != right)
|
|
192
|
+
case OpCode.LT:
|
|
193
|
+
right, left = pop(stack), pop(stack)
|
|
194
|
+
push(stack, left < right)
|
|
195
|
+
case OpCode.GT:
|
|
196
|
+
right, left = pop(stack), pop(stack)
|
|
197
|
+
push(stack, left > right)
|
|
198
|
+
case OpCode.LE:
|
|
199
|
+
right, left = pop(stack), pop(stack)
|
|
200
|
+
push(stack, left <= right)
|
|
201
|
+
case OpCode.GE:
|
|
202
|
+
right, left = pop(stack), pop(stack)
|
|
203
|
+
push(stack, left >= right)
|
|
204
|
+
case OpCode.IS:
|
|
205
|
+
right, left = pop(stack), pop(stack)
|
|
206
|
+
push(stack, left is right)
|
|
207
|
+
case OpCode.IS_NOT:
|
|
208
|
+
right, left = pop(stack), pop(stack)
|
|
209
|
+
push(stack, left is not right)
|
|
210
|
+
case OpCode.CONTAINS:
|
|
211
|
+
push(stack, pop(stack) in pop(stack))
|
|
212
|
+
case OpCode.IS_TYPE:
|
|
213
|
+
val = pop(stack)
|
|
214
|
+
cls = TYPES[arg]
|
|
215
|
+
if cls is bool:
|
|
216
|
+
push(stack, type(val) is cls)
|
|
217
|
+
else:
|
|
218
|
+
push(stack, isinstance(val, cls))
|
|
219
|
+
|
|
220
|
+
case OpCode.LIST:
|
|
221
|
+
items = deque()
|
|
222
|
+
for _ in range(arg):
|
|
223
|
+
items.appendleft(pop(stack))
|
|
224
|
+
push(stack, list(items))
|
|
225
|
+
case OpCode.DICT:
|
|
226
|
+
items = deque()
|
|
227
|
+
for _ in range(arg):
|
|
228
|
+
value, key = pop(stack), pop(stack)
|
|
229
|
+
items.appendleft((key, value))
|
|
230
|
+
push(stack, dict(items))
|
|
231
|
+
case OpCode.LEN:
|
|
232
|
+
push(stack, len(peek(stack)))
|
|
233
|
+
case OpCode.APPEND:
|
|
234
|
+
item, obj = (pop(stack), peek(stack))
|
|
235
|
+
obj.append(item)
|
|
236
|
+
case OpCode.KEYS:
|
|
237
|
+
push(stack, list(peek(stack).keys()))
|
|
238
|
+
|
|
239
|
+
case OpCode.GETITEM:
|
|
240
|
+
index, obj = (pop(stack), peek(stack))
|
|
241
|
+
push(stack, obj[index])
|
|
242
|
+
case OpCode.SETITEM:
|
|
243
|
+
value, index, obj = (pop(stack), pop(stack), peek(stack))
|
|
244
|
+
obj[index] = value
|
|
245
|
+
case OpCode.DELITEM:
|
|
246
|
+
index, obj = (pop(stack), peek(stack))
|
|
247
|
+
del obj[index]
|
|
248
|
+
|
|
249
|
+
case OpCode.JUMP:
|
|
250
|
+
to = arg
|
|
251
|
+
case OpCode.JUMP_FALSE:
|
|
252
|
+
if not pop(stack):
|
|
253
|
+
to = arg
|
|
254
|
+
case OpCode.JUMP_TRUE:
|
|
255
|
+
if pop(stack):
|
|
256
|
+
to = arg
|
|
257
|
+
case OpCode.JUMP_INDIRECT:
|
|
258
|
+
target = pop(stack)
|
|
259
|
+
if target < 0:
|
|
260
|
+
raise ValueError("Cannot jump to a negative index")
|
|
261
|
+
to = target
|
|
262
|
+
|
|
263
|
+
case OpCode.CALL:
|
|
264
|
+
if len(stack) < arg2:
|
|
265
|
+
raise StackUnderflowError
|
|
266
|
+
|
|
267
|
+
if arg2:
|
|
268
|
+
call_args = stack[-arg2:]
|
|
269
|
+
del stack[-arg2:]
|
|
270
|
+
else:
|
|
271
|
+
call_args = []
|
|
272
|
+
|
|
273
|
+
state.frames.append(VMFrame(current=arg, stack=call_args, prev=current, globals_=env.maps[1]))
|
|
274
|
+
|
|
275
|
+
case OpCode.CALL_INDIRECT:
|
|
276
|
+
target = pop(stack)
|
|
277
|
+
if target < 0:
|
|
278
|
+
raise ValueError("Cannot jump to a negative index")
|
|
279
|
+
|
|
280
|
+
if len(stack) < arg2:
|
|
281
|
+
raise StackUnderflowError
|
|
282
|
+
|
|
283
|
+
if arg2:
|
|
284
|
+
call_args = stack[-arg2:]
|
|
285
|
+
del stack[-arg2:]
|
|
286
|
+
else:
|
|
287
|
+
call_args = []
|
|
288
|
+
|
|
289
|
+
state.frames.append(VMFrame(current=target, stack=call_args, prev=current, globals_=env.maps[1]))
|
|
290
|
+
|
|
291
|
+
case OpCode.NOW:
|
|
292
|
+
push(stack, time.time())
|
|
293
|
+
case OpCode.OFTEN:
|
|
294
|
+
push(stack, rng.random() < 3 / 4)
|
|
295
|
+
case OpCode.SELDOM:
|
|
296
|
+
push(stack, rng.random() < 1 / 6)
|
|
297
|
+
|
|
298
|
+
case OpCode.INPUT:
|
|
299
|
+
push(stack, stdin())
|
|
300
|
+
case OpCode.PRINT:
|
|
301
|
+
print(pop(stack), file=stdout)
|
|
302
|
+
|
|
303
|
+
case OpCode.OPEN:
|
|
304
|
+
push(stack, open(pop(stack), FILE_MODES[arg])) # noqa: SIM115
|
|
305
|
+
case OpCode.READ:
|
|
306
|
+
push(stack, peek(stack).read(arg))
|
|
307
|
+
case OpCode.WRITE:
|
|
308
|
+
content, file = (pop(stack), peek(stack))
|
|
309
|
+
file.write(content)
|
|
310
|
+
case OpCode.CLOSE:
|
|
311
|
+
pop(stack).close()
|
|
312
|
+
|
|
313
|
+
case OpCode.SIZE:
|
|
314
|
+
push(stack, len(stack))
|
|
315
|
+
case OpCode.LOCATE:
|
|
316
|
+
push(stack, current)
|
|
317
|
+
|
|
318
|
+
# terminal operations don't require an item on the stack
|
|
319
|
+
case OpCode.ERROR:
|
|
320
|
+
try:
|
|
321
|
+
raise InstructedError(pop(stack))
|
|
322
|
+
except StackUnderflowError:
|
|
323
|
+
raise InstructedError
|
|
324
|
+
case OpCode.RETURN:
|
|
325
|
+
returned = True
|
|
326
|
+
try:
|
|
327
|
+
value = stack[-1]
|
|
328
|
+
except StackUnderflowError:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
case OpCode.SHOW:
|
|
332
|
+
title = f"Stack at Instruction {current}"
|
|
333
|
+
print("-" * len(title), file=stdout)
|
|
334
|
+
print(title, file=stdout)
|
|
335
|
+
print(f"(Instruction: {None if prev is None else instructions[prev]})", file=stdout)
|
|
336
|
+
print("-" * len(title), file=stdout)
|
|
337
|
+
|
|
338
|
+
for index in range(len(stack) - 1, -1, -1):
|
|
339
|
+
value = peek(stack, index)
|
|
340
|
+
print(f"{len(stack) - index}: {value!r}", file=stdout)
|
|
341
|
+
|
|
342
|
+
case _:
|
|
343
|
+
assert_never(opcode)
|
|
344
|
+
raise NotImplementedError(f"Unknown opcode: {instr.opcode}")
|
|
345
|
+
except StackUnderflowError:
|
|
346
|
+
raise StackUnderflowError(f"Stack underflow at instruction {current}: {instructions[current]}") from None
|
|
347
|
+
|
|
348
|
+
frame.current = to
|
|
349
|
+
frame.prev = current
|
|
350
|
+
return returned, value
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Executor:
|
|
354
|
+
instructions: tuple[ResolvedInstruction, ...]
|
|
355
|
+
args: tuple[Any, ...]
|
|
356
|
+
call: bool
|
|
357
|
+
rng: Random
|
|
358
|
+
push: Callable[[list[Any], Any], None]
|
|
359
|
+
pop: PopPeek
|
|
360
|
+
peek: PopPeek
|
|
361
|
+
stdin: Callable[[], str]
|
|
362
|
+
stdout: TextIO
|
|
363
|
+
state: VMState
|
|
364
|
+
done: bool
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
instructions: tuple[ResolvedInstruction, ...],
|
|
369
|
+
*args: None | int | str | bool,
|
|
370
|
+
seed: int | str | Random | None = None,
|
|
371
|
+
push: Callable[[list[Any], Any], None],
|
|
372
|
+
pop: PopPeek,
|
|
373
|
+
peek: PopPeek,
|
|
374
|
+
stdin: Callable[[], str],
|
|
375
|
+
stdout: TextIO,
|
|
376
|
+
) -> None:
|
|
377
|
+
self.instructions = instructions
|
|
378
|
+
self.args = args
|
|
379
|
+
self.rng = seed if isinstance(seed, Random) else Random(seed)
|
|
380
|
+
self.push = push
|
|
381
|
+
self.pop = pop
|
|
382
|
+
self.peek = peek
|
|
383
|
+
self.stdin = stdin
|
|
384
|
+
self.stdout = stdout
|
|
385
|
+
self.state = VMState()
|
|
386
|
+
self.done = False
|
|
387
|
+
|
|
388
|
+
for arg in self.args:
|
|
389
|
+
push(self.state.frames[-1].stack, arg)
|
|
390
|
+
|
|
391
|
+
def __call__(self) -> Any:
|
|
392
|
+
if self.done:
|
|
393
|
+
raise RuntimeError("Executor has already returned")
|
|
394
|
+
|
|
395
|
+
returned, value = False, None
|
|
396
|
+
while not returned:
|
|
397
|
+
returned, value = self.step()
|
|
398
|
+
self.done = True
|
|
399
|
+
return value
|
|
400
|
+
|
|
401
|
+
def step(self) -> tuple[Literal[True], Any] | tuple[Literal[False], None]:
|
|
402
|
+
if self.done:
|
|
403
|
+
raise RuntimeError("Executor has already returned")
|
|
404
|
+
|
|
405
|
+
returned, value = process_instruction(
|
|
406
|
+
self.state, self.instructions, self.push, self.pop, self.peek, self.rng, self.stdin, self.stdout
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if returned:
|
|
410
|
+
self.state.frames.pop()
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
frame = self.state.frames[-1]
|
|
414
|
+
except IndexError:
|
|
415
|
+
self.done = True
|
|
416
|
+
return True, self.peek([value])
|
|
417
|
+
|
|
418
|
+
frame.stack.append(value)
|
|
419
|
+
|
|
420
|
+
return False, None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class StackMachine:
|
|
424
|
+
__slots__ = ("_forward_refs", "_instructions", "_sealed", "_used_vars")
|
|
425
|
+
|
|
426
|
+
_instructions: list[Instruction]
|
|
427
|
+
_sealed: bool
|
|
428
|
+
_used_vars: set[int]
|
|
429
|
+
_forward_refs: dict[sentinel, int]
|
|
430
|
+
|
|
431
|
+
def __init__(self) -> None:
|
|
432
|
+
self._instructions = []
|
|
433
|
+
self._sealed = False
|
|
434
|
+
self._used_vars = set()
|
|
435
|
+
self._forward_refs = {}
|
|
436
|
+
|
|
437
|
+
def __call__(self, *args: None | int | str | bool, seed: int | str | Random | None = None) -> Any:
|
|
438
|
+
"""
|
|
439
|
+
Docstring for __call__
|
|
440
|
+
|
|
441
|
+
:param args: The initial arguments to pass into the program.
|
|
442
|
+
These will be present on the stack at the start of the program, with the first supplied argument at the bottom.
|
|
443
|
+
:param seed: The random seed or `Random` instance used for the `OFTEN` and `SELDOM` instructions.
|
|
444
|
+
:return: The value returned by the program.
|
|
445
|
+
"""
|
|
446
|
+
executor = self.executor(*args, seed=seed)
|
|
447
|
+
return executor()
|
|
448
|
+
|
|
449
|
+
def __bytes__(self) -> bytes:
|
|
450
|
+
result = bytearray()
|
|
451
|
+
for instr in self.instructions:
|
|
452
|
+
result.extend(bytes(instr))
|
|
453
|
+
return bytes(result)
|
|
454
|
+
|
|
455
|
+
def executor(
|
|
456
|
+
self,
|
|
457
|
+
*args: None | int | str | bool,
|
|
458
|
+
seed: int | str | Random | None = None,
|
|
459
|
+
push: Callable[[list[Any], Any], None] | None = None,
|
|
460
|
+
pop: PopPeek | None = None,
|
|
461
|
+
peek: PopPeek | None = None,
|
|
462
|
+
) -> Executor:
|
|
463
|
+
if push is None:
|
|
464
|
+
|
|
465
|
+
def default_push(stack: list[Any], obj: Any) -> None:
|
|
466
|
+
stack.append(obj)
|
|
467
|
+
|
|
468
|
+
push = default_push
|
|
469
|
+
|
|
470
|
+
if pop is None:
|
|
471
|
+
|
|
472
|
+
def default_pop(stack: list[Any], index: int = -1) -> Any:
|
|
473
|
+
try:
|
|
474
|
+
return stack.pop(index)
|
|
475
|
+
except IndexError:
|
|
476
|
+
raise StackUnderflowError from None
|
|
477
|
+
|
|
478
|
+
pop = default_pop
|
|
479
|
+
|
|
480
|
+
if peek is None:
|
|
481
|
+
|
|
482
|
+
def default_peek(stack: list[Any], index: int = -1) -> Any:
|
|
483
|
+
try:
|
|
484
|
+
return stack[index]
|
|
485
|
+
except IndexError:
|
|
486
|
+
raise StackUnderflowError from None
|
|
487
|
+
|
|
488
|
+
peek = default_peek
|
|
489
|
+
|
|
490
|
+
instructions = tuple(self._instructions)
|
|
491
|
+
if not self._sealed and any(isinstance(instr.arg, sentinel) for instr in instructions):
|
|
492
|
+
raise RuntimeError("Unbound forward reference found in instructions")
|
|
493
|
+
instructions = cast(tuple[ResolvedInstruction, ...], instructions)
|
|
494
|
+
|
|
495
|
+
if not all(isinstance(arg, NoneType | int) or isinstance(arg, str) and len(arg) <= 1 for arg in args):
|
|
496
|
+
raise TypeError("Only arguments of None, int, bool, or chr are permitted")
|
|
497
|
+
|
|
498
|
+
return Executor(instructions, *args, seed=seed, push=push, pop=pop, peek=peek, stdin=input, stdout=sys.stdout)
|
|
499
|
+
|
|
500
|
+
def ui_executor(
|
|
501
|
+
self,
|
|
502
|
+
*args: None | int | str | bool,
|
|
503
|
+
seed: int | str | Random | None = None,
|
|
504
|
+
stdin: Callable[[], str] = input,
|
|
505
|
+
stdout: TextIO = sys.stdout,
|
|
506
|
+
) -> Executor:
|
|
507
|
+
instructions = tuple(self._instructions)
|
|
508
|
+
if not self._sealed and any(isinstance(instr.arg, sentinel) for instr in instructions):
|
|
509
|
+
raise RuntimeError("Unbound forward reference found in instructions")
|
|
510
|
+
instructions = cast(tuple[ResolvedInstruction, ...], instructions)
|
|
511
|
+
|
|
512
|
+
if not all(isinstance(arg, NoneType | int) or isinstance(arg, str) and len(arg) <= 1 for arg in args):
|
|
513
|
+
raise TypeError("Only arguments of None, int, bool, or chr are permitted")
|
|
514
|
+
|
|
515
|
+
def push(stack: list[Any], obj: Any) -> None:
|
|
516
|
+
stack.append(StackItem(obj))
|
|
517
|
+
|
|
518
|
+
def pop(stack: list[StackItem], index: int = -1) -> Any:
|
|
519
|
+
try:
|
|
520
|
+
return stack.pop(index).obj
|
|
521
|
+
except IndexError:
|
|
522
|
+
raise StackUnderflowError from None
|
|
523
|
+
|
|
524
|
+
def peek(stack: list[StackItem], index: int = -1) -> Any:
|
|
525
|
+
try:
|
|
526
|
+
return stack[index].obj
|
|
527
|
+
except IndexError:
|
|
528
|
+
raise StackUnderflowError from None
|
|
529
|
+
|
|
530
|
+
return Executor(
|
|
531
|
+
instructions,
|
|
532
|
+
*args,
|
|
533
|
+
seed=seed,
|
|
534
|
+
push=push,
|
|
535
|
+
pop=pop,
|
|
536
|
+
peek=peek,
|
|
537
|
+
stdin=stdin,
|
|
538
|
+
stdout=stdout,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def ui(self, *args: None | int | bool | str, seed: int | str | Random | None = None) -> Any:
|
|
542
|
+
"""
|
|
543
|
+
Executes the program in the interactive UI powered by Flet.
|
|
544
|
+
|
|
545
|
+
:param args: The initial arguments to pass into the program.
|
|
546
|
+
These will be present on the stack at the start of the program, with the first supplied argument at the bottom.
|
|
547
|
+
:param seed: The random seed or `Random` instance used for the `OFTEN` and `SELDOM` instructions.
|
|
548
|
+
:return: The value returned by the program.
|
|
549
|
+
"""
|
|
550
|
+
from stackvm._ui import execute
|
|
551
|
+
|
|
552
|
+
return execute(self, *args, seed=seed)
|
|
553
|
+
|
|
554
|
+
def dump_file(self, path: str | Path | BinaryIO) -> None:
|
|
555
|
+
if isinstance(path, str):
|
|
556
|
+
path = Path(path)
|
|
557
|
+
|
|
558
|
+
if isinstance(path, Path):
|
|
559
|
+
path.write_bytes(bytes(self))
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
path.write(bytes(self))
|
|
563
|
+
|
|
564
|
+
@classmethod
|
|
565
|
+
def load_file(cls, path: str | Path | BinaryIO) -> Self:
|
|
566
|
+
if isinstance(path, str):
|
|
567
|
+
path = Path(path)
|
|
568
|
+
|
|
569
|
+
if isinstance(path, Path):
|
|
570
|
+
data = path.read_bytes()
|
|
571
|
+
return cls.from_bytes(data)
|
|
572
|
+
|
|
573
|
+
data = path.read()
|
|
574
|
+
return cls.from_bytes(data)
|
|
575
|
+
|
|
576
|
+
@classmethod
|
|
577
|
+
def from_bytes(cls, data: bytes | bytearray) -> Self:
|
|
578
|
+
if len(data) % 2 == 1:
|
|
579
|
+
raise ValueError("Cannot load an odd number of bytes")
|
|
580
|
+
|
|
581
|
+
instructions: list[Instruction] = []
|
|
582
|
+
|
|
583
|
+
used_vars: set[int] = set()
|
|
584
|
+
|
|
585
|
+
current: int = 0
|
|
586
|
+
for chunk in it.batched(data, 2):
|
|
587
|
+
opcode_value, arg = chunk
|
|
588
|
+
current += arg
|
|
589
|
+
|
|
590
|
+
if opcode_value == 255:
|
|
591
|
+
current <<= 8
|
|
592
|
+
continue
|
|
593
|
+
|
|
594
|
+
arg = unzigzag(current)
|
|
595
|
+
|
|
596
|
+
opcode = OpCode(opcode_value)
|
|
597
|
+
if opcode.args == 2:
|
|
598
|
+
arg, arg2 = arg >> 4, arg & 15
|
|
599
|
+
instructions.append(Instruction(opcode, arg, arg2))
|
|
600
|
+
else:
|
|
601
|
+
instructions.append(Instruction(opcode, arg))
|
|
602
|
+
|
|
603
|
+
if opcode in {OpCode.LOAD, OpCode.STORE, OpCode.GLOBAL}:
|
|
604
|
+
used_vars.add(arg)
|
|
605
|
+
|
|
606
|
+
current = 0
|
|
607
|
+
|
|
608
|
+
self = cls()
|
|
609
|
+
self._instructions = instructions
|
|
610
|
+
self._used_vars = used_vars
|
|
611
|
+
self._sealed = True
|
|
612
|
+
|
|
613
|
+
return self
|
|
614
|
+
|
|
615
|
+
def __enter__(self) -> Self:
|
|
616
|
+
return self
|
|
617
|
+
|
|
618
|
+
def __exit__(self, *args):
|
|
619
|
+
if any(isinstance(instr.arg, sentinel) for instr in self._instructions):
|
|
620
|
+
raise RuntimeError("Unresolved forward reference found in stack machine definition")
|
|
621
|
+
self._sealed = True
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def instructions(self) -> tuple[Instruction, ...]:
|
|
625
|
+
return tuple(self._instructions)
|
|
626
|
+
|
|
627
|
+
def dis(self) -> str:
|
|
628
|
+
result = io.StringIO()
|
|
629
|
+
|
|
630
|
+
jump_targets = set()
|
|
631
|
+
jump_opcodes = {OpCode.JUMP, OpCode.JUMP_TRUE, OpCode.JUMP_FALSE, OpCode.CALL}
|
|
632
|
+
for i, instr in enumerate(self._instructions):
|
|
633
|
+
if instr.opcode in jump_opcodes and isinstance(instr.arg, int):
|
|
634
|
+
jump_targets.add(instr.arg)
|
|
635
|
+
|
|
636
|
+
labels = {target: i for i, target in enumerate(sorted(jump_targets))}
|
|
637
|
+
label_width = 0 if not labels else len(str(len(labels))) + 2
|
|
638
|
+
|
|
639
|
+
digits = len(str(len(self._instructions) - 1))
|
|
640
|
+
|
|
641
|
+
print("-------------------------", file=result)
|
|
642
|
+
print("STACK MACHINE DISASSEMBLY", file=result)
|
|
643
|
+
print("-------------------------", file=result)
|
|
644
|
+
|
|
645
|
+
for i, instr in enumerate(self._instructions):
|
|
646
|
+
if label_width:
|
|
647
|
+
if i in labels:
|
|
648
|
+
print(f"{'L' + str(labels[i]) + ':':>{label_width}} ", file=result, end="")
|
|
649
|
+
else:
|
|
650
|
+
print(" " * (label_width + 1), file=result, end="")
|
|
651
|
+
|
|
652
|
+
print(f"{i:0{digits}d} {instr.opcode.name:<12} {instr.arg}", file=result, end="")
|
|
653
|
+
|
|
654
|
+
if instr.has_second_arg():
|
|
655
|
+
print(f" {instr.arg2}", file=result, end="")
|
|
656
|
+
|
|
657
|
+
if instr.opcode in jump_opcodes and not isinstance(instr.arg, sentinel):
|
|
658
|
+
try:
|
|
659
|
+
print(f" (L{labels[instr.arg]})", file=result)
|
|
660
|
+
except KeyError:
|
|
661
|
+
print(file=result)
|
|
662
|
+
else:
|
|
663
|
+
print(file=result)
|
|
664
|
+
|
|
665
|
+
return result.getvalue()
|
|
666
|
+
|
|
667
|
+
def _instr(self, opcode: OpCode, arg: int | sentinel = 0, arg2: int = 0) -> None:
|
|
668
|
+
if self._sealed:
|
|
669
|
+
raise RuntimeError("Stack machine definition has been finalized")
|
|
670
|
+
self._instructions.append(Instruction(opcode, arg, arg2))
|
|
671
|
+
|
|
672
|
+
def noop(self) -> Self:
|
|
673
|
+
"""This instruction does nothing when the program is executed."""
|
|
674
|
+
self._instr(OpCode.NOOP)
|
|
675
|
+
return self
|
|
676
|
+
|
|
677
|
+
def const(self, *values: None | int | bool | str) -> Self:
|
|
678
|
+
"""
|
|
679
|
+
Pushes a constant to the stack.
|
|
680
|
+
Permitted constants are those of type `None`, `int`, `bool`, as well as strings of length 0 or 1.
|
|
681
|
+
"""
|
|
682
|
+
for value in values:
|
|
683
|
+
if value is None:
|
|
684
|
+
self._instr(OpCode.NONE)
|
|
685
|
+
elif type(value) is bool:
|
|
686
|
+
self._instr(OpCode.BOOL, int(value))
|
|
687
|
+
elif type(value) is int:
|
|
688
|
+
self._instr(OpCode.INT, value)
|
|
689
|
+
elif type(value) is str and len(value) <= 1:
|
|
690
|
+
self._instr(OpCode.CHR, ord(value) if value else -1)
|
|
691
|
+
else:
|
|
692
|
+
raise TypeError(f"Disallowed constant {value!r}")
|
|
693
|
+
return self
|
|
694
|
+
|
|
695
|
+
def int(self) -> Self:
|
|
696
|
+
"""Calls `int()` on the topmost value on the stack."""
|
|
697
|
+
self._instr(OpCode.TO_INT)
|
|
698
|
+
return self
|
|
699
|
+
|
|
700
|
+
def float(self) -> Self:
|
|
701
|
+
"""Calls `float()` on the topmost value on the stack."""
|
|
702
|
+
self._instr(OpCode.TO_FLOAT)
|
|
703
|
+
return self
|
|
704
|
+
|
|
705
|
+
def chr(self) -> Self:
|
|
706
|
+
"""Calls `chr()` on the topmost value on the stack."""
|
|
707
|
+
self._instr(OpCode.TO_CHR)
|
|
708
|
+
return self
|
|
709
|
+
|
|
710
|
+
def str(self) -> Self:
|
|
711
|
+
"""Calls `str()` on the topmost value on the stack."""
|
|
712
|
+
self._instr(OpCode.TO_STR)
|
|
713
|
+
return self
|
|
714
|
+
|
|
715
|
+
def bool(self) -> Self:
|
|
716
|
+
"""Calls `bool()` on the topmost value on the stack."""
|
|
717
|
+
self._instr(OpCode.TO_BOOL)
|
|
718
|
+
return self
|
|
719
|
+
|
|
720
|
+
def bytes(self) -> Self:
|
|
721
|
+
"""Calls `bytes()` on the topmost value on the stack."""
|
|
722
|
+
self._instr(OpCode.TO_BYTES)
|
|
723
|
+
return self
|
|
724
|
+
|
|
725
|
+
def ord(self) -> Self:
|
|
726
|
+
"""Calls `ord()` on the topmost value on the stack."""
|
|
727
|
+
self._instr(OpCode.ORD)
|
|
728
|
+
return self
|
|
729
|
+
|
|
730
|
+
def slice(self) -> Self:
|
|
731
|
+
"""
|
|
732
|
+
Pops the three topmost items from the stack, and pushes `slice(third, second, top)` to the stack.
|
|
733
|
+
"""
|
|
734
|
+
self._instr(OpCode.SLICE)
|
|
735
|
+
return self
|
|
736
|
+
|
|
737
|
+
def type(self) -> Self:
|
|
738
|
+
"""
|
|
739
|
+
Replaces the on top of the stack with an integer representing its type, according to the following:
|
|
740
|
+
|
|
741
|
+
* `None`: 0
|
|
742
|
+
* `bool`: 1
|
|
743
|
+
* `int`: 2
|
|
744
|
+
* `float`: 3
|
|
745
|
+
* `str`: 4
|
|
746
|
+
* `bytes`: 5
|
|
747
|
+
* `slice`: 6
|
|
748
|
+
* `list`: 7
|
|
749
|
+
* `dict`: 8
|
|
750
|
+
* File (`IOBase`): 9
|
|
751
|
+
"""
|
|
752
|
+
self._instr(OpCode.TYPE)
|
|
753
|
+
return self
|
|
754
|
+
|
|
755
|
+
def new_var(self) -> int:
|
|
756
|
+
"""
|
|
757
|
+
Allocates a new variable, guaranteed to use a variable ID that has not been used yet.
|
|
758
|
+
|
|
759
|
+
:return: The ID of the allocated variable.
|
|
760
|
+
"""
|
|
761
|
+
for i in it.count():
|
|
762
|
+
if i not in self._used_vars:
|
|
763
|
+
self._used_vars.add(i)
|
|
764
|
+
return i
|
|
765
|
+
assert False
|
|
766
|
+
|
|
767
|
+
def new_global(self) -> int:
|
|
768
|
+
"""
|
|
769
|
+
Allocates a new variable, guaranteed to use a variable ID that has not been used yet.
|
|
770
|
+
This new variable is set to be global in the current frame.
|
|
771
|
+
|
|
772
|
+
:return: The ID of the allocated variable.
|
|
773
|
+
"""
|
|
774
|
+
for i in it.count():
|
|
775
|
+
if i not in self._used_vars:
|
|
776
|
+
self._used_vars.add(i)
|
|
777
|
+
self.global_var(i)
|
|
778
|
+
return i
|
|
779
|
+
assert False
|
|
780
|
+
|
|
781
|
+
def load(self, *ids: int) -> Self:
|
|
782
|
+
"""
|
|
783
|
+
Loads the variable assigned to the given ID, pushing it onto the stack.
|
|
784
|
+
If multiple IDs are given, they will all be pushed onto the stack in order.
|
|
785
|
+
|
|
786
|
+
:param ids: The IDs of the variables to load.
|
|
787
|
+
It is recommended that these are values returned by `new_var()` or `new_global()`, to avoid conflicts.
|
|
788
|
+
"""
|
|
789
|
+
for var in ids:
|
|
790
|
+
self._used_vars.add(var)
|
|
791
|
+
self._instr(OpCode.LOAD, var)
|
|
792
|
+
return self
|
|
793
|
+
|
|
794
|
+
def store(self, *ids: int) -> Self:
|
|
795
|
+
"""
|
|
796
|
+
Stores the topmost value on the stack to the variable with the given ID.
|
|
797
|
+
If multiple IDs are given, the topmost value on the stack will be assigned to the first supplied ID,
|
|
798
|
+
the next value on the stack will be assigned to the second supplied ID, and so on.
|
|
799
|
+
|
|
800
|
+
:param ids: The IDs of the variables to store.
|
|
801
|
+
It is recommended that these are values returned by `new_var()` or `new_global()`, to avoid conflicts.
|
|
802
|
+
"""
|
|
803
|
+
for var in ids:
|
|
804
|
+
self._used_vars.add(var)
|
|
805
|
+
self._instr(OpCode.STORE, var)
|
|
806
|
+
return self
|
|
807
|
+
|
|
808
|
+
def global_var(self, *ids: int) -> Self:
|
|
809
|
+
"""
|
|
810
|
+
Marks the variable(s) with the given ID(s) as global in the current frame.
|
|
811
|
+
|
|
812
|
+
:param ids: The IDs of the variables to set as global.
|
|
813
|
+
It is recommended that these are values returned by `new_var()` or `new_global()`, to avoid conflicts.
|
|
814
|
+
"""
|
|
815
|
+
for var in ids:
|
|
816
|
+
self._used_vars.add(var)
|
|
817
|
+
self._instr(OpCode.GLOBAL, var)
|
|
818
|
+
return self
|
|
819
|
+
|
|
820
|
+
def copy(self, *depths: int) -> Self:
|
|
821
|
+
"""
|
|
822
|
+
Pushes a copy of a value in the stack to the top of the stack.
|
|
823
|
+
|
|
824
|
+
The stack is 1-indexed, so 1 indicates the top item, 2 indicates the second item from top, and so on.
|
|
825
|
+
Negative indices are relative to the bottom of the stack, so -1 indicates the bottom item, etc.
|
|
826
|
+
|
|
827
|
+
If multiple depths are specified, all positive ones following the first will be adjusted
|
|
828
|
+
to account for the items pushed onto the stack by the previous copy instructions.
|
|
829
|
+
Therefore, `.copy(1, 2, 3)` is equivalent to `.copy(1).copy(3).copy(5)`.
|
|
830
|
+
|
|
831
|
+
:param depths: The 1-indexed locations of values from the stack to copy.
|
|
832
|
+
If not supplied, defaults to `(1,)`, copying the topmost value on the stack.
|
|
833
|
+
"""
|
|
834
|
+
if not depths:
|
|
835
|
+
depths = (1,)
|
|
836
|
+
|
|
837
|
+
for i, depth in enumerate(depths):
|
|
838
|
+
if depth == 0:
|
|
839
|
+
raise TypeError("Copy depth cannot be 0")
|
|
840
|
+
if depth < 0:
|
|
841
|
+
self._instr(OpCode.COPY, depth)
|
|
842
|
+
else:
|
|
843
|
+
self._instr(OpCode.COPY, depth + i)
|
|
844
|
+
return self
|
|
845
|
+
|
|
846
|
+
def swap(self, depth: int = 2) -> Self:
|
|
847
|
+
"""
|
|
848
|
+
Swaps the value at the given index with the topmost value on the stack.
|
|
849
|
+
|
|
850
|
+
The stack is 1-indexed, so 1 indicates the top item, 2 indicates the second item from top, and so on.
|
|
851
|
+
Negative indices are relative to the bottom of the stack, so -1 indicates the bottom item, etc.
|
|
852
|
+
|
|
853
|
+
:param depth: The 1-indexed location of value from the stack to swap with.
|
|
854
|
+
If not supplied, defaults to `2`, swapping the two topmost values on the stack.
|
|
855
|
+
"""
|
|
856
|
+
if depth == 0:
|
|
857
|
+
raise TypeError("Swap depth cannot be 0")
|
|
858
|
+
self._instr(OpCode.SWAP, depth)
|
|
859
|
+
return self
|
|
860
|
+
|
|
861
|
+
def roll(self, depth: int = 3) -> Self:
|
|
862
|
+
"""
|
|
863
|
+
Moves the value at the given index to the top of the stack, shifting intervening items down one position.
|
|
864
|
+
|
|
865
|
+
The stack is 1-indexed, so 1 indicates the top item, 2 indicates the second item from top, and so on.
|
|
866
|
+
Negative indices are relative to the bottom of the stack, so -1 indicates the bottom item, etc.
|
|
867
|
+
|
|
868
|
+
:param depth: The 1-indexed location of value from the stack to swap with. If not supplied, defaults to `3`,
|
|
869
|
+
moving the third item on the stack to the top, while shifting the two topmost items down one position.
|
|
870
|
+
"""
|
|
871
|
+
if depth == 0:
|
|
872
|
+
raise TypeError("Roll depth cannot be 0")
|
|
873
|
+
self._instr(OpCode.ROLL, depth)
|
|
874
|
+
return self
|
|
875
|
+
|
|
876
|
+
def pop(self, times: int = 1) -> Self:
|
|
877
|
+
"""
|
|
878
|
+
Pops the topmost item on the stack.
|
|
879
|
+
|
|
880
|
+
:param times: How many items to pop from the stack. Defaults to 1.
|
|
881
|
+
"""
|
|
882
|
+
for _ in range(times):
|
|
883
|
+
self._instr(OpCode.POP)
|
|
884
|
+
return self
|
|
885
|
+
|
|
886
|
+
def add(self) -> Self:
|
|
887
|
+
"""
|
|
888
|
+
Pops the two topmost items from the stack and pushes `second + top` to the stack.
|
|
889
|
+
"""
|
|
890
|
+
self._instr(OpCode.ADD)
|
|
891
|
+
return self
|
|
892
|
+
|
|
893
|
+
def sub(self) -> Self:
|
|
894
|
+
"""
|
|
895
|
+
Pops the two topmost items from the stack and pushes `second - top` to the stack.
|
|
896
|
+
"""
|
|
897
|
+
self._instr(OpCode.SUB)
|
|
898
|
+
return self
|
|
899
|
+
|
|
900
|
+
def mul(self) -> Self:
|
|
901
|
+
"""
|
|
902
|
+
Pops the two topmost items from the stack and pushes `second * top` to the stack.
|
|
903
|
+
"""
|
|
904
|
+
self._instr(OpCode.MUL)
|
|
905
|
+
return self
|
|
906
|
+
|
|
907
|
+
def div(self) -> Self:
|
|
908
|
+
"""
|
|
909
|
+
Pops the two topmost items from the stack and pushes `second / top` to the stack.
|
|
910
|
+
"""
|
|
911
|
+
self._instr(OpCode.DIV)
|
|
912
|
+
return self
|
|
913
|
+
|
|
914
|
+
def floordiv(self) -> Self:
|
|
915
|
+
"""
|
|
916
|
+
Pops the two topmost items from the stack and pushes `second // top` to the stack.
|
|
917
|
+
"""
|
|
918
|
+
self._instr(OpCode.FLOORDIV)
|
|
919
|
+
return self
|
|
920
|
+
|
|
921
|
+
def mod(self) -> Self:
|
|
922
|
+
"""
|
|
923
|
+
Pops the two topmost items from the stack and pushes `second % top` to the stack.
|
|
924
|
+
|
|
925
|
+
Uniquely, if `second` is of type `str` and `top` is of type `list`,
|
|
926
|
+
`top` is converted to `tuple` before executing this instruction.
|
|
927
|
+
This will perform %-style string formatting.
|
|
928
|
+
"""
|
|
929
|
+
self._instr(OpCode.MOD)
|
|
930
|
+
return self
|
|
931
|
+
|
|
932
|
+
def pow(self) -> Self:
|
|
933
|
+
"""
|
|
934
|
+
Pops the two topmost items from the stack and pushes `second ** top` to the stack.
|
|
935
|
+
"""
|
|
936
|
+
self._instr(OpCode.POW)
|
|
937
|
+
return self
|
|
938
|
+
|
|
939
|
+
def band(self) -> Self:
|
|
940
|
+
"""
|
|
941
|
+
Pops the two topmost items from the stack and pushes `second & top` to the stack.
|
|
942
|
+
"""
|
|
943
|
+
self._instr(OpCode.AND)
|
|
944
|
+
return self
|
|
945
|
+
|
|
946
|
+
def bor(self) -> Self:
|
|
947
|
+
"""
|
|
948
|
+
Pops the two topmost items from the stack and pushes `second | top` to the stack.
|
|
949
|
+
"""
|
|
950
|
+
self._instr(OpCode.OR)
|
|
951
|
+
return self
|
|
952
|
+
|
|
953
|
+
def xor(self) -> Self:
|
|
954
|
+
"""
|
|
955
|
+
Pops the two topmost items from the stack and pushes `second ^ top` to the stack.
|
|
956
|
+
"""
|
|
957
|
+
self._instr(OpCode.XOR)
|
|
958
|
+
return self
|
|
959
|
+
|
|
960
|
+
def lshift(self) -> Self:
|
|
961
|
+
"""
|
|
962
|
+
Pops the two topmost items from the stack and pushes `second << top` to the stack.
|
|
963
|
+
"""
|
|
964
|
+
self._instr(OpCode.LSHIFT)
|
|
965
|
+
return self
|
|
966
|
+
|
|
967
|
+
def rshift(self) -> Self:
|
|
968
|
+
"""
|
|
969
|
+
Pops the two topmost items from the stack and pushes `second >> top` to the stack.
|
|
970
|
+
"""
|
|
971
|
+
self._instr(OpCode.RSHIFT)
|
|
972
|
+
return self
|
|
973
|
+
|
|
974
|
+
def invert(self) -> Self:
|
|
975
|
+
"""
|
|
976
|
+
Inverts (`~`) the topmost item on the stack.
|
|
977
|
+
"""
|
|
978
|
+
self._instr(OpCode.INVERT)
|
|
979
|
+
return self
|
|
980
|
+
|
|
981
|
+
def neg(self) -> Self:
|
|
982
|
+
"""
|
|
983
|
+
Negates (`-`) the topmost item on the stack.
|
|
984
|
+
"""
|
|
985
|
+
self._instr(OpCode.NEG)
|
|
986
|
+
return self
|
|
987
|
+
|
|
988
|
+
def abs(self) -> Self:
|
|
989
|
+
"""
|
|
990
|
+
Calls `abs()` on the topmost item on the stack.
|
|
991
|
+
"""
|
|
992
|
+
self._instr(OpCode.ABS)
|
|
993
|
+
return self
|
|
994
|
+
|
|
995
|
+
def lnot(self) -> Self:
|
|
996
|
+
"""
|
|
997
|
+
Pops the topmost item on the stack, and pushes `not top` to the stack.
|
|
998
|
+
"""
|
|
999
|
+
self._instr(OpCode.NOT)
|
|
1000
|
+
return self
|
|
1001
|
+
|
|
1002
|
+
def eq(self) -> Self:
|
|
1003
|
+
"""
|
|
1004
|
+
Pops the two topmost items from the stack and pushes `second == top` to the stack.
|
|
1005
|
+
"""
|
|
1006
|
+
self._instr(OpCode.EQ)
|
|
1007
|
+
return self
|
|
1008
|
+
|
|
1009
|
+
def ne(self) -> Self:
|
|
1010
|
+
"""
|
|
1011
|
+
Pops the two topmost items from the stack and pushes `second != top` to the stack.
|
|
1012
|
+
"""
|
|
1013
|
+
self._instr(OpCode.NE)
|
|
1014
|
+
return self
|
|
1015
|
+
|
|
1016
|
+
def lt(self) -> Self:
|
|
1017
|
+
"""
|
|
1018
|
+
Pops the two topmost items from the stack and pushes `second < top` to the stack.
|
|
1019
|
+
"""
|
|
1020
|
+
self._instr(OpCode.LT)
|
|
1021
|
+
return self
|
|
1022
|
+
|
|
1023
|
+
def gt(self) -> Self:
|
|
1024
|
+
"""
|
|
1025
|
+
Pops the two topmost items from the stack and pushes `second > top` to the stack.
|
|
1026
|
+
"""
|
|
1027
|
+
self._instr(OpCode.GT)
|
|
1028
|
+
return self
|
|
1029
|
+
|
|
1030
|
+
def le(self) -> Self:
|
|
1031
|
+
"""
|
|
1032
|
+
Pops the two topmost items from the stack and pushes `second <= top` to the stack.
|
|
1033
|
+
"""
|
|
1034
|
+
self._instr(OpCode.LE)
|
|
1035
|
+
return self
|
|
1036
|
+
|
|
1037
|
+
def ge(self) -> Self:
|
|
1038
|
+
"""
|
|
1039
|
+
Pops the two topmost items from the stack and pushes `second >= top` to the stack.
|
|
1040
|
+
"""
|
|
1041
|
+
self._instr(OpCode.GE)
|
|
1042
|
+
return self
|
|
1043
|
+
|
|
1044
|
+
def ident(self) -> Self:
|
|
1045
|
+
"""
|
|
1046
|
+
Pops the two topmost items from the stack and pushes `second is top` to the stack.
|
|
1047
|
+
"""
|
|
1048
|
+
self._instr(OpCode.IS)
|
|
1049
|
+
return self
|
|
1050
|
+
|
|
1051
|
+
def is_not(self) -> Self:
|
|
1052
|
+
"""
|
|
1053
|
+
Pops the two topmost items from the stack and pushes `second is not top` to the stack.
|
|
1054
|
+
"""
|
|
1055
|
+
self._instr(OpCode.IS_NOT)
|
|
1056
|
+
return self
|
|
1057
|
+
|
|
1058
|
+
def contains(self) -> Self:
|
|
1059
|
+
"""
|
|
1060
|
+
Pops the two topmost items from the stack and pushes `top in second` to the stack.
|
|
1061
|
+
Note the differing order of the operands in contrast to the rest of the binary operations.
|
|
1062
|
+
"""
|
|
1063
|
+
self._instr(OpCode.CONTAINS)
|
|
1064
|
+
return self
|
|
1065
|
+
|
|
1066
|
+
def is_type(self, cls: None | type) -> Self:
|
|
1067
|
+
"""
|
|
1068
|
+
Pops the topmost item on the stack, and pushes True only if it is of the specified type.
|
|
1069
|
+
|
|
1070
|
+
:param cls: The type to check the item against.
|
|
1071
|
+
Can be `None`, `NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `slice`, `list`, `dict`, or `IOBase`.
|
|
1072
|
+
"""
|
|
1073
|
+
try:
|
|
1074
|
+
self._instr(OpCode.IS_TYPE, TYPES.index(NoneType if cls is None else cls))
|
|
1075
|
+
except ValueError:
|
|
1076
|
+
assert cls is not None
|
|
1077
|
+
raise ValueError(f"Type {cls.__name__} is unobtainable") from None
|
|
1078
|
+
return self
|
|
1079
|
+
|
|
1080
|
+
def list(self, length: int = 0) -> Self:
|
|
1081
|
+
"""
|
|
1082
|
+
Pops the top `length` items off the stack, collecting them into a list which is then pushed onto the stack.
|
|
1083
|
+
The topmost item on the stack becomes the last item in the list.
|
|
1084
|
+
|
|
1085
|
+
:param length: The size of the list to create. 0 by default, creating an empty list.
|
|
1086
|
+
"""
|
|
1087
|
+
if length < 0:
|
|
1088
|
+
raise ValueError("List size cannot be negative")
|
|
1089
|
+
self._instr(OpCode.LIST, length)
|
|
1090
|
+
return self
|
|
1091
|
+
|
|
1092
|
+
def dict(self, length: int = 0) -> Self:
|
|
1093
|
+
"""
|
|
1094
|
+
Pushes a dictionary of size `length` onto the stack, collecting the topmost items on the stack.
|
|
1095
|
+
The topmost item will become the value of the last item in the dictionary,
|
|
1096
|
+
and the item below that will be the last key in the dictionary, and so on.
|
|
1097
|
+
|
|
1098
|
+
:param length: The size of the dictionary to create. 0 by default, creating an empty default.
|
|
1099
|
+
"""
|
|
1100
|
+
if length < 0:
|
|
1101
|
+
raise ValueError("Dictionary size cannot be negative")
|
|
1102
|
+
self._instr(OpCode.DICT, length)
|
|
1103
|
+
return self
|
|
1104
|
+
|
|
1105
|
+
def len(self) -> Self:
|
|
1106
|
+
"""
|
|
1107
|
+
Pushes the length of the topmost item to the stack.
|
|
1108
|
+
"""
|
|
1109
|
+
self._instr(OpCode.LEN)
|
|
1110
|
+
return self
|
|
1111
|
+
|
|
1112
|
+
def append(self) -> Self:
|
|
1113
|
+
"""
|
|
1114
|
+
Pops the topmost item on the stack and calls `second.append(top)`.
|
|
1115
|
+
"""
|
|
1116
|
+
self._instr(OpCode.APPEND)
|
|
1117
|
+
return self
|
|
1118
|
+
|
|
1119
|
+
def keys(self) -> Self:
|
|
1120
|
+
"""
|
|
1121
|
+
Pushes a list containing the keys of the dictionary on top of the stack.
|
|
1122
|
+
"""
|
|
1123
|
+
self._instr(OpCode.KEYS)
|
|
1124
|
+
return self
|
|
1125
|
+
|
|
1126
|
+
def getitem(self) -> Self:
|
|
1127
|
+
"""
|
|
1128
|
+
Pops the top item from the stack, and pushes `second[top]` to the stack.
|
|
1129
|
+
"""
|
|
1130
|
+
self._instr(OpCode.GETITEM)
|
|
1131
|
+
return self
|
|
1132
|
+
|
|
1133
|
+
def setitem(self) -> Self:
|
|
1134
|
+
"""
|
|
1135
|
+
Pops the two topmost items from the stack, and calls `third[second] = top`.
|
|
1136
|
+
"""
|
|
1137
|
+
self._instr(OpCode.SETITEM)
|
|
1138
|
+
return self
|
|
1139
|
+
|
|
1140
|
+
def delitem(self) -> Self:
|
|
1141
|
+
"""
|
|
1142
|
+
Pops the topmost item on the stack, and calls `del second[top]`.
|
|
1143
|
+
"""
|
|
1144
|
+
self._instr(OpCode.DELITEM)
|
|
1145
|
+
return self
|
|
1146
|
+
|
|
1147
|
+
def forward(self) -> sentinel:
|
|
1148
|
+
"""
|
|
1149
|
+
Creates a forward reference, which can be passed into jump and call instructions.
|
|
1150
|
+
|
|
1151
|
+
The forward reference can later be bound using `label()`.
|
|
1152
|
+
|
|
1153
|
+
:return: A forward reference object that can be used as a jump target or later bound.
|
|
1154
|
+
"""
|
|
1155
|
+
return sentinel("FORWARD")
|
|
1156
|
+
|
|
1157
|
+
def jump(self, target: int | sentinel) -> Self:
|
|
1158
|
+
"""
|
|
1159
|
+
Jumps to just before the instruction indicated by the target location.
|
|
1160
|
+
|
|
1161
|
+
If `target` is a sentinel, it will be replaced with the appropriate location when it is bound.
|
|
1162
|
+
|
|
1163
|
+
:param target: The location of the instruction or the forward reference to jump to.
|
|
1164
|
+
"""
|
|
1165
|
+
if isinstance(target, sentinel) and target in self._forward_refs:
|
|
1166
|
+
target = self._forward_refs[target]
|
|
1167
|
+
|
|
1168
|
+
if isinstance(target, int) and target < 0:
|
|
1169
|
+
raise ValueError("Jump destination cannot be negative")
|
|
1170
|
+
|
|
1171
|
+
self._instr(OpCode.JUMP, target)
|
|
1172
|
+
return self
|
|
1173
|
+
|
|
1174
|
+
def jump_false(self, target: int | sentinel) -> Self:
|
|
1175
|
+
"""
|
|
1176
|
+
Pops the topmost item on the stack,
|
|
1177
|
+
and jumps to just before the instruction indicated by the target location if it is falsy.
|
|
1178
|
+
|
|
1179
|
+
If `target` is a sentinel, it will be replaced with the appropriate location when it is bound.
|
|
1180
|
+
|
|
1181
|
+
:param target: The location of the instruction or the forward reference to jump to.
|
|
1182
|
+
"""
|
|
1183
|
+
if isinstance(target, sentinel) and target in self._forward_refs:
|
|
1184
|
+
target = self._forward_refs[target]
|
|
1185
|
+
|
|
1186
|
+
if isinstance(target, int) and target < 0:
|
|
1187
|
+
raise ValueError("Jump destination cannot be negative")
|
|
1188
|
+
|
|
1189
|
+
self._instr(OpCode.JUMP_FALSE, target)
|
|
1190
|
+
return self
|
|
1191
|
+
|
|
1192
|
+
def jump_true(self, target: int | sentinel) -> Self:
|
|
1193
|
+
"""
|
|
1194
|
+
Pops the topmost item on the stack,
|
|
1195
|
+
and jumps to just before the instruction indicated by the target location if it is truthy.
|
|
1196
|
+
|
|
1197
|
+
If `target` is a sentinel, it will be replaced with the appropriate location when it is bound.
|
|
1198
|
+
|
|
1199
|
+
:param target: The location of the instruction or the forward reference to jump to.
|
|
1200
|
+
"""
|
|
1201
|
+
if isinstance(target, sentinel) and target in self._forward_refs:
|
|
1202
|
+
target = self._forward_refs[target]
|
|
1203
|
+
|
|
1204
|
+
if isinstance(target, int) and target < 0:
|
|
1205
|
+
raise ValueError("Jump destination cannot be negative")
|
|
1206
|
+
|
|
1207
|
+
self._instr(OpCode.JUMP_TRUE, target)
|
|
1208
|
+
return self
|
|
1209
|
+
|
|
1210
|
+
def jump_indirect(self) -> Self:
|
|
1211
|
+
"""
|
|
1212
|
+
Pops the topmost item on the stack,
|
|
1213
|
+
and jumps to just before the instruction at the location indicated by the value of the popped item.
|
|
1214
|
+
"""
|
|
1215
|
+
self._instr(OpCode.JUMP_INDIRECT)
|
|
1216
|
+
return self
|
|
1217
|
+
|
|
1218
|
+
def call(self, target: int | sentinel, args: int = 0) -> Self:
|
|
1219
|
+
"""
|
|
1220
|
+
Creates a new frame that starts with its instruction pointer at the instruction with location `target`.
|
|
1221
|
+
|
|
1222
|
+
This frame is given a separate operand stack, with only the top `args` items
|
|
1223
|
+
of the current stack, popped and passed in as arguments.
|
|
1224
|
+
|
|
1225
|
+
The frame has its own set of local variables, though it inherits any global variables from the current frame.
|
|
1226
|
+
However, if these global variables are then assigned in the new frame, they will be local unless redeclared as global there.
|
|
1227
|
+
|
|
1228
|
+
When a `RETURN` instruction is executed in the new frame, that frame will be discarded
|
|
1229
|
+
and the return value will be pushed onto the stack of the previous frame.
|
|
1230
|
+
|
|
1231
|
+
:param target: The location of the instruction or forward reference to start the new frame at.
|
|
1232
|
+
:param args: The number of arguments to pass to the new frame. Defaults to 0.
|
|
1233
|
+
"""
|
|
1234
|
+
if isinstance(target, sentinel) and target in self._forward_refs:
|
|
1235
|
+
target = self._forward_refs[target]
|
|
1236
|
+
|
|
1237
|
+
if isinstance(target, int) and target < 0:
|
|
1238
|
+
raise ValueError("Call target cannot be negative")
|
|
1239
|
+
|
|
1240
|
+
self._instr(OpCode.CALL, target, args)
|
|
1241
|
+
return self
|
|
1242
|
+
|
|
1243
|
+
def call_indirect(self, args: int = 0) -> Self:
|
|
1244
|
+
"""
|
|
1245
|
+
Pops the topmost item of the stack, and creates a new frame that starts
|
|
1246
|
+
with its instruction pointer at the location indicated by the value of the popped item.
|
|
1247
|
+
|
|
1248
|
+
This frame is given a separate operand stack, with only the top `args` items
|
|
1249
|
+
of the current stack, popped and passed in as arguments.
|
|
1250
|
+
|
|
1251
|
+
The frame has its own set of local variables, though it inherits any global variables from the current frame.
|
|
1252
|
+
However, if these global variables are then assigned in the new frame, they will be local unless redeclared as global there.
|
|
1253
|
+
|
|
1254
|
+
When a `RETURN` instruction is executed in the new frame, that frame will be discarded
|
|
1255
|
+
and the return value will be pushed onto the stack of the previous frame.
|
|
1256
|
+
|
|
1257
|
+
:param target: The location of the instruction or forward reference to start the new frame at.
|
|
1258
|
+
:param args: The number of arguments to pass to the new frame. Defaults to 0.
|
|
1259
|
+
"""
|
|
1260
|
+
self._instr(OpCode.CALL_INDIRECT, 0, args)
|
|
1261
|
+
return self
|
|
1262
|
+
|
|
1263
|
+
def label(self, forward: sentinel | None = None) -> int:
|
|
1264
|
+
"""
|
|
1265
|
+
Returns the index of the next instruction, used for creating labels for `.jump()` and `.call()`.
|
|
1266
|
+
|
|
1267
|
+
If a forward reference is passed, it will also bind that forward reference here.
|
|
1268
|
+
|
|
1269
|
+
:param forward: The forward reference to bind, if any
|
|
1270
|
+
"""
|
|
1271
|
+
loc = len(self._instructions)
|
|
1272
|
+
|
|
1273
|
+
if isinstance(forward, sentinel):
|
|
1274
|
+
if forward in self._forward_refs:
|
|
1275
|
+
raise ValueError("Forward reference has already been bound")
|
|
1276
|
+
|
|
1277
|
+
self._forward_refs[forward] = loc
|
|
1278
|
+
for index, instr in enumerate(self._instructions):
|
|
1279
|
+
if instr.arg is forward:
|
|
1280
|
+
self._instructions[index] = Instruction(instr.opcode, loc, instr.arg2)
|
|
1281
|
+
|
|
1282
|
+
return loc
|
|
1283
|
+
|
|
1284
|
+
@contextmanager
|
|
1285
|
+
def block(
|
|
1286
|
+
self, start: sentinel | None = None, stop: sentinel | None = None
|
|
1287
|
+
) -> Generator[tuple[int, sentinel], None, None]:
|
|
1288
|
+
"""
|
|
1289
|
+
Returns a label and a forward reference, bound to the start and end of the block, respectively.
|
|
1290
|
+
|
|
1291
|
+
If a forward reference sentinel is passed into `start` or `stop`,
|
|
1292
|
+
those will be bound instead of creating a new label or a new forward reference.
|
|
1293
|
+
"""
|
|
1294
|
+
if stop is None:
|
|
1295
|
+
stop = self.forward()
|
|
1296
|
+
yield self.label(start) if isinstance(start, sentinel) else self.label(), stop
|
|
1297
|
+
self.label(stop)
|
|
1298
|
+
|
|
1299
|
+
def now(self) -> Self:
|
|
1300
|
+
"""
|
|
1301
|
+
Pushes the current time in seconds since the UNIX epoch onto the stack.
|
|
1302
|
+
"""
|
|
1303
|
+
self._instr(OpCode.NOW)
|
|
1304
|
+
return self
|
|
1305
|
+
|
|
1306
|
+
def often(self) -> Self:
|
|
1307
|
+
"""
|
|
1308
|
+
Pushes `True` onto the stack with 3/4 probability and `False` with 1/4 probability.
|
|
1309
|
+
"""
|
|
1310
|
+
self._instr(OpCode.OFTEN)
|
|
1311
|
+
return self
|
|
1312
|
+
|
|
1313
|
+
def seldom(self) -> Self:
|
|
1314
|
+
"""
|
|
1315
|
+
Pushes `True` onto the stack with 1/6 probability and `False` with 5/6 probability.
|
|
1316
|
+
"""
|
|
1317
|
+
self._instr(OpCode.SELDOM)
|
|
1318
|
+
return self
|
|
1319
|
+
|
|
1320
|
+
def input(self) -> Self:
|
|
1321
|
+
"""
|
|
1322
|
+
Reads a line from standard input and pushes it onto the stack.
|
|
1323
|
+
"""
|
|
1324
|
+
self._instr(OpCode.INPUT)
|
|
1325
|
+
return self
|
|
1326
|
+
|
|
1327
|
+
def print(self) -> Self:
|
|
1328
|
+
"""
|
|
1329
|
+
Writes the topmost item on the stack to standard output.
|
|
1330
|
+
"""
|
|
1331
|
+
self._instr(OpCode.PRINT)
|
|
1332
|
+
return self
|
|
1333
|
+
|
|
1334
|
+
def open(self, mode: str = "r") -> Self:
|
|
1335
|
+
"""
|
|
1336
|
+
Pops the topmost item on the stack, and opens the file indicated by its value, taken as a file path.
|
|
1337
|
+
|
|
1338
|
+
:param mode: The mode to open the file with, as passed into `open()`. Defaults to `r`.
|
|
1339
|
+
"""
|
|
1340
|
+
try:
|
|
1341
|
+
self._instr(OpCode.OPEN, FILE_MODES.index(mode))
|
|
1342
|
+
except ValueError:
|
|
1343
|
+
raise ValueError(f"Disallowed file mode {mode!r}") from None
|
|
1344
|
+
return self
|
|
1345
|
+
|
|
1346
|
+
def read(self, length: int = -1) -> Self:
|
|
1347
|
+
"""
|
|
1348
|
+
Reads `length` characters or bytes from file on top of the stack, pushing the result to the stack.
|
|
1349
|
+
|
|
1350
|
+
:param length: The number of characters or bytes to read.
|
|
1351
|
+
If `-1` (as is default), reads all remaining contents of the file.
|
|
1352
|
+
"""
|
|
1353
|
+
self._instr(OpCode.READ, length)
|
|
1354
|
+
return self
|
|
1355
|
+
|
|
1356
|
+
def write(self) -> Self:
|
|
1357
|
+
"""
|
|
1358
|
+
Pops the topmost item on the stack, and writes it to the file below it on the stack.
|
|
1359
|
+
"""
|
|
1360
|
+
self._instr(OpCode.WRITE)
|
|
1361
|
+
return self
|
|
1362
|
+
|
|
1363
|
+
def close(self) -> Self:
|
|
1364
|
+
"""
|
|
1365
|
+
Pops the file on top of the stack and closes it.
|
|
1366
|
+
"""
|
|
1367
|
+
self._instr(OpCode.CLOSE)
|
|
1368
|
+
return self
|
|
1369
|
+
|
|
1370
|
+
def size(self) -> Self:
|
|
1371
|
+
"""
|
|
1372
|
+
Pushes the current size of the operand stack onto the stack.
|
|
1373
|
+
The additional item pushed by this command is not included.
|
|
1374
|
+
"""
|
|
1375
|
+
self._instr(OpCode.SIZE)
|
|
1376
|
+
return self
|
|
1377
|
+
|
|
1378
|
+
def locate(self) -> Self:
|
|
1379
|
+
"""
|
|
1380
|
+
Pushes the index of this instruction onto the stack.
|
|
1381
|
+
"""
|
|
1382
|
+
self._instr(OpCode.LOCATE)
|
|
1383
|
+
return self
|
|
1384
|
+
|
|
1385
|
+
def error(self) -> Self:
|
|
1386
|
+
"""
|
|
1387
|
+
Raises an `InstructedError` when this instruction is executed.
|
|
1388
|
+
This will terminate the program, as this exception cannot be caught inside the virtual machine.
|
|
1389
|
+
|
|
1390
|
+
If the stack is not empty, the topmost value on the stack will be used as the exception message.
|
|
1391
|
+
"""
|
|
1392
|
+
self._instr(OpCode.ERROR)
|
|
1393
|
+
return self
|
|
1394
|
+
|
|
1395
|
+
def ret(self) -> Self:
|
|
1396
|
+
"""
|
|
1397
|
+
Returns the topmost value on the stack, or None if the stack is empty.
|
|
1398
|
+
"""
|
|
1399
|
+
self._instr(OpCode.RETURN)
|
|
1400
|
+
return self
|
|
1401
|
+
|
|
1402
|
+
def show(self) -> Self:
|
|
1403
|
+
"""
|
|
1404
|
+
Prints the current operand stack to the console, intended for debugging.
|
|
1405
|
+
"""
|
|
1406
|
+
self._instr(OpCode.SHOW)
|
|
1407
|
+
return self
|