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/bytecode.py ADDED
@@ -0,0 +1,372 @@
1
+ """Bytecode definitions, .fsb file format (encode/decode) and disassembler.
2
+
3
+ See SPEC.md section 8 for the normative description.
4
+ """
5
+
6
+ import struct
7
+
8
+ from .values import pack_value, unpack_value, TYPE_NAMES, NONE_TYPE, show, BUILTINS
9
+
10
+ MAGIC = b"FSB1"
11
+ FORMAT_VERSION = 2
12
+ FLAG_DEBUG = 1
13
+ # Features the program needs from the VM. A C VM built without one (FS_NO_FLOAT,
14
+ # FS_NO_TIME, FS_NO_STR) refuses images that set its flag.
15
+ FLAG_FLOAT = 2 # float values
16
+ FLAG_TIME = 4 # time values
17
+ FLAG_STR = 8 # strings made at run time (string slots); literals need no flag
18
+ FLAG_TEXT = 16 # text conversion of floats/times: str(float), str(time), float(s), time(s)
19
+ FEATURES = [(FLAG_FLOAT, "float", "FS_NO_FLOAT"), (FLAG_TIME, "time", "FS_NO_TIME"),
20
+ (FLAG_STR, "run-time strings", "FS_NO_STR"),
21
+ (FLAG_TEXT, "float/time text conversion", "FS_NO_TEXT")]
22
+ TEXT_BUILTINS = (1, 6, 7, 8) # float(str), str(float), str(time), time(str)
23
+ NONE16 = 0xFFFF
24
+
25
+ EV_BOOT, EV_ENTER, EV_EXIT = 0, 1, 2
26
+ BUILTIN_EVENTS = ["boot", "enter", "exit"]
27
+
28
+ # name -> (opcode, operand kind) operand kinds: None, i32, i8, u8, u16, f64, i64
29
+ OPCODES = {
30
+ "NOP": (0x00, None), "PUSH": (0x01, "i32"), "PUSH8": (0x02, "i8"), "PUSHS": (0x03, "u16"),
31
+ "POP": (0x04, None), "DUP": (0x05, None), "PUSHF": (0x06, "f64"), "PUSHT": (0x07, "i64"),
32
+ "LOADG": (0x10, "u16"), "STOREG": (0x11, "u16"), "LOADL": (0x12, "u8"), "STOREL": (0x13, "u8"),
33
+ "ADD": (0x20, None), "SUB": (0x21, None), "MUL": (0x22, None), "DIV": (0x23, None),
34
+ "MOD": (0x24, None), "NEG": (0x25, None), "BAND": (0x26, None), "BOR": (0x27, None),
35
+ "BXOR": (0x28, None), "BNOT": (0x29, None), "SHL": (0x2A, None), "SHR": (0x2B, None),
36
+ "CONCAT": (0x2C, None),
37
+ "EQ": (0x30, None), "NE": (0x31, None), "LT": (0x32, None), "LE": (0x33, None),
38
+ "GT": (0x34, None), "GE": (0x35, None), "NOT": (0x36, None), "BOOL": (0x37, None),
39
+ "JMP": (0x40, "u16"), "JZ": (0x41, "u16"), "JNZ": (0x42, "u16"),
40
+ "CALL": (0x50, "u16"), "RET": (0x51, None), "RET0": (0x52, None), "XCALL": (0x53, "u16"),
41
+ "BUILTIN": (0x54, "u8"),
42
+ "GOTO": (0x60, "u16"), "EMIT": (0x61, "u16"), "TSTART": (0x62, "u16"),
43
+ "TEVERY": (0x63, "u16"), "TSTOP": (0x64, "u16"),
44
+ }
45
+ BY_CODE = {code: (name, kind) for name, (code, kind) in OPCODES.items()}
46
+ OPERAND_SIZE = {None: 0, "i8": 1, "u8": 1, "u16": 2, "i32": 4, "f64": 8, "i64": 8}
47
+ OPERAND_FMT = {"i8": "<b", "u8": "<B", "u16": "<H", "i32": "<i", "f64": "<d", "i64": "<q"}
48
+
49
+ FAULTS = {
50
+ 1: "stack overflow", 2: "stack underflow", 3: "division by zero",
51
+ 4: "instruction budget exceeded", 5: "bad opcode", 6: "call depth exceeded",
52
+ 7: "event queue full", 8: "unbound extern", 9: "dispatch chain limit (script never idle)",
53
+ 10: "type mismatch", 11: "out of string memory", 12: "string too long", 13: "invalid value",
54
+ }
55
+
56
+
57
+ def pack_types(types):
58
+ """Parameter types, 2 bits each (int 0, float 1, str 2, time 3), first parameter lowest."""
59
+ v = 0
60
+ for i, t in enumerate(types):
61
+ v |= t << (2 * i)
62
+ return v
63
+
64
+
65
+ def unpack_types(v, n):
66
+ return tuple((v >> (2 * i)) & 3 for i in range(n))
67
+
68
+
69
+ class Program:
70
+ """In-memory representation of a compiled program (mirrors the .fsb file)."""
71
+
72
+ def __init__(self):
73
+ self.name = None
74
+ self.version = 0
75
+ self.strings = [] # list[str] (non-UTF-8 bytes kept with surrogateescape)
76
+ self.globals = [] # initial values (int, float, values.Str, values.Time)
77
+ self.externs = [] # (name, argc, param types, return type or NONE_TYPE)
78
+ self.events = [] # (name, argc, param types)
79
+ self.states = [] # name
80
+ self.timers = [] # event index
81
+ self.funcs = [] # (name, argc, nlocals, offset)
82
+ self.handlers = [] # (state|None, event, argc, nlocals, offset)
83
+ self.code = b""
84
+ # debug info (optional)
85
+ self.lines = [] # (pc, line, node)
86
+ self.global_names = []
87
+ # set by flowscript.compile(); None/empty for a program loaded from bytecode
88
+ self.source = None
89
+ self.ast = None
90
+ self.debug_info = None
91
+ self.warnings = []
92
+
93
+ # -- helpers -------------------------------------------------------------
94
+ def event_index(self, name):
95
+ for i, ev in enumerate(self.events):
96
+ if ev[0] == name:
97
+ return i
98
+ return -1
99
+
100
+ def state_index(self, name):
101
+ try:
102
+ return self.states.index(name)
103
+ except ValueError:
104
+ return -1
105
+
106
+ def find_handler(self, state, event):
107
+ glob = None
108
+ for h in self.handlers:
109
+ if h[1] != event:
110
+ continue
111
+ if h[0] == state and state is not None:
112
+ return h
113
+ if h[0] is None and glob is None:
114
+ glob = h
115
+ return glob
116
+
117
+ # -- serialisation ---------------------------------------------------------
118
+ def save(self, path, debug=False):
119
+ """Write the .fsb image (debug=True adds source lines and global names)."""
120
+ with open(path, "wb") as f:
121
+ f.write(self.to_bytes(debug))
122
+
123
+ def to_bytes(self, debug=False):
124
+ strings = list(self.strings)
125
+
126
+ def sidx(s):
127
+ if s is None:
128
+ return NONE16
129
+ if s not in strings:
130
+ strings.append(s)
131
+ return strings.index(s)
132
+
133
+ name_idx = sidx(self.name)
134
+ ext = b"".join(struct.pack("<HBBI", sidx(n), a, r, pack_types(ts)) for n, a, ts, r in self.externs)
135
+ evs = b"".join(struct.pack("<HBBH", sidx(n), a, 0, pack_types(ts)) for n, a, ts in self.events)
136
+ glb = b"".join(pack_value(v, lambda b: sidx(b.decode("utf-8", "surrogateescape"))) for v in self.globals)
137
+ sts = b"".join(struct.pack("<H", sidx(n)) for n in self.states)
138
+ tms = b"".join(struct.pack("<H", e) for e in self.timers)
139
+ fns = b"".join(struct.pack("<HBBH", sidx(n), a, l, o) for n, a, l, o in self.funcs)
140
+ hds = b"".join(struct.pack("<HHBBH", NONE16 if s is None else s, e, a, l, o)
141
+ for s, e, a, l, o in self.handlers)
142
+ dbg = b""
143
+ if debug:
144
+ dbg = struct.pack("<H", len(self.lines))
145
+ dbg += b"".join(struct.pack("<HHH", pc, ln & 0xFFFF, nd & 0xFFFF) for pc, ln, nd in self.lines)
146
+ names = self.global_names or [""] * len(self.globals)
147
+ dbg += b"".join(struct.pack("<H", sidx(n)) for n in names)
148
+ strs = b""
149
+ for s in strings:
150
+ b = s.encode("utf-8", "surrogateescape")
151
+ strs += struct.pack("<H", len(b)) + b + b"\x00"
152
+ header = MAGIC + struct.pack(
153
+ "<HHHHHHHHHHHHI", FORMAT_VERSION, (FLAG_DEBUG if debug else 0) | features(self), name_idx,
154
+ self.version & 0xFFFF, len(strings), len(self.globals), len(self.externs),
155
+ len(self.events), len(self.states), len(self.timers), len(self.funcs),
156
+ len(self.handlers), len(self.code))
157
+ assert len(header) == 32
158
+ return header + strs + glb + ext + evs + sts + tms + fns + hds + self.code + dbg
159
+
160
+ @classmethod
161
+ def from_bytes(cls, data):
162
+ try:
163
+ return cls._from_bytes(data)
164
+ except ValueError:
165
+ raise
166
+ except (IndexError, struct.error, UnicodeDecodeError) as e:
167
+ raise ValueError("corrupt bytecode image: %s" % e)
168
+
169
+ @classmethod
170
+ def _from_bytes(cls, data):
171
+ if data[:4] != MAGIC:
172
+ raise ValueError("not a FlowScript bytecode file")
173
+ (ver, flags, name_idx, pver, ns, ng, nx, ne, nst, nt, nf, nh,
174
+ csize) = struct.unpack_from("<HHHHHHHHHHHHI", data, 4)
175
+ if ver != FORMAT_VERSION:
176
+ raise ValueError("unsupported bytecode version %d" % ver)
177
+ p = cls()
178
+ off = 32
179
+ for _ in range(ns):
180
+ (ln,) = struct.unpack_from("<H", data, off)
181
+ if len(data) < off + 3 + ln:
182
+ raise ValueError("corrupt bytecode image: string table truncated")
183
+ p.strings.append(data[off + 2:off + 2 + ln].decode("utf-8", "surrogateescape"))
184
+ off += 3 + ln
185
+ for _ in range(ng):
186
+ p.globals.append(unpack_value(data, off, p.strings))
187
+ off += 9
188
+ for _ in range(nx):
189
+ n, a, r, ts = struct.unpack_from("<HBBI", data, off)
190
+ p.externs.append((p.strings[n], a, unpack_types(ts, a), r))
191
+ off += 8
192
+ for _ in range(ne):
193
+ n, a, _r, ts = struct.unpack_from("<HBBH", data, off)
194
+ p.events.append((p.strings[n], a, unpack_types(ts, a)))
195
+ off += 6
196
+ for _ in range(nst):
197
+ p.states.append(p.strings[struct.unpack_from("<H", data, off)[0]])
198
+ off += 2
199
+ for _ in range(nt):
200
+ p.timers.append(struct.unpack_from("<H", data, off)[0])
201
+ off += 2
202
+ for _ in range(nf):
203
+ n, a, l, o = struct.unpack_from("<HBBH", data, off)
204
+ p.funcs.append((p.strings[n], a, l, o))
205
+ off += 6
206
+ for _ in range(nh):
207
+ s, e, a, l, o = struct.unpack_from("<HHBBH", data, off)
208
+ p.handlers.append((None if s == NONE16 else s, e, a, l, o))
209
+ off += 8
210
+ p.code = bytes(data[off:off + csize])
211
+ off += csize
212
+ if flags & FLAG_DEBUG:
213
+ (nl,) = struct.unpack_from("<H", data, off)
214
+ off += 2
215
+ for _ in range(nl):
216
+ p.lines.append(struct.unpack_from("<HHH", data, off))
217
+ off += 6
218
+ for _ in range(ng):
219
+ p.global_names.append(p.strings[struct.unpack_from("<H", data, off)[0]])
220
+ off += 2
221
+ p.name = None if name_idx == NONE16 else p.strings[name_idx]
222
+ p.version = pver
223
+ return p
224
+
225
+
226
+ def features(prog):
227
+ """FLAG_FLOAT | FLAG_TIME | FLAG_STR | FLAG_TEXT bits for what `prog` can ever use.
228
+
229
+ Every value of those kinds starts at a typed declaration (global, extern, event),
230
+ a typed constant push, CONCAT or a built-in, so scanning them is exact.
231
+ """
232
+ from .values import FLOAT, TIME, STR, type_of
233
+ need = set()
234
+ for v in prog.globals:
235
+ need.add(type_of(v))
236
+ for _n, _a, ts, r in prog.externs:
237
+ need.update(ts)
238
+ if r == STR:
239
+ need.add("str-result")
240
+ need.add(r)
241
+ for _n, _a, ts in prog.events:
242
+ need.update(ts)
243
+ if STR in ts:
244
+ need.add("str-result") # the host's strings are copied into slots
245
+ pc = 0
246
+ while pc < len(prog.code):
247
+ name, val, size = decode(prog.code, pc)
248
+ if name == "PUSHF":
249
+ need.add(FLOAT)
250
+ elif name == "PUSHT":
251
+ need.add(TIME)
252
+ elif name == "CONCAT":
253
+ need.add("str-result")
254
+ elif name == "BUILTIN" and 0 <= val < len(BUILTINS):
255
+ _bn, ps, r = BUILTINS[val]
256
+ need.update(ps)
257
+ need.add(r)
258
+ if r == STR:
259
+ need.add("str-result")
260
+ if val in TEXT_BUILTINS:
261
+ need.add("text")
262
+ pc += size
263
+ return ((FLAG_FLOAT if FLOAT in need else 0) | (FLAG_TIME if TIME in need else 0) |
264
+ (FLAG_STR if "str-result" in need else 0) | (FLAG_TEXT if "text" in need else 0))
265
+
266
+
267
+ def decode(code, pc):
268
+ """Decode one instruction. Returns (name, operand, size)."""
269
+ op = code[pc]
270
+ if op not in BY_CODE:
271
+ return ("???", op, 1)
272
+ name, kind = BY_CODE[op]
273
+ if kind is None:
274
+ return (name, None, 1)
275
+ (val,) = struct.unpack_from(OPERAND_FMT[kind], code, pc + 1)
276
+ return (name, val, 1 + OPERAND_SIZE[kind])
277
+
278
+
279
+ def operand_comment(prog, name, val, local_names=None):
280
+ try:
281
+ if name == "PUSHS":
282
+ return show_str(prog.strings[val])
283
+ if name == "PUSHT":
284
+ from .values import Time
285
+ return show(Time(val))
286
+ if name == "BUILTIN":
287
+ b = BUILTINS[val]
288
+ return "%s(%s) -> %s" % (b[0], ", ".join(TYPE_NAMES[t] for t in b[1]), TYPE_NAMES[b[2]])
289
+ if name in ("LOADG", "STOREG"):
290
+ return prog.global_names[val] if prog.global_names else ""
291
+ if name in ("LOADL", "STOREL") and local_names:
292
+ return local_names[val] if val < len(local_names) else ""
293
+ if name == "CALL":
294
+ return prog.funcs[val][0]
295
+ if name == "XCALL":
296
+ return prog.externs[val][0]
297
+ if name == "GOTO":
298
+ return prog.states[val]
299
+ if name == "EMIT":
300
+ return prog.events[val][0]
301
+ if name in ("TSTART", "TEVERY", "TSTOP"):
302
+ return prog.events[prog.timers[val]][0]
303
+ except (IndexError, TypeError):
304
+ pass
305
+ return ""
306
+
307
+
308
+ def show_str(s):
309
+ from .values import Str
310
+ return show(Str(s.encode("utf-8", "surrogateescape")))
311
+
312
+
313
+ def sig(types, ret=None):
314
+ out = "(%s)" % ", ".join(TYPE_NAMES[t] for t in types)
315
+ if ret is not None and ret != NONE_TYPE:
316
+ out += " -> " + TYPE_NAMES[ret]
317
+ return out
318
+
319
+
320
+ def code_labels(prog):
321
+ """pc -> label for handler/function entry points."""
322
+ labels = {}
323
+ for name, _a, _l, off in prog.funcs:
324
+ labels[off] = "func %s" % name
325
+ for st, ev, _a, _l, off in prog.handlers:
326
+ evn = prog.events[ev][0]
327
+ labels[off] = ("on %s" % evn) if st is None else ("%s: on %s" % (prog.states[st], evn))
328
+ return labels
329
+
330
+
331
+ def disassemble(prog, local_names=None):
332
+ """Returns a list of dicts: pc, size, bytes(hex), op, arg, comment, label."""
333
+ out = []
334
+ labels = code_labels(prog)
335
+ pc = 0
336
+ code = prog.code
337
+ while pc < len(code):
338
+ name, val, size = decode(code, pc)
339
+ out.append({
340
+ "pc": pc, "size": size, "hex": code[pc:pc + size].hex(" "),
341
+ "op": name, "arg": val,
342
+ "comment": operand_comment(prog, name, val, (local_names or {}).get(pc)),
343
+ "label": labels.get(pc),
344
+ })
345
+ pc += size
346
+ return out
347
+
348
+
349
+ def disassemble_text(prog):
350
+ lines = []
351
+ if prog.name:
352
+ lines.append("; flow %s version %d" % (prog.name, prog.version))
353
+ lines.append("; %d bytes code, %d globals, %d externs, %d events, %d states, %d timers"
354
+ % (len(prog.code), len(prog.globals), len(prog.externs), len(prog.events),
355
+ len(prog.states), len(prog.timers)))
356
+ for i, (n, _a, ts, r) in enumerate(prog.externs):
357
+ lines.append("; extern %d: %s%s" % (i, n, sig(ts, r)))
358
+ for i, (n, _a, ts) in enumerate(prog.events):
359
+ lines.append("; event %d: %s%s" % (i, n, sig(ts)))
360
+ for i, v in enumerate(prog.globals):
361
+ name = prog.global_names[i] if i < len(prog.global_names) else ""
362
+ lines.append("; global %d: %s = %s" % (i, name, show(v)))
363
+ for i, n in enumerate(prog.states):
364
+ lines.append("; state %d: %s" % (i, n))
365
+ for ins in disassemble(prog):
366
+ if ins["label"]:
367
+ lines.append("")
368
+ lines.append("%s:" % ins["label"])
369
+ arg = "" if ins["arg"] is None else str(ins["arg"])
370
+ cmt = (" ; " + ins["comment"]) if ins["comment"] else ""
371
+ lines.append(" %04x %-14s %-7s %s%s" % (ins["pc"], ins["hex"], ins["op"], arg, cmt))
372
+ return "\n".join(lines) + "\n"
flowscript/cli.py ADDED
@@ -0,0 +1,126 @@
1
+ """Command line front end: python3 -m flowscript <command> ...
2
+
3
+ compile FILE.fs [-o OUT.fsb] [-g] [--size]
4
+ compile to bytecode (-g: include debug section)
5
+ size FILE.fs|FILE.fsb [-g] [--json]
6
+ where the image bytes go, RAM needed by the C VM,
7
+ smallest FS_* build settings, optimization hints
8
+ disasm FILE.fs|FILE.fsb print a disassembly
9
+ fmt FILE.fs [-w] print (or rewrite) in canonical format
10
+ ast FILE.fs print the visual model (AST JSON)
11
+ run FILE.fs|FILE.fsb [SCENARIO] run with stubbed externs, print trace
12
+ mermaid FILE.fs print a Mermaid flowchart
13
+ debug FILE.fs [--port N] start the 3-view web debugger
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ from .lexer import FlowError
22
+ from .parser import parse
23
+ from .printer import to_source
24
+ from .compiler import compile_source, compile_ast
25
+ from .bytecode import Program, disassemble_text
26
+
27
+
28
+ def load_program(path):
29
+ if path.endswith(".fsb"):
30
+ with open(path, "rb") as f:
31
+ return Program.from_bytes(f.read())
32
+ with open(path) as f:
33
+ _ast, prog, _dbg = compile_source(f.read())
34
+ return prog
35
+
36
+
37
+ def main(argv=None):
38
+ ap = argparse.ArgumentParser(prog="flowscript", description=__doc__,
39
+ formatter_class=argparse.RawDescriptionHelpFormatter)
40
+ sub = ap.add_subparsers(dest="cmd")
41
+ p = sub.add_parser("compile")
42
+ p.add_argument("file")
43
+ p.add_argument("-o", "--output")
44
+ p.add_argument("-g", "--debug", action="store_true")
45
+ p.add_argument("--size", action="store_true", help="print the size report")
46
+ p = sub.add_parser("size")
47
+ p.add_argument("file")
48
+ p.add_argument("-g", "--debug", action="store_true", help="include the debug section")
49
+ p.add_argument("--json", action="store_true")
50
+ p = sub.add_parser("disasm")
51
+ p.add_argument("file")
52
+ p = sub.add_parser("fmt")
53
+ p.add_argument("file")
54
+ p.add_argument("-w", "--write", action="store_true")
55
+ p = sub.add_parser("ast")
56
+ p.add_argument("file")
57
+ p = sub.add_parser("run")
58
+ p.add_argument("file")
59
+ p.add_argument("scenario", nargs="?")
60
+ p = sub.add_parser("mermaid")
61
+ p.add_argument("file")
62
+ p = sub.add_parser("debug")
63
+ p.add_argument("file", nargs="?")
64
+ p.add_argument("--port", type=int, default=8642)
65
+ p.add_argument("--no-browser", action="store_true")
66
+ args = ap.parse_args(argv)
67
+
68
+ try:
69
+ if args.cmd == "compile":
70
+ with open(args.file) as f:
71
+ _ast, prog, dbg = compile_source(f.read())
72
+ for w in dbg.warnings:
73
+ print("%s: line %d: warning: %s" % (args.file, w["line"], w["msg"]), file=sys.stderr)
74
+ out = args.output or os.path.splitext(args.file)[0] + ".fsb"
75
+ data = prog.to_bytes(debug=args.debug)
76
+ with open(out, "wb") as f:
77
+ f.write(data)
78
+ print("%s: %d bytes (%d bytes code)" % (out, len(data), len(prog.code)))
79
+ if args.size:
80
+ from .sizes import size_report, format_report
81
+ print()
82
+ sys.stdout.write(format_report(size_report(prog, args.debug), os.path.basename(args.file)))
83
+ elif args.cmd == "size":
84
+ from .sizes import size_report, format_report
85
+ prog = load_program(args.file)
86
+ image = open(args.file, "rb").read() if args.file.endswith(".fsb") else None
87
+ rep = size_report(prog, args.debug, image)
88
+ if args.json:
89
+ print(json.dumps(rep, indent=1))
90
+ else:
91
+ sys.stdout.write(format_report(rep, os.path.basename(args.file)))
92
+ elif args.cmd == "disasm":
93
+ sys.stdout.write(disassemble_text(load_program(args.file)))
94
+ elif args.cmd == "fmt":
95
+ with open(args.file) as f:
96
+ src = to_source(parse(f.read()))
97
+ if args.write:
98
+ with open(args.file, "w") as f:
99
+ f.write(src)
100
+ else:
101
+ sys.stdout.write(src)
102
+ elif args.cmd == "ast":
103
+ with open(args.file) as f:
104
+ print(json.dumps(parse(f.read()), indent=1))
105
+ elif args.cmd == "run":
106
+ from .runner import run_scenario
107
+ prog = load_program(args.file)
108
+ text = ""
109
+ if args.scenario:
110
+ with open(args.scenario) as f:
111
+ text = f.read()
112
+ run_scenario(prog, text, print)
113
+ elif args.cmd == "mermaid":
114
+ from .mermaid import to_mermaid
115
+ with open(args.file) as f:
116
+ sys.stdout.write(to_mermaid(parse(f.read())))
117
+ elif args.cmd == "debug":
118
+ from .debugger.server import serve
119
+ serve(args.file, args.port, open_browser=not args.no_browser)
120
+ else:
121
+ ap.print_help()
122
+ return 1
123
+ except FlowError as e:
124
+ print("%s: %s" % (getattr(args, "file", ""), e), file=sys.stderr)
125
+ return 1
126
+ return 0