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/__init__.py +35 -0
- flowscript/__main__.py +4 -0
- flowscript/api.py +286 -0
- flowscript/bytecode.py +372 -0
- flowscript/cli.py +126 -0
- flowscript/compiler.py +1019 -0
- flowscript/debugger/__init__.py +0 -0
- flowscript/debugger/server.py +505 -0
- flowscript/debugger/static/app.js +626 -0
- flowscript/debugger/static/chart.js +620 -0
- flowscript/debugger/static/edit.js +513 -0
- flowscript/debugger/static/index.html +115 -0
- flowscript/debugger/static/style.css +216 -0
- flowscript/intops.py +63 -0
- flowscript/lexer.py +265 -0
- flowscript/mermaid.py +109 -0
- flowscript/parser.py +578 -0
- flowscript/printer.py +225 -0
- flowscript/runner.py +164 -0
- flowscript/sizes.py +512 -0
- flowscript/svg.py +766 -0
- flowscript/values.py +724 -0
- flowscript/vm.py +577 -0
- flowscript_vm-0.1.0.dist-info/METADATA +400 -0
- flowscript_vm-0.1.0.dist-info/RECORD +29 -0
- flowscript_vm-0.1.0.dist-info/WHEEL +5 -0
- flowscript_vm-0.1.0.dist-info/entry_points.txt +2 -0
- flowscript_vm-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowscript_vm-0.1.0.dist-info/top_level.txt +1 -0
flowscript/printer.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""AST (visual model) -> canonical FlowScript source."""
|
|
2
|
+
|
|
3
|
+
from .parser import BINARY_LEVELS, COMPARE_OPS
|
|
4
|
+
|
|
5
|
+
INDENT = " "
|
|
6
|
+
|
|
7
|
+
_LEVEL = {}
|
|
8
|
+
for _i, _ops in enumerate(BINARY_LEVELS):
|
|
9
|
+
if _ops:
|
|
10
|
+
for _op in _ops:
|
|
11
|
+
_LEVEL[_op] = _i
|
|
12
|
+
NOT_LEVEL = 3
|
|
13
|
+
UNARY_LEVEL = len(BINARY_LEVELS)
|
|
14
|
+
ATOM_LEVEL = 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def prec(e):
|
|
18
|
+
t = e["type"]
|
|
19
|
+
if t == "binary":
|
|
20
|
+
return _LEVEL[e["op"]]
|
|
21
|
+
if t == "unary":
|
|
22
|
+
return NOT_LEVEL if e["op"] == "not" else UNARY_LEVEL
|
|
23
|
+
if t == "num" and (e["value"] < 0 or str(e["value"]).startswith("-")):
|
|
24
|
+
return UNARY_LEVEL
|
|
25
|
+
return ATOM_LEVEL
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _quote(s):
|
|
29
|
+
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\t", "\\t") + '"'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _num_text(e):
|
|
33
|
+
text = e.get("text")
|
|
34
|
+
if text:
|
|
35
|
+
try:
|
|
36
|
+
from .lexer import tokenize
|
|
37
|
+
toks = [t for t in tokenize(text.lstrip("-")) if t.kind in ("NUM", "FLOAT")]
|
|
38
|
+
v = toks[0].value if toks else None
|
|
39
|
+
if text.startswith("-") and v is not None:
|
|
40
|
+
v = -v
|
|
41
|
+
if v == e["value"]:
|
|
42
|
+
return text
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
v = e["value"]
|
|
46
|
+
if isinstance(v, float):
|
|
47
|
+
r = repr(v)
|
|
48
|
+
return r if ("e" in r or "." in r or "n" in r) else r + ".0"
|
|
49
|
+
return str(v)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def expr_to_str(e):
|
|
53
|
+
t = e["type"]
|
|
54
|
+
if t == "num":
|
|
55
|
+
return _num_text(e)
|
|
56
|
+
if t == "bool":
|
|
57
|
+
return "true" if e["value"] else "false"
|
|
58
|
+
if t == "str":
|
|
59
|
+
return _quote(e["value"])
|
|
60
|
+
if t == "name":
|
|
61
|
+
return e["name"]
|
|
62
|
+
if t in ("callx", "call"):
|
|
63
|
+
return "%s(%s)" % (e["name"], ", ".join(expr_to_str(a) for a in e["args"]))
|
|
64
|
+
if t == "unary":
|
|
65
|
+
op = e["op"]
|
|
66
|
+
inner = e["operand"]
|
|
67
|
+
s = expr_to_str(inner)
|
|
68
|
+
need = NOT_LEVEL if op == "not" else UNARY_LEVEL
|
|
69
|
+
if prec(inner) < need:
|
|
70
|
+
s = "(" + s + ")"
|
|
71
|
+
return ("not " + s) if op == "not" else (op + s)
|
|
72
|
+
if t == "binary":
|
|
73
|
+
lvl = _LEVEL[e["op"]]
|
|
74
|
+
ls = expr_to_str(e["left"])
|
|
75
|
+
rs = expr_to_str(e["right"])
|
|
76
|
+
lmin = lvl + 1 if e["op"] in COMPARE_OPS else lvl
|
|
77
|
+
if prec(e["left"]) < lmin:
|
|
78
|
+
ls = "(" + ls + ")"
|
|
79
|
+
if prec(e["right"]) <= lvl:
|
|
80
|
+
rs = "(" + rs + ")"
|
|
81
|
+
return "%s %s %s" % (ls, e["op"], rs)
|
|
82
|
+
raise ValueError("unknown expression type %r" % t)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Printer:
|
|
86
|
+
def __init__(self):
|
|
87
|
+
self.lines = []
|
|
88
|
+
|
|
89
|
+
def emit(self, depth, text):
|
|
90
|
+
self.lines.append(INDENT * depth + text if text else "")
|
|
91
|
+
|
|
92
|
+
def blank(self):
|
|
93
|
+
if self.lines and self.lines[-1] != "":
|
|
94
|
+
self.lines.append("")
|
|
95
|
+
|
|
96
|
+
def note(self, depth, node):
|
|
97
|
+
n = node.get("note")
|
|
98
|
+
if n:
|
|
99
|
+
for ln in str(n).split("\n"):
|
|
100
|
+
self.emit(depth, ("# " + ln) if ln else "#")
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def params(ps, types=None):
|
|
104
|
+
if types:
|
|
105
|
+
ps = ["%s %s" % (t, p) for t, p in zip(types, ps)]
|
|
106
|
+
return "(" + ", ".join(ps) + ")"
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def returns(it):
|
|
110
|
+
return (" -> " + it["returns"]) if it.get("returns") else ""
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def decl(it):
|
|
114
|
+
v = it.get("value")
|
|
115
|
+
return "%s %s%s" % (it.get("vtype") or "int", it["name"], (" = " + expr_to_str(v)) if v is not None else "")
|
|
116
|
+
|
|
117
|
+
def program(self, prog):
|
|
118
|
+
for c in prog.get("pre") or []:
|
|
119
|
+
self.note(0, {"note": c.get("text", "")})
|
|
120
|
+
self.blank()
|
|
121
|
+
self.note(0, prog)
|
|
122
|
+
if prog.get("name"):
|
|
123
|
+
v = prog.get("version") or 0
|
|
124
|
+
self.emit(0, "flow %s%s" % (prog["name"], (" version %d" % v) if v else ""))
|
|
125
|
+
prev = None
|
|
126
|
+
for item in prog.get("items", []):
|
|
127
|
+
kind = item["type"]
|
|
128
|
+
block = kind in ("func", "on", "state", "any_state")
|
|
129
|
+
if self.lines and (block or prev is None or prev in ("func", "on", "state", "any_state") or prev != kind):
|
|
130
|
+
self.blank()
|
|
131
|
+
self.item(0, item)
|
|
132
|
+
prev = kind
|
|
133
|
+
return "\n".join(self.lines) + "\n"
|
|
134
|
+
|
|
135
|
+
def item(self, d, it):
|
|
136
|
+
t = it["type"]
|
|
137
|
+
if t == "comment":
|
|
138
|
+
self.note(d, {"note": it.get("text", "")})
|
|
139
|
+
self.blank()
|
|
140
|
+
return
|
|
141
|
+
self.note(d, it)
|
|
142
|
+
if t == "extern":
|
|
143
|
+
self.emit(d, "extern %s%s%s" % (it["name"], self.params(it.get("params", []), it.get("ptypes")),
|
|
144
|
+
self.returns(it)))
|
|
145
|
+
elif t == "event":
|
|
146
|
+
ps = it.get("params") or []
|
|
147
|
+
self.emit(d, "event %s%s" % (it["name"], self.params(ps, it.get("ptypes")) if ps else ""))
|
|
148
|
+
elif t == "timer":
|
|
149
|
+
self.emit(d, "timer %s" % it["name"])
|
|
150
|
+
elif t == "const":
|
|
151
|
+
self.emit(d, "const %s = %s" % (it["name"], expr_to_str(it["value"])))
|
|
152
|
+
elif t == "var":
|
|
153
|
+
self.emit(d, self.decl(it))
|
|
154
|
+
elif t == "func":
|
|
155
|
+
self.emit(d, "func %s%s%s:" % (it["name"], self.params(it.get("params", []), it.get("ptypes")),
|
|
156
|
+
self.returns(it)))
|
|
157
|
+
self.block(d + 1, it.get("body", []))
|
|
158
|
+
elif t == "on":
|
|
159
|
+
ps = it.get("params")
|
|
160
|
+
self.emit(d, "on %s%s:" % (it["event"], self.params(ps) if ps else ""))
|
|
161
|
+
self.block(d + 1, it.get("body", []))
|
|
162
|
+
elif t in ("state", "any_state"):
|
|
163
|
+
self.emit(d, ("%sstate %s:" % ("initial " if it.get("initial") else "", it["name"]))
|
|
164
|
+
if t == "state" else "any_state:")
|
|
165
|
+
vs = it.get("vars") or []
|
|
166
|
+
for v in vs:
|
|
167
|
+
self.item(d + 1, v)
|
|
168
|
+
hs = it.get("handlers", [])
|
|
169
|
+
for i, h in enumerate(hs):
|
|
170
|
+
if i or vs:
|
|
171
|
+
self.blank()
|
|
172
|
+
self.item(d + 1, h)
|
|
173
|
+
if not vs and not any(h["type"] != "comment" for h in hs):
|
|
174
|
+
self.emit(d + 1, "pass")
|
|
175
|
+
else:
|
|
176
|
+
raise ValueError("unknown item type %r" % t)
|
|
177
|
+
|
|
178
|
+
def block(self, d, body):
|
|
179
|
+
body = body or []
|
|
180
|
+
for s in body:
|
|
181
|
+
self.stmt(d, s)
|
|
182
|
+
if not any(s["type"] != "comment" for s in body):
|
|
183
|
+
self.emit(d, "pass")
|
|
184
|
+
|
|
185
|
+
def stmt(self, d, s):
|
|
186
|
+
t = s["type"]
|
|
187
|
+
if t == "comment":
|
|
188
|
+
self.note(d, {"note": s.get("text", "")})
|
|
189
|
+
self.blank()
|
|
190
|
+
return
|
|
191
|
+
self.note(d, s)
|
|
192
|
+
if t == "assign":
|
|
193
|
+
self.emit(d, "%s = %s" % (s["target"], expr_to_str(s["value"])))
|
|
194
|
+
elif t == "local":
|
|
195
|
+
self.emit(d, self.decl(s))
|
|
196
|
+
elif t == "call":
|
|
197
|
+
self.emit(d, expr_to_str(s))
|
|
198
|
+
elif t == "if":
|
|
199
|
+
for i, br in enumerate(s["branches"]):
|
|
200
|
+
self.emit(d, ("if " if i == 0 else "elif ") + expr_to_str(br["cond"]) + ":")
|
|
201
|
+
self.block(d + 1, br.get("body", []))
|
|
202
|
+
if s.get("else_body") is not None:
|
|
203
|
+
self.emit(d, "else:")
|
|
204
|
+
self.block(d + 1, s["else_body"])
|
|
205
|
+
elif t == "while":
|
|
206
|
+
self.emit(d, "while " + expr_to_str(s["cond"]) + ":")
|
|
207
|
+
self.block(d + 1, s.get("body", []))
|
|
208
|
+
elif t == "goto":
|
|
209
|
+
self.emit(d, "goto_state " + s["state"])
|
|
210
|
+
elif t == "emit":
|
|
211
|
+
args = s.get("args") or []
|
|
212
|
+
self.emit(d, "raise_event %s%s" % (s["event"], ("(" + ", ".join(expr_to_str(a) for a in args) + ")") if args else ""))
|
|
213
|
+
elif t == "start":
|
|
214
|
+
self.emit(d, "start %s %s%s" % (s["timer"], "every " if s.get("every") else "", expr_to_str(s["ms"])))
|
|
215
|
+
elif t == "stop":
|
|
216
|
+
self.emit(d, "stop " + s["timer"])
|
|
217
|
+
elif t == "return":
|
|
218
|
+
v = s.get("value")
|
|
219
|
+
self.emit(d, "return" + ((" " + expr_to_str(v)) if v is not None else ""))
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError("unknown statement type %r" % t)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def to_source(prog):
|
|
225
|
+
return Printer().program(prog)
|
flowscript/runner.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
r"""Scenario runner with a canonical trace format.
|
|
2
|
+
|
|
3
|
+
The C runner (c/fsrun.c) implements the exact same format; the conformance test
|
|
4
|
+
compares the two traces line by line.
|
|
5
|
+
|
|
6
|
+
Scenario file (one command per line, '#' comments outside quotes):
|
|
7
|
+
extern NAME VALUE set the return value of a stubbed extern (default: zero of its type)
|
|
8
|
+
post EVENT [ARGS...] queue an event, then run until idle
|
|
9
|
+
tick MS advance timers and the clock, then run until idle
|
|
10
|
+
clock TIME set the clock now() reads (YYYY-MM-DDTHH:MM:SS[.mmm]Z or ms; default 0)
|
|
11
|
+
dump print all globals
|
|
12
|
+
host_timers (anywhere) the host runs the timers: start/stop requests are
|
|
13
|
+
traced and `tick` does nothing
|
|
14
|
+
fire TIMER host timers: the host's timer fired, then run until idle
|
|
15
|
+
|
|
16
|
+
Values are written by type: int 12, float 2.5 (also 1e-07, nan, inf), str "a b\n"
|
|
17
|
+
(quoted, escapes \" \\ \n \t \xNN), time 2026-11-01T12:00:00.000Z (or ms in a scenario).
|
|
18
|
+
|
|
19
|
+
Trace lines:
|
|
20
|
+
call NAME(a,b) -> r (call NAME(a,b) when it returns nothing)
|
|
21
|
+
emit NAME(a,b)
|
|
22
|
+
state NAME
|
|
23
|
+
fault CODE at PC
|
|
24
|
+
global[i] = v
|
|
25
|
+
timer start|every NAME MS / timer stop NAME (host_timers only)
|
|
26
|
+
fire NAME ignored (timer was stopped meanwhile)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .vm import VM, IDLE
|
|
30
|
+
from . import values as V
|
|
31
|
+
|
|
32
|
+
WS = " \t\n\r\x0b\x0c\x1c\x1d\x1e\x1f"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def split_words(line):
|
|
36
|
+
"""Words separated by whitespace; a word starting with '"' runs to the closing quote
|
|
37
|
+
(backslash escapes the next character). '#' outside quotes starts a comment."""
|
|
38
|
+
words = []
|
|
39
|
+
i, n = 0, len(line)
|
|
40
|
+
while i < n:
|
|
41
|
+
c = line[i]
|
|
42
|
+
if c in WS:
|
|
43
|
+
i += 1
|
|
44
|
+
continue
|
|
45
|
+
if c == "#":
|
|
46
|
+
break
|
|
47
|
+
j = i
|
|
48
|
+
if c == '"':
|
|
49
|
+
j += 1
|
|
50
|
+
while j < n and line[j] != '"':
|
|
51
|
+
j += 2 if line[j] == "\\" else 1
|
|
52
|
+
j = min(j + 1, n)
|
|
53
|
+
while j < n and line[j] not in WS and line[j] != "#":
|
|
54
|
+
j += 1
|
|
55
|
+
words.append(line[i:j])
|
|
56
|
+
i = j
|
|
57
|
+
return words
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_scenario(text):
|
|
61
|
+
cmds = []
|
|
62
|
+
for raw in text.splitlines():
|
|
63
|
+
parts = split_words(raw)
|
|
64
|
+
if not parts:
|
|
65
|
+
continue
|
|
66
|
+
cmds.append((parts[0], parts[1:]))
|
|
67
|
+
return cmds
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def show_args(args):
|
|
71
|
+
return ",".join(V.show(a) for a in args)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run_scenario(prog, scenario_text, out):
|
|
75
|
+
stubs = {}
|
|
76
|
+
clock = [0]
|
|
77
|
+
|
|
78
|
+
def make_stub(name, ret):
|
|
79
|
+
def stub(vm, args):
|
|
80
|
+
if ret == V.NONE_TYPE:
|
|
81
|
+
out("call %s(%s)" % (name, show_args(args)))
|
|
82
|
+
return 0
|
|
83
|
+
r = stubs.get(name, V.zero(ret))
|
|
84
|
+
out("call %s(%s) -> %s" % (name, show_args(args), V.show(r)))
|
|
85
|
+
if name in stubs and isinstance(r, V.Str):
|
|
86
|
+
return r.b # a host string: the VM copies it into a slot
|
|
87
|
+
return r
|
|
88
|
+
return stub
|
|
89
|
+
|
|
90
|
+
externs = {e[0]: make_stub(e[0], e[3]) for e in prog.externs}
|
|
91
|
+
ext_types = {e[0]: e[3] for e in prog.externs}
|
|
92
|
+
|
|
93
|
+
def on_emit(vm, name, args):
|
|
94
|
+
out("emit %s(%s)" % (name, show_args(args)))
|
|
95
|
+
|
|
96
|
+
def on_fault(vm, code, pc):
|
|
97
|
+
out("fault %d at %d" % (code, pc))
|
|
98
|
+
|
|
99
|
+
def on_timer(vm, i, start, ms, periodic):
|
|
100
|
+
name = prog.events[prog.timers[i]][0]
|
|
101
|
+
if start:
|
|
102
|
+
out("timer %s %s %d" % ("every" if periodic else "start", name, ms))
|
|
103
|
+
else:
|
|
104
|
+
out("timer stop %s" % name)
|
|
105
|
+
|
|
106
|
+
cmds = parse_scenario(scenario_text)
|
|
107
|
+
host_timers = any(c == "host_timers" for c, _a in cmds)
|
|
108
|
+
|
|
109
|
+
vm = VM(prog, externs, on_emit=on_emit, on_fault=on_fault,
|
|
110
|
+
on_timer=on_timer if host_timers else None, clock=lambda vm: clock[0])
|
|
111
|
+
last_state = [None]
|
|
112
|
+
|
|
113
|
+
def run():
|
|
114
|
+
while True:
|
|
115
|
+
r = vm.step()
|
|
116
|
+
if vm.state != last_state[0]:
|
|
117
|
+
last_state[0] = vm.state
|
|
118
|
+
out("state %s" % prog.states[vm.state])
|
|
119
|
+
if r == IDLE:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
def dump():
|
|
123
|
+
for i, v in enumerate(vm.globals):
|
|
124
|
+
out("global[%d] = %s" % (i, V.show(v)))
|
|
125
|
+
|
|
126
|
+
vm.start()
|
|
127
|
+
run()
|
|
128
|
+
for cmd, args in cmds:
|
|
129
|
+
if cmd == "extern":
|
|
130
|
+
t = ext_types.get(args[0])
|
|
131
|
+
if t is not None and t != V.NONE_TYPE:
|
|
132
|
+
stubs[args[0]] = V.parse_literal(t, args[1])
|
|
133
|
+
elif cmd == "post":
|
|
134
|
+
ev = prog.event_index(args[0])
|
|
135
|
+
if ev < 0:
|
|
136
|
+
raise ValueError("unknown event %r" % args[0])
|
|
137
|
+
ptypes = prog.events[ev][2]
|
|
138
|
+
vals = [V.parse_literal(t, a) for t, a in zip(ptypes, args[1:])]
|
|
139
|
+
vm.post(ev, *[v.b if isinstance(v, V.Str) else v for v in vals])
|
|
140
|
+
run()
|
|
141
|
+
elif cmd == "tick":
|
|
142
|
+
ms = int(args[0], 0)
|
|
143
|
+
if not -2 ** 31 <= ms < 2 ** 31:
|
|
144
|
+
raise ValueError("tick value out of int32 range")
|
|
145
|
+
clock[0] = V.wrap64(clock[0] + ms)
|
|
146
|
+
vm.tick(ms)
|
|
147
|
+
run()
|
|
148
|
+
elif cmd == "clock":
|
|
149
|
+
clock[0] = V.parse_literal(V.TIME, args[0]).ms
|
|
150
|
+
elif cmd == "dump":
|
|
151
|
+
dump()
|
|
152
|
+
elif cmd == "host_timers":
|
|
153
|
+
pass
|
|
154
|
+
elif cmd == "fire":
|
|
155
|
+
idx = vm.timer_index(args[0])
|
|
156
|
+
if vm.timers[idx][0]:
|
|
157
|
+
vm.timer_expired(idx)
|
|
158
|
+
else:
|
|
159
|
+
out("fire %s ignored" % args[0])
|
|
160
|
+
run()
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError("unknown scenario command %r" % cmd)
|
|
163
|
+
dump()
|
|
164
|
+
return vm
|