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