flowscript-vm 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.
flowscript/vm.py ADDED
@@ -0,0 +1,577 @@
1
+ """Reference FlowScript virtual machine (Python).
2
+
3
+ Behaviour is normative: the C VM (c/flowscript.c) must produce identical traces.
4
+ """
5
+
6
+ import struct
7
+
8
+ from .bytecode import Program, BY_CODE, OPERAND_SIZE, EV_BOOT, EV_ENTER, EV_EXIT, FAULTS
9
+ from .intops import wrap, DivZero
10
+ from . import values as V
11
+ from .values import INT, FLOAT, STR, TIME, NONE_TYPE, Str, Time
12
+
13
+ STACK_SIZE = 64
14
+ MAX_FRAMES = 16
15
+ QUEUE_SIZE = 16
16
+ BUDGET = 10000
17
+ MAX_CHAIN = 256 # dispatches in a row without the VM becoming idle
18
+ STR_SLOTS = V.STR_SLOTS # run-time strings alive at once
19
+ STR_LEN = V.STR_LEN # bytes per run-time string
20
+
21
+ # step() results
22
+ IDLE = "idle" # nothing to do
23
+ DISPATCHED = "dispatch" # a handler was started (no instruction executed)
24
+ RUNNING = "running" # an instruction was executed, handler still active
25
+ DONE = "done" # an instruction was executed and the handler finished
26
+ FAULT = "fault" # the instruction faulted, handler aborted
27
+
28
+
29
+ class Fault(Exception):
30
+ def __init__(self, code):
31
+ super().__init__(FAULTS.get(code, "fault"))
32
+ self.code = code
33
+
34
+
35
+ class VM:
36
+ """
37
+ externs: dict name -> callable(vm, args) -> value. Values are int, float, values.Str
38
+ (.b bytes, .text) and values.Time (.ms); a str extern may also return a
39
+ Python str/bytes. An extern declared without a return type may return anything.
40
+ clock: callable(vm) -> int ms since 1970 (UTC) for now(); default: always 0
41
+ on_emit: callable(vm, event_name, args) or None
42
+ on_fault: callable(vm, code, pc) or None
43
+ on_timer: callable(vm, timer, start, ms, periodic) or None. When set the host runs the
44
+ timers: the VM asks it to start/stop them and the host calls
45
+ timer_expired() when one fires; tick() then does nothing.
46
+ """
47
+
48
+ def __init__(self, prog, externs=None, on_emit=None, on_fault=None, on_timer=None,
49
+ stack_size=STACK_SIZE, max_frames=MAX_FRAMES, queue_size=QUEUE_SIZE, budget=BUDGET,
50
+ max_chain=MAX_CHAIN, str_slots=STR_SLOTS, str_len=STR_LEN, clock=None):
51
+ if isinstance(prog, (bytes, bytearray)):
52
+ prog = Program.from_bytes(prog)
53
+ self.prog = prog
54
+ self.code = prog.code
55
+ self.externs = externs or {}
56
+ self.on_emit = on_emit
57
+ self.on_fault = on_fault
58
+ self.on_timer = on_timer
59
+ self.stack_size = stack_size
60
+ self.max_frames = max_frames
61
+ self.queue_size = queue_size
62
+ self.budget = budget
63
+ self.max_chain = max_chain
64
+ self.str_slots = str_slots
65
+ self.str_len = str_len
66
+ self.clock = clock
67
+ self.bound = []
68
+ for ext in prog.externs:
69
+ self.bound.append(self.externs.get(ext[0]))
70
+ self.reset()
71
+
72
+ def unbound_externs(self):
73
+ return [self.prog.externs[i][0] for i, f in enumerate(self.bound) if f is None]
74
+
75
+ def reset(self):
76
+ for i, t in enumerate(getattr(self, "timers", [])):
77
+ if t[0] and self.on_timer:
78
+ self.on_timer(self, i, 0, 0, 0)
79
+ self.globals = list(self.prog.globals)
80
+ self.pool = [None] * self.str_slots # run-time strings: slot -> bytes, None = free
81
+ self.stack = []
82
+ self.frames = [] # [return_pc, base]
83
+ self.pc = 0
84
+ self.queue = [] # (event, args)
85
+ self.state = None # current state index or None
86
+ self.pending = None # requested goto_state target
87
+ self.transition = None # target being entered (after exit handler)
88
+ self.in_exit = False
89
+ self.handler = None # active handler tuple
90
+ self.steps = 0 # instructions executed in this handler
91
+ # active, periodic, period, remaining, owner (state whose handler started it, or None)
92
+ self.timers = [[False, False, 0, 0, None] for _ in self.prog.timers]
93
+ self.boot_pending = False
94
+ self.cur_pc = -1
95
+ self.faults = 0
96
+ self.chain = 0 # dispatches since the VM was last idle
97
+
98
+ # -- host API ------------------------------------------------------------------
99
+ def start(self):
100
+ """Queue boot and the entry into the initial state."""
101
+ self.boot_pending = True
102
+ if self.prog.states:
103
+ self.pending = 0
104
+ return self
105
+
106
+ def post(self, event, *args):
107
+ """Queue an event. Arguments are converted to the event's parameter types (an int
108
+ is accepted for a float, a Python str/bytes for a str). Returns False if the queue
109
+ or the string memory is full (a fault is reported). Raises TypeError for a wrong type."""
110
+ if isinstance(event, str):
111
+ idx = self.prog.event_index(event)
112
+ if idx < 0:
113
+ raise KeyError("unknown event %r" % event)
114
+ event = idx
115
+ pc = self.cur_pc if self.handler is not None else -1
116
+ if len(self.queue) >= self.queue_size:
117
+ self.report_fault(7, pc)
118
+ return False
119
+ _name, argc, ptypes = self.prog.events[event]
120
+ entry = (event, [0] * argc)
121
+ self.queue.append(entry) # a root while its strings are allocated
122
+ try:
123
+ for i in range(argc):
124
+ v = args[i] if i < len(args) else V.zero(ptypes[i])
125
+ entry[1][i] = self.host_value(v, ptypes[i])
126
+ except V.ValueFault as f:
127
+ self.queue.remove(entry)
128
+ if f.code == V.TYPE_MISMATCH:
129
+ raise TypeError("event %s: argument has the wrong type" % _name)
130
+ self.report_fault(f.code, pc)
131
+ return False
132
+ return True
133
+
134
+ def host_value(self, v, t):
135
+ """A value from the host as type t (allocates a str). Raises ValueFault."""
136
+ if t == STR and isinstance(v, (str, bytes)):
137
+ return self.alloc(v.encode("utf-8") if isinstance(v, str) else v)
138
+ try:
139
+ vt = V.type_of(v)
140
+ except TypeError:
141
+ raise V.ValueFault(V.TYPE_MISMATCH)
142
+ if t == FLOAT and vt == INT:
143
+ return float(v)
144
+ if vt != t:
145
+ raise V.ValueFault(V.TYPE_MISMATCH)
146
+ if vt == INT:
147
+ return wrap(int(v))
148
+ if vt == STR and v.slot is not None and self.pool[v.slot] is None:
149
+ raise V.ValueFault(V.TYPE_MISMATCH) # a stale run-time string
150
+ return v
151
+
152
+ def set_global(self, index, value):
153
+ """Debugger/host: set a global (converted to its current type)."""
154
+ self.globals[index] = self.host_value(value, V.type_of(self.globals[index]))
155
+
156
+ # -- strings -------------------------------------------------------------------------
157
+ def alloc(self, b):
158
+ """A new run-time string. Raises ValueFault 12 (too long) or 11 (no free slot)."""
159
+ if len(b) > self.str_len:
160
+ raise V.ValueFault(V.STRING_TOO_LONG)
161
+ for attempt in (0, 1):
162
+ for i, used in enumerate(self.pool):
163
+ if used is None:
164
+ self.pool[i] = b
165
+ return Str(b, i)
166
+ if attempt == 0:
167
+ self.collect()
168
+ raise V.ValueFault(V.OUT_OF_STRINGS)
169
+
170
+ def collect(self):
171
+ """Mark and sweep: free every slot not referenced by a global, the stack or the queue."""
172
+ live = set()
173
+ for v in self.globals + self.stack:
174
+ if isinstance(v, Str) and v.slot is not None:
175
+ live.add(v.slot)
176
+ for _ev, args in self.queue:
177
+ for v in args:
178
+ if isinstance(v, Str) and v.slot is not None:
179
+ live.add(v.slot)
180
+ for i in range(len(self.pool)):
181
+ if i not in live:
182
+ self.pool[i] = None
183
+
184
+ def strings_in_use(self):
185
+ return sum(1 for b in self.pool if b is not None)
186
+
187
+ def timer_index(self, timer):
188
+ if isinstance(timer, str):
189
+ for i, ev in enumerate(self.prog.timers):
190
+ if self.prog.events[ev][0] == timer:
191
+ return i
192
+ raise KeyError("unknown timer %r" % timer)
193
+ return timer
194
+
195
+ def timer_expired(self, timer):
196
+ """Host-managed timers: the host's timer fired. Returns True if the event was queued."""
197
+ t = self.timers[self.timer_index(timer)]
198
+ if not t[0]:
199
+ return False # stopped by the script meanwhile: ignore
200
+ if not t[1]:
201
+ t[0] = False
202
+ return self.post(self.prog.timers[self.timer_index(timer)])
203
+
204
+ def stop_timer(self, i):
205
+ if self.timers[i][0]:
206
+ self.timers[i][0] = False
207
+ if self.on_timer:
208
+ self.on_timer(self, i, 0, 0, 0)
209
+
210
+ def tick(self, ms):
211
+ if self.on_timer:
212
+ return
213
+ for i, t in enumerate(self.timers):
214
+ if not t[0]:
215
+ continue
216
+ t[3] -= ms
217
+ if t[3] <= 0:
218
+ self.post(self.prog.timers[i])
219
+ if t[1]:
220
+ t[3] += t[2]
221
+ if t[3] <= 0:
222
+ t[3] = t[2]
223
+ else:
224
+ t[0] = False
225
+
226
+ def run(self, max_steps=1000000):
227
+ """Run until idle. Returns number of steps."""
228
+ n = 0
229
+ while n < max_steps:
230
+ r = self.step()
231
+ if r == IDLE:
232
+ break
233
+ n += 1
234
+ return n
235
+
236
+ @property
237
+ def active(self):
238
+ return self.handler is not None
239
+
240
+ # -- dispatch --------------------------------------------------------------------
241
+ def begin(self, h, args):
242
+ _st, _ev, argc, nlocals, off = h
243
+ self.handler = h
244
+ self.stack = list(args[:argc]) + [0] * (argc - len(args))
245
+ self.stack += [0] * (nlocals - len(self.stack))
246
+ self.frames = [[-1, 0]]
247
+ self.pc = off
248
+ self.steps = 0
249
+
250
+ def dispatch(self):
251
+ """Start the next handler. Returns True if one was started or a transition progressed."""
252
+ p = self.prog
253
+ if self.boot_pending:
254
+ self.boot_pending = False
255
+ h = p.find_handler(None, EV_BOOT)
256
+ if h is not None:
257
+ self.begin(h, [])
258
+ return True
259
+ if self.pending is not None:
260
+ self.transition = self.pending
261
+ self.pending = None
262
+ if self.state is not None:
263
+ h = p.find_handler(self.state, EV_EXIT)
264
+ if h is not None and h[0] == self.state:
265
+ self.in_exit = True
266
+ self.begin(h, [])
267
+ return True
268
+ if self.transition is not None:
269
+ if self.state is not None: # leaving: stop the old state's timers
270
+ for i, t in enumerate(self.timers):
271
+ if t[4] == self.state:
272
+ self.stop_timer(i)
273
+ self.state = self.transition
274
+ self.transition = None
275
+ h = p.find_handler(self.state, EV_ENTER)
276
+ if h is not None and h[0] == self.state:
277
+ self.begin(h, [])
278
+ return True
279
+ while self.queue:
280
+ ev, args = self.queue.pop(0)
281
+ h = p.find_handler(self.state, ev)
282
+ if h is not None:
283
+ self.begin(h, args)
284
+ return True
285
+ return False
286
+
287
+ def finish(self):
288
+ self.handler = None
289
+ self.frames = []
290
+ self.stack = []
291
+ self.in_exit = False
292
+
293
+ def report_fault(self, code, pc):
294
+ self.faults += 1
295
+ if self.on_fault:
296
+ self.on_fault(self, code, pc)
297
+
298
+ # -- execution -------------------------------------------------------------------
299
+ def has_work(self):
300
+ return (self.boot_pending or self.pending is not None or self.transition is not None
301
+ or bool(self.queue))
302
+
303
+ def step(self):
304
+ if self.handler is None:
305
+ if self.chain >= self.max_chain and self.has_work():
306
+ # the script keeps itself busy (goto_state/emit loop): drop the pending work
307
+ self.pending = self.transition = None
308
+ self.boot_pending = False
309
+ self.queue = []
310
+ self.chain = 0
311
+ self.report_fault(9, -1)
312
+ return IDLE
313
+ if self.dispatch():
314
+ self.chain += 1
315
+ return DISPATCHED
316
+ self.chain = 0
317
+ return IDLE
318
+ try:
319
+ return self.execute()
320
+ except Fault as f:
321
+ self.report_fault(f.code, self.cur_pc)
322
+ self.finish()
323
+ return FAULT
324
+
325
+ def pop(self):
326
+ if not self.stack or len(self.stack) <= 0:
327
+ raise Fault(2)
328
+ return self.stack.pop()
329
+
330
+ def push(self, v):
331
+ if len(self.stack) >= self.stack_size:
332
+ raise Fault(1)
333
+ self.stack.append(v)
334
+
335
+ def execute(self):
336
+ code = self.code
337
+ pc = self.pc
338
+ self.cur_pc = pc
339
+ self.steps += 1
340
+ if self.steps > self.budget:
341
+ raise Fault(4)
342
+ if pc >= len(code):
343
+ raise Fault(5)
344
+ op = code[pc]
345
+ ent = BY_CODE.get(op)
346
+ if ent is None:
347
+ raise Fault(5)
348
+ name, kind = ent
349
+ if pc + OPERAND_SIZE[kind] >= len(code): # operand cut off by end of code
350
+ raise Fault(5)
351
+ if kind in ("f64", "i64"):
352
+ arg = struct.unpack_from("<d" if kind == "f64" else "<q", code, pc + 1)[0]
353
+ npc = pc + 9
354
+ elif kind is None:
355
+ arg = None
356
+ npc = pc + 1
357
+ elif kind == "u16":
358
+ arg = code[pc + 1] | (code[pc + 2] << 8)
359
+ npc = pc + 3
360
+ elif kind == "u8":
361
+ arg = code[pc + 1]
362
+ npc = pc + 2
363
+ elif kind == "i8":
364
+ arg = struct.unpack_from("<b", code, pc + 1)[0]
365
+ npc = pc + 2
366
+ elif kind == "i32":
367
+ arg = struct.unpack_from("<i", code, pc + 1)[0]
368
+ npc = pc + 5
369
+ self.pc = npc
370
+ push, pop = self.push, self.pop
371
+
372
+ if name in ("PUSH", "PUSH8"):
373
+ push(arg)
374
+ elif name == "PUSHF":
375
+ push(arg)
376
+ elif name == "PUSHT":
377
+ push(Time(arg))
378
+ elif name == "PUSHS":
379
+ if arg >= len(self.prog.strings):
380
+ raise Fault(5)
381
+ push(Str(self.prog.strings[arg].encode("utf-8", "surrogateescape")))
382
+ elif name == "POP":
383
+ pop()
384
+ elif name == "DUP":
385
+ v = pop()
386
+ push(v)
387
+ push(v)
388
+ elif name == "LOADG":
389
+ if arg >= len(self.globals):
390
+ raise Fault(5)
391
+ push(self.globals[arg])
392
+ elif name == "STOREG":
393
+ if arg >= len(self.globals):
394
+ raise Fault(5)
395
+ self.globals[arg] = pop()
396
+ elif name == "LOADL":
397
+ i = self.frames[-1][1] + arg
398
+ if i >= len(self.stack):
399
+ raise Fault(5)
400
+ push(self.stack[i])
401
+ elif name == "STOREL":
402
+ v = pop()
403
+ i = self.frames[-1][1] + arg
404
+ if i >= len(self.stack):
405
+ raise Fault(5)
406
+ self.stack[i] = v
407
+ elif name in BINOPS:
408
+ if len(self.stack) < 2:
409
+ raise Fault(2)
410
+ a, b = self.stack[-2], self.stack[-1]
411
+ op = BINOPS[name]
412
+ ta, tb = V.type_of(a), V.type_of(b)
413
+ if op in ("&", "|", "^", "<<", ">>") and not (ta == tb == INT):
414
+ raise Fault(10)
415
+ if (name == "CONCAT") != (ta == STR and tb == STR and op == "+"):
416
+ raise Fault(10) # strings are joined by CONCAT, numbers by ADD
417
+ try:
418
+ r = V.binary(op, a, b)
419
+ except DivZero:
420
+ raise Fault(3)
421
+ except V.ValueFault as f:
422
+ raise Fault(f.code)
423
+ if isinstance(r, bytes):
424
+ r = self.alloc_or_fault(r) # the operands are still on the stack (roots)
425
+ del self.stack[-2:]
426
+ push(r)
427
+ elif name == "NEG":
428
+ v = pop()
429
+ t = V.type_of(v)
430
+ if t == INT:
431
+ push(wrap(-v))
432
+ elif t == FLOAT:
433
+ push(-v)
434
+ else:
435
+ raise Fault(10)
436
+ elif name == "BNOT":
437
+ push(wrap(~self.pop_int()))
438
+ elif name == "NOT":
439
+ push(0 if self.pop_int() else 1)
440
+ elif name == "BOOL":
441
+ push(1 if self.pop_int() else 0)
442
+ elif name == "JMP":
443
+ self.pc = arg
444
+ elif name == "JZ":
445
+ if self.pop_int() == 0:
446
+ self.pc = arg
447
+ elif name == "JNZ":
448
+ if self.pop_int() != 0:
449
+ self.pc = arg
450
+ elif name == "CALL":
451
+ if arg >= len(self.prog.funcs):
452
+ raise Fault(5)
453
+ _n, argc, nlocals, off = self.prog.funcs[arg]
454
+ if len(self.frames) >= self.max_frames:
455
+ raise Fault(6)
456
+ base = len(self.stack) - argc
457
+ if base < 0:
458
+ raise Fault(2)
459
+ for _ in range(nlocals - argc):
460
+ push(0)
461
+ self.frames.append([npc, base])
462
+ self.pc = off
463
+ elif name in ("RET", "RET0"):
464
+ v = pop() if name == "RET" else 0
465
+ ret_pc, base = self.frames.pop()
466
+ del self.stack[base:]
467
+ if not self.frames:
468
+ self.finish()
469
+ return DONE
470
+ self.pc = ret_pc
471
+ push(v)
472
+ elif name == "XCALL":
473
+ if arg >= len(self.bound):
474
+ raise Fault(5)
475
+ _xn, argc, ptypes, ret = self.prog.externs[arg]
476
+ if len(self.stack) < argc:
477
+ raise Fault(2)
478
+ args = self.stack[len(self.stack) - argc:] if argc else []
479
+ self.check_types(args, ptypes)
480
+ fn = self.bound[arg]
481
+ if fn is None:
482
+ raise Fault(8)
483
+ r = fn(self, list(args)) # the arguments stay on the stack (roots) meanwhile
484
+ if ret == NONE_TYPE:
485
+ r = 0
486
+ else:
487
+ try:
488
+ r = self.host_value(r, ret)
489
+ except V.ValueFault as f:
490
+ raise Fault(f.code)
491
+ del self.stack[len(self.stack) - argc:]
492
+ push(r)
493
+ elif name == "BUILTIN":
494
+ if arg >= len(V.BUILTINS):
495
+ raise Fault(5)
496
+ _bn, ptypes, _ret = V.BUILTINS[arg]
497
+ argc = len(ptypes)
498
+ if len(self.stack) < argc:
499
+ raise Fault(2)
500
+ args = self.stack[len(self.stack) - argc:] if argc else []
501
+ self.check_types(args, ptypes)
502
+ try:
503
+ r = V.call_builtin(arg, args, (lambda: self.clock(self)) if self.clock else None)
504
+ except V.ValueFault as f:
505
+ raise Fault(f.code)
506
+ if isinstance(r, bytes):
507
+ r = self.alloc_or_fault(r)
508
+ del self.stack[len(self.stack) - argc:]
509
+ push(r)
510
+ elif name == "GOTO":
511
+ if arg >= len(self.prog.states):
512
+ raise Fault(5)
513
+ if not self.in_exit:
514
+ self.pending = arg
515
+ self.finish()
516
+ return DONE
517
+ elif name == "EMIT":
518
+ if arg >= len(self.prog.events):
519
+ raise Fault(5)
520
+ _en, argc, ptypes = self.prog.events[arg]
521
+ if len(self.stack) < argc:
522
+ raise Fault(2)
523
+ args = list(self.stack[len(self.stack) - argc:]) if argc else []
524
+ self.check_types(args, ptypes)
525
+ if self.on_emit:
526
+ self.on_emit(self, self.prog.events[arg][0], list(args))
527
+ self.post(arg, *args)
528
+ del self.stack[len(self.stack) - argc:]
529
+ elif name in ("TSTART", "TEVERY"):
530
+ if arg >= len(self.timers):
531
+ raise Fault(5)
532
+ ms = self.pop_int()
533
+ t = self.timers[arg]
534
+ t[0] = True
535
+ t[1] = name == "TEVERY"
536
+ t[2] = ms
537
+ t[3] = ms
538
+ t[4] = self.handler[0]
539
+ if self.on_timer:
540
+ self.on_timer(self, arg, 1, ms, 1 if t[1] else 0)
541
+ elif name == "TSTOP":
542
+ if arg >= len(self.timers):
543
+ raise Fault(5)
544
+ self.stop_timer(arg)
545
+ elif name == "NOP":
546
+ pass
547
+ else:
548
+ raise Fault(5)
549
+ return RUNNING
550
+
551
+ def pop_int(self):
552
+ v = self.pop()
553
+ if V.type_of(v) != INT:
554
+ raise Fault(10)
555
+ return v
556
+
557
+ def check_types(self, args, ptypes):
558
+ for v, t in zip(args, ptypes):
559
+ if V.type_of(v) != t:
560
+ raise Fault(10)
561
+
562
+ def alloc_or_fault(self, b):
563
+ try:
564
+ return self.alloc(b)
565
+ except V.ValueFault as f:
566
+ raise Fault(f.code)
567
+
568
+ # -- introspection -----------------------------------------------------------------
569
+ def state_name(self):
570
+ return None if self.state is None else self.prog.states[self.state]
571
+
572
+
573
+ BINOPS = {
574
+ "ADD": "+", "CONCAT": "+", "SUB": "-", "MUL": "*", "DIV": "/", "MOD": "%",
575
+ "BAND": "&", "BOR": "|", "BXOR": "^", "SHL": "<<", "SHR": ">>",
576
+ "EQ": "==", "NE": "!=", "LT": "<", "LE": "<=", "GT": ">", "GE": ">=",
577
+ }