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/_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