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
|
File without changes
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""FlowScript 3-view debugger: a tiny stdlib HTTP server around the compiler + Python VM.
|
|
2
|
+
|
|
3
|
+
python3 -m flowscript debug examples/gps_tracker.fs
|
|
4
|
+
|
|
5
|
+
Views (in the browser): source | bytecode | flowchart, all following the execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import webbrowser
|
|
13
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
14
|
+
|
|
15
|
+
from ..lexer import FlowError
|
|
16
|
+
from ..parser import parse, parse_expression
|
|
17
|
+
from ..sizes import size_report
|
|
18
|
+
from ..printer import to_source, expr_to_str
|
|
19
|
+
from ..compiler import compile_ast
|
|
20
|
+
from ..bytecode import disassemble, FAULTS
|
|
21
|
+
from ..vm import VM, IDLE, DISPATCHED, RUNNING, DONE, FAULT
|
|
22
|
+
from .. import values as V
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def jv(v):
|
|
26
|
+
"""A VM value for JSON: an int as a number, anything else as its canonical text
|
|
27
|
+
(floats can be nan/inf, which JSON can't carry)."""
|
|
28
|
+
if isinstance(v, int) and not isinstance(v, bool):
|
|
29
|
+
return v
|
|
30
|
+
try:
|
|
31
|
+
return V.show(v)
|
|
32
|
+
except TypeError:
|
|
33
|
+
return str(v)
|
|
34
|
+
|
|
35
|
+
STATIC = os.path.join(os.path.dirname(__file__), "static")
|
|
36
|
+
LOG_LIMIT = 400
|
|
37
|
+
|
|
38
|
+
DEFAULT_SOURCE = """flow hello
|
|
39
|
+
|
|
40
|
+
extern led(on)
|
|
41
|
+
event button
|
|
42
|
+
timer blink
|
|
43
|
+
|
|
44
|
+
var count = 0
|
|
45
|
+
|
|
46
|
+
on boot
|
|
47
|
+
start blink every 500ms
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
on blink
|
|
51
|
+
count = count + 1
|
|
52
|
+
led(count % 2)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
on button
|
|
56
|
+
if count > 10 and not count > 100
|
|
57
|
+
stop blink
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Session:
|
|
64
|
+
def __init__(self, path=None):
|
|
65
|
+
self.path = path
|
|
66
|
+
if path and os.path.exists(path):
|
|
67
|
+
with open(path) as f:
|
|
68
|
+
self.source = f.read()
|
|
69
|
+
else:
|
|
70
|
+
self.source = DEFAULT_SOURCE
|
|
71
|
+
self.extern_values = {}
|
|
72
|
+
self.breakpoints = set() # source lines
|
|
73
|
+
self.ast = None
|
|
74
|
+
self.prog = None
|
|
75
|
+
self.dbg = None
|
|
76
|
+
self.vm = None
|
|
77
|
+
self.errors = []
|
|
78
|
+
self.compile(self.source)
|
|
79
|
+
|
|
80
|
+
# -- compilation -------------------------------------------------------------------
|
|
81
|
+
def compile(self, source):
|
|
82
|
+
self.source = source
|
|
83
|
+
self.errors = []
|
|
84
|
+
self.prog = None
|
|
85
|
+
self.vm = None
|
|
86
|
+
try:
|
|
87
|
+
self.ast = parse(source)
|
|
88
|
+
except FlowError as e:
|
|
89
|
+
self.errors = [e.to_dict()]
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
self.prog, self.dbg = compile_ast(self.ast)
|
|
93
|
+
except FlowError as e:
|
|
94
|
+
self.errors = [e.to_dict()]
|
|
95
|
+
return
|
|
96
|
+
# per-statement expression lists, used to clear stale gate values
|
|
97
|
+
self.stmt_exprs = {}
|
|
98
|
+
for nid, sid in self.dbg.stmt_of.items():
|
|
99
|
+
if nid != sid:
|
|
100
|
+
self.stmt_exprs.setdefault(sid, []).append(nid)
|
|
101
|
+
self.reset()
|
|
102
|
+
|
|
103
|
+
def reset(self):
|
|
104
|
+
self.log = []
|
|
105
|
+
self.values = {}
|
|
106
|
+
self.call_marks = []
|
|
107
|
+
self.last_state = None
|
|
108
|
+
self.total_steps = 0
|
|
109
|
+
if not self.prog:
|
|
110
|
+
self.vm = None
|
|
111
|
+
return
|
|
112
|
+
externs = {e[0]: self.make_extern(i, e[0], e[3]) for i, e in enumerate(self.prog.externs)}
|
|
113
|
+
self.vm = VM(self.prog, externs, on_emit=self.on_emit, on_fault=self.on_fault,
|
|
114
|
+
clock=lambda vm: int(time.time() * 1000))
|
|
115
|
+
self.vm.start()
|
|
116
|
+
self.add_log("sys", "VM started" + (" — initial state '%s'" % self.prog.states[0] if self.prog.states else ""))
|
|
117
|
+
|
|
118
|
+
def add_log(self, kind, text):
|
|
119
|
+
self.log.append({"kind": kind, "text": text, "n": self.total_steps})
|
|
120
|
+
if len(self.log) > LOG_LIMIT:
|
|
121
|
+
del self.log[:len(self.log) - LOG_LIMIT]
|
|
122
|
+
|
|
123
|
+
def make_extern(self, idx, name, ret):
|
|
124
|
+
def fn(vm, args):
|
|
125
|
+
shown = ", ".join(V.show(a) for a in args)
|
|
126
|
+
if ret == V.NONE_TYPE:
|
|
127
|
+
self.add_log("call", "%s(%s)" % (name, shown))
|
|
128
|
+
return 0
|
|
129
|
+
r = V.zero(ret)
|
|
130
|
+
text = self.extern_values.get(name)
|
|
131
|
+
if text not in (None, ""):
|
|
132
|
+
try:
|
|
133
|
+
r = V.parse_literal(ret, str(text))
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
self.add_log("fault", "%s: return value %r is not a %s (%s): returning %s"
|
|
136
|
+
% (name, text, V.TYPE_NAMES[ret], e, V.show(r)))
|
|
137
|
+
self.add_log("call", "%s(%s) → %s" % (name, shown, V.show(r)))
|
|
138
|
+
return r.b if isinstance(r, V.Str) and text not in (None, "") else r
|
|
139
|
+
return fn
|
|
140
|
+
|
|
141
|
+
def on_emit(self, vm, name, args):
|
|
142
|
+
self.add_log("emit", "emit %s(%s)" % (name, ", ".join(V.show(a) for a in args)))
|
|
143
|
+
|
|
144
|
+
def on_fault(self, vm, code, pc):
|
|
145
|
+
self.add_log("fault", "FAULT %d: %s at pc %s" % (code, FAULTS.get(code, "?"), pc))
|
|
146
|
+
|
|
147
|
+
def node_by_id(self, nid):
|
|
148
|
+
if nid is None:
|
|
149
|
+
return None
|
|
150
|
+
if not hasattr(self, "_node_index") or self._node_index[0] is not self.ast:
|
|
151
|
+
idx = {}
|
|
152
|
+
|
|
153
|
+
def walk(o):
|
|
154
|
+
if isinstance(o, dict):
|
|
155
|
+
if "id" in o and "type" in o:
|
|
156
|
+
idx[o["id"]] = o
|
|
157
|
+
for v in o.values():
|
|
158
|
+
walk(v)
|
|
159
|
+
elif isinstance(o, list):
|
|
160
|
+
for v in o:
|
|
161
|
+
walk(v)
|
|
162
|
+
walk(self.ast)
|
|
163
|
+
self._node_index = (self.ast, idx)
|
|
164
|
+
return self._node_index[1].get(nid)
|
|
165
|
+
|
|
166
|
+
# -- stepping ----------------------------------------------------------------------
|
|
167
|
+
def stmt_at(self, pc):
|
|
168
|
+
nid = self.dbg.pc_node.get(pc)
|
|
169
|
+
return self.dbg.stmt_of.get(nid, nid)
|
|
170
|
+
|
|
171
|
+
def one(self):
|
|
172
|
+
"""One VM step with debugger bookkeeping. Returns the VM step result."""
|
|
173
|
+
vm = self.vm
|
|
174
|
+
was_active = vm.active
|
|
175
|
+
pc = vm.pc
|
|
176
|
+
depth = len(vm.frames)
|
|
177
|
+
if was_active and pc in self.dbg.stmt_starts:
|
|
178
|
+
for nid in self.stmt_exprs.get(self.dbg.stmt_starts[pc], []):
|
|
179
|
+
self.values.pop(nid, None)
|
|
180
|
+
r = vm.step()
|
|
181
|
+
self.total_steps += 1
|
|
182
|
+
if vm.state != self.last_state:
|
|
183
|
+
self.last_state = vm.state
|
|
184
|
+
self.add_log("state", "state → %s" % vm.prog.states[vm.state])
|
|
185
|
+
if r == DISPATCHED:
|
|
186
|
+
self.call_marks = []
|
|
187
|
+
if vm.active:
|
|
188
|
+
rt = self.dbg.routine_at(vm.pc)
|
|
189
|
+
self.add_log("dispatch", "→ %s" % (rt["name"] if rt else "?"))
|
|
190
|
+
elif r == RUNNING and pc in self.dbg.value_marks:
|
|
191
|
+
nid = self.dbg.value_marks[pc]
|
|
192
|
+
if len(vm.frames) > depth: # CALL: value known after return
|
|
193
|
+
self.call_marks.append((nid, depth))
|
|
194
|
+
elif vm.stack:
|
|
195
|
+
self.values[nid] = vm.stack[-1]
|
|
196
|
+
if r == RUNNING and self.call_marks and len(vm.frames) == self.call_marks[-1][1] and len(vm.frames) < depth:
|
|
197
|
+
nid, _ = self.call_marks.pop()
|
|
198
|
+
if vm.stack:
|
|
199
|
+
self.values[nid] = vm.stack[-1]
|
|
200
|
+
if r in (DONE, FAULT):
|
|
201
|
+
self.call_marks = []
|
|
202
|
+
return r
|
|
203
|
+
|
|
204
|
+
def at_breakpoint(self):
|
|
205
|
+
vm = self.vm
|
|
206
|
+
if not vm.active or not self.breakpoints:
|
|
207
|
+
return False
|
|
208
|
+
lines = set()
|
|
209
|
+
if vm.pc in self.dbg.stmt_starts:
|
|
210
|
+
lines.add(self.dbg.pc_line.get(vm.pc))
|
|
211
|
+
if vm.steps == 0: # just dispatched: the handler head counts too
|
|
212
|
+
rt = self.dbg.routine_at(vm.pc)
|
|
213
|
+
node = self.node_by_id(rt["node"]) if rt else None
|
|
214
|
+
if node:
|
|
215
|
+
lines.add(node.get("line"))
|
|
216
|
+
return bool(lines & self.breakpoints)
|
|
217
|
+
|
|
218
|
+
def step(self, mode):
|
|
219
|
+
vm = self.vm
|
|
220
|
+
if vm is None:
|
|
221
|
+
return "nocode"
|
|
222
|
+
limit = 200000
|
|
223
|
+
if mode == "instr":
|
|
224
|
+
return self.one()
|
|
225
|
+
if mode in ("stmt", "over", "out"):
|
|
226
|
+
if not vm.active:
|
|
227
|
+
return self.one()
|
|
228
|
+
start_stmt = self.stmt_at(vm.pc)
|
|
229
|
+
depth = len(vm.frames)
|
|
230
|
+
for _ in range(limit):
|
|
231
|
+
r = self.one()
|
|
232
|
+
if r != RUNNING:
|
|
233
|
+
return r
|
|
234
|
+
d = len(vm.frames)
|
|
235
|
+
if mode == "out":
|
|
236
|
+
if d < depth:
|
|
237
|
+
return r
|
|
238
|
+
continue
|
|
239
|
+
if mode == "over" and d > depth:
|
|
240
|
+
continue
|
|
241
|
+
if mode == "over" and d < depth:
|
|
242
|
+
return r
|
|
243
|
+
if self.stmt_at(vm.pc) != start_stmt or vm.pc in self.dbg.stmt_starts or d != depth:
|
|
244
|
+
return r
|
|
245
|
+
return "limit"
|
|
246
|
+
if mode == "handler":
|
|
247
|
+
if not vm.active:
|
|
248
|
+
r = self.one()
|
|
249
|
+
if r != DISPATCHED:
|
|
250
|
+
return r
|
|
251
|
+
for _ in range(limit):
|
|
252
|
+
r = self.one()
|
|
253
|
+
if r != RUNNING:
|
|
254
|
+
return r
|
|
255
|
+
return "limit"
|
|
256
|
+
if mode == "run":
|
|
257
|
+
first = True
|
|
258
|
+
for _ in range(limit):
|
|
259
|
+
if not first and self.at_breakpoint():
|
|
260
|
+
return "breakpoint"
|
|
261
|
+
first = False
|
|
262
|
+
r = self.one()
|
|
263
|
+
if r == IDLE:
|
|
264
|
+
return r
|
|
265
|
+
return "limit"
|
|
266
|
+
raise ValueError("unknown step mode %r" % mode)
|
|
267
|
+
|
|
268
|
+
# -- snapshot ----------------------------------------------------------------------
|
|
269
|
+
def snapshot(self, status=None):
|
|
270
|
+
vm = self.vm
|
|
271
|
+
if vm is None:
|
|
272
|
+
return {"ready": False, "status": "error", "log": self.log if hasattr(self, "log") else []}
|
|
273
|
+
p = self.prog
|
|
274
|
+
snap = {
|
|
275
|
+
"ready": True,
|
|
276
|
+
"status": status,
|
|
277
|
+
"active": vm.active,
|
|
278
|
+
"pc": vm.pc if vm.active else None,
|
|
279
|
+
"node": self.dbg.pc_node.get(vm.pc) if vm.active else None,
|
|
280
|
+
"line": self.dbg.pc_line.get(vm.pc) if vm.active else None,
|
|
281
|
+
"state": vm.state_name(),
|
|
282
|
+
"pending": None if vm.pending is None else p.states[vm.pending],
|
|
283
|
+
"globals": [{"name": n, "value": jv(v), "type": V.TYPE_NAMES[V.type_of(v)]}
|
|
284
|
+
for n, v in zip(p.global_names, vm.globals)],
|
|
285
|
+
"stack": [jv(v) for v in vm.stack],
|
|
286
|
+
"frames": [],
|
|
287
|
+
"queue": [{"event": p.events[e][0], "args": [jv(x) for x in a]} for e, a in vm.queue],
|
|
288
|
+
"timers": [],
|
|
289
|
+
"values": {str(k): jv(v) for k, v in self.values.items()},
|
|
290
|
+
"strings_in_use": vm.strings_in_use(),
|
|
291
|
+
"steps": vm.steps if vm.active else 0,
|
|
292
|
+
"total_steps": self.total_steps,
|
|
293
|
+
"log": self.log[-200:],
|
|
294
|
+
}
|
|
295
|
+
if vm.active:
|
|
296
|
+
snap["stmt"] = self.stmt_at(vm.pc)
|
|
297
|
+
for i, fr in enumerate(vm.frames):
|
|
298
|
+
# outer frames are paused at their CALL instruction (3 bytes before return pc)
|
|
299
|
+
fpc = vm.frames[i + 1][0] - 3 if i + 1 < len(vm.frames) else vm.pc
|
|
300
|
+
rt = self.dbg.routine_at(fpc)
|
|
301
|
+
base = fr[1]
|
|
302
|
+
local_vals = [{"name": nm, "value": jv(vm.stack[base + j])}
|
|
303
|
+
for j, nm in enumerate(rt["locals"] if rt else [])
|
|
304
|
+
if base + j < len(vm.stack)]
|
|
305
|
+
snap["frames"].append({"routine": rt["name"] if rt else "?", "pc": fpc,
|
|
306
|
+
"node": self.dbg.pc_node.get(fpc), "locals": local_vals,
|
|
307
|
+
"base": base})
|
|
308
|
+
for i, t in enumerate(vm.timers):
|
|
309
|
+
snap["timers"].append({"name": p.events[p.timers[i]][0], "active": t[0],
|
|
310
|
+
"periodic": t[1], "period": t[2], "remaining": t[3],
|
|
311
|
+
"owner": None if t[4] is None else p.states[t[4]]})
|
|
312
|
+
return snap
|
|
313
|
+
|
|
314
|
+
def compile_result(self):
|
|
315
|
+
res = {"source": self.source, "errors": self.errors, "ast": self.ast,
|
|
316
|
+
"warnings": self.dbg.warnings if self.prog else [],
|
|
317
|
+
"path": self.path, "extern_values": self.extern_values,
|
|
318
|
+
"breakpoints": sorted(self.breakpoints)}
|
|
319
|
+
if self.prog:
|
|
320
|
+
p = self.prog
|
|
321
|
+
local_names = {}
|
|
322
|
+
for r in self.dbg.routines:
|
|
323
|
+
for pc in range(r["start"], r["end"]):
|
|
324
|
+
local_names[pc] = r["locals"]
|
|
325
|
+
dis = disassemble(p, local_names)
|
|
326
|
+
for ins in dis:
|
|
327
|
+
if isinstance(ins["arg"], float):
|
|
328
|
+
ins["arg"] = V.fmt_float(ins["arg"])
|
|
329
|
+
ins["node"] = self.dbg.pc_node.get(ins["pc"])
|
|
330
|
+
ins["line"] = self.dbg.pc_line.get(ins["pc"])
|
|
331
|
+
ins["stmt"] = self.dbg.stmt_of.get(ins["node"], ins["node"])
|
|
332
|
+
res.update({
|
|
333
|
+
"disasm": dis,
|
|
334
|
+
"size": len(p.to_bytes()),
|
|
335
|
+
"code_size": len(p.code),
|
|
336
|
+
"events": [{"name": n, "argc": a, "types": [V.TYPE_NAMES[t] for t in ts]}
|
|
337
|
+
for n, a, ts in p.events[3:]],
|
|
338
|
+
"externs": [{"name": n, "argc": a, "types": [V.TYPE_NAMES[t] for t in ts],
|
|
339
|
+
"returns": None if r == V.NONE_TYPE else V.TYPE_NAMES[r]}
|
|
340
|
+
for n, a, ts, r in p.externs],
|
|
341
|
+
"states": list(p.states),
|
|
342
|
+
"timers": [p.events[e][0] for e in p.timers],
|
|
343
|
+
"stmt_of": {str(k): v for k, v in self.dbg.stmt_of.items()},
|
|
344
|
+
"routines": self.dbg.routines,
|
|
345
|
+
"size_report": size_report(p),
|
|
346
|
+
})
|
|
347
|
+
res["snapshot"] = self.snapshot("compiled")
|
|
348
|
+
return res
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def make_handler(session, lock):
|
|
352
|
+
class Handler(BaseHTTPRequestHandler):
|
|
353
|
+
def log_message(self, fmt, *args):
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
def send_json(self, obj, code=200):
|
|
357
|
+
data = json.dumps(obj).encode("utf-8")
|
|
358
|
+
self.send_response(code)
|
|
359
|
+
self.send_header("Content-Type", "application/json")
|
|
360
|
+
self.send_header("Content-Length", str(len(data)))
|
|
361
|
+
self.send_header("Cache-Control", "no-store")
|
|
362
|
+
self.end_headers()
|
|
363
|
+
self.wfile.write(data)
|
|
364
|
+
|
|
365
|
+
def do_GET(self):
|
|
366
|
+
path = self.path.split("?", 1)[0]
|
|
367
|
+
if path == "/":
|
|
368
|
+
path = "/index.html"
|
|
369
|
+
fp = os.path.normpath(os.path.join(STATIC, path.lstrip("/")))
|
|
370
|
+
if not fp.startswith(STATIC) or not os.path.isfile(fp):
|
|
371
|
+
self.send_error(404)
|
|
372
|
+
return
|
|
373
|
+
ctype = {"html": "text/html", "js": "application/javascript", "css": "text/css",
|
|
374
|
+
"svg": "image/svg+xml"}.get(fp.rsplit(".", 1)[-1], "application/octet-stream")
|
|
375
|
+
with open(fp, "rb") as f:
|
|
376
|
+
data = f.read()
|
|
377
|
+
self.send_response(200)
|
|
378
|
+
self.send_header("Content-Type", ctype + "; charset=utf-8")
|
|
379
|
+
self.send_header("Content-Length", str(len(data)))
|
|
380
|
+
self.send_header("Cache-Control", "no-store")
|
|
381
|
+
self.end_headers()
|
|
382
|
+
self.wfile.write(data)
|
|
383
|
+
|
|
384
|
+
def do_POST(self):
|
|
385
|
+
n = int(self.headers.get("Content-Length") or 0)
|
|
386
|
+
try:
|
|
387
|
+
body = json.loads(self.rfile.read(n) or b"{}")
|
|
388
|
+
except ValueError:
|
|
389
|
+
self.send_json({"error": "bad json"}, 400)
|
|
390
|
+
return
|
|
391
|
+
route = self.path.split("?", 1)[0]
|
|
392
|
+
with lock:
|
|
393
|
+
try:
|
|
394
|
+
self.send_json(self.route(route, body))
|
|
395
|
+
except FlowError as e:
|
|
396
|
+
self.send_json({"error": str(e), "detail": e.to_dict()})
|
|
397
|
+
except Exception as e: # keep the debugger alive
|
|
398
|
+
import traceback
|
|
399
|
+
traceback.print_exc()
|
|
400
|
+
self.send_json({"error": "%s: %s" % (type(e).__name__, e)}, 500)
|
|
401
|
+
|
|
402
|
+
def route(self, route, body):
|
|
403
|
+
s = session
|
|
404
|
+
if route == "/api/load":
|
|
405
|
+
return s.compile_result()
|
|
406
|
+
if route == "/api/compile":
|
|
407
|
+
s.compile(body.get("source", ""))
|
|
408
|
+
return s.compile_result()
|
|
409
|
+
if route == "/api/from_model":
|
|
410
|
+
src = to_source(body["ast"])
|
|
411
|
+
s.compile(src)
|
|
412
|
+
return s.compile_result()
|
|
413
|
+
if route == "/api/format":
|
|
414
|
+
return {"source": to_source(parse(body.get("source", "")))}
|
|
415
|
+
if route == "/api/parse_exprs":
|
|
416
|
+
out = []
|
|
417
|
+
for text in body.get("texts", []):
|
|
418
|
+
try:
|
|
419
|
+
e = parse_expression(text)
|
|
420
|
+
out.append({"ok": True, "expr": e, "text": expr_to_str(e)})
|
|
421
|
+
except FlowError as err:
|
|
422
|
+
out.append({"ok": False, "error": err.msg})
|
|
423
|
+
return {"results": out}
|
|
424
|
+
if route == "/api/save":
|
|
425
|
+
path = body.get("path") or s.path
|
|
426
|
+
if not path:
|
|
427
|
+
return {"error": "no file name"}
|
|
428
|
+
with open(path, "w") as f:
|
|
429
|
+
f.write(body.get("source", s.source))
|
|
430
|
+
s.path = path
|
|
431
|
+
return {"ok": True, "path": path}
|
|
432
|
+
if route == "/api/bytecode":
|
|
433
|
+
if not s.prog:
|
|
434
|
+
return {"error": "program does not compile"}
|
|
435
|
+
import base64
|
|
436
|
+
return {"base64": base64.b64encode(s.prog.to_bytes(debug=bool(body.get("debug")))).decode()}
|
|
437
|
+
if s.vm is None and route.startswith("/api/vm/"):
|
|
438
|
+
return {"snapshot": s.snapshot("error")}
|
|
439
|
+
if route == "/api/vm/reset":
|
|
440
|
+
s.reset()
|
|
441
|
+
return {"snapshot": s.snapshot("reset")}
|
|
442
|
+
if route == "/api/vm/step":
|
|
443
|
+
r = s.step(body.get("mode", "instr"))
|
|
444
|
+
return {"snapshot": s.snapshot(r)}
|
|
445
|
+
if route == "/api/vm/post":
|
|
446
|
+
ev = s.prog.event_index(body["event"])
|
|
447
|
+
types = s.prog.events[ev][2] if ev >= 0 else ()
|
|
448
|
+
try:
|
|
449
|
+
args = [V.parse_literal(t, str(a)) for t, a in zip(types, body.get("args", []))]
|
|
450
|
+
except ValueError as e:
|
|
451
|
+
s.add_log("fault", "post %s: %s" % (body["event"], e))
|
|
452
|
+
return {"snapshot": s.snapshot("posted")}
|
|
453
|
+
try:
|
|
454
|
+
ok = s.vm.post(body["event"], *[a.b if isinstance(a, V.Str) else a for a in args])
|
|
455
|
+
except TypeError as e:
|
|
456
|
+
ok = False
|
|
457
|
+
s.add_log("fault", "post %s: %s" % (body["event"], e))
|
|
458
|
+
s.add_log("post", "post %s(%s)%s" % (body["event"], ", ".join(V.show(a) for a in args),
|
|
459
|
+
"" if ok else " — not queued"))
|
|
460
|
+
return {"snapshot": s.snapshot("posted")}
|
|
461
|
+
if route == "/api/vm/tick":
|
|
462
|
+
ms = int(body.get("ms", 0))
|
|
463
|
+
s.vm.tick(ms)
|
|
464
|
+
s.add_log("tick", "tick %d ms" % ms)
|
|
465
|
+
return {"snapshot": s.snapshot("ticked")}
|
|
466
|
+
if route == "/api/vm/extern":
|
|
467
|
+
s.extern_values[body["name"]] = str(body.get("value", ""))
|
|
468
|
+
return {"ok": True}
|
|
469
|
+
if route == "/api/vm/breakpoints":
|
|
470
|
+
s.breakpoints = set(int(x) for x in body.get("lines", []))
|
|
471
|
+
return {"ok": True}
|
|
472
|
+
if route == "/api/vm/set_global":
|
|
473
|
+
idx = s.prog.global_names.index(body["name"])
|
|
474
|
+
t = V.type_of(s.vm.globals[idx])
|
|
475
|
+
try:
|
|
476
|
+
v = V.parse_literal(t, str(body["value"]))
|
|
477
|
+
s.vm.set_global(idx, v.b if isinstance(v, V.Str) else v)
|
|
478
|
+
except (ValueError, V.ValueFault) as e:
|
|
479
|
+
s.add_log("fault", "%s: %r is not a %s value (%s)" % (body["name"], body["value"], V.TYPE_NAMES[t], e))
|
|
480
|
+
return {"snapshot": s.snapshot("edited")}
|
|
481
|
+
raise ValueError("unknown route %s" % route)
|
|
482
|
+
|
|
483
|
+
return Handler
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def serve(path=None, port=8642, open_browser=True):
|
|
487
|
+
session = Session(path)
|
|
488
|
+
if session.errors:
|
|
489
|
+
print("warning: %s" % session.errors[0])
|
|
490
|
+
try:
|
|
491
|
+
httpd = HTTPServer(("127.0.0.1", port), make_handler(session, threading.Lock()))
|
|
492
|
+
except OSError as e:
|
|
493
|
+
import errno
|
|
494
|
+
if e.errno == errno.EADDRINUSE:
|
|
495
|
+
raise SystemExit("error: port %d is already in use (another debugger still running?). "
|
|
496
|
+
"Stop it, or pick another port with --port N." % port)
|
|
497
|
+
raise
|
|
498
|
+
url = "http://127.0.0.1:%d/" % port
|
|
499
|
+
print("FlowScript debugger on %s (Ctrl+C to stop)" % url)
|
|
500
|
+
if open_browser:
|
|
501
|
+
threading.Timer(0.4, lambda: webbrowser.open(url)).start()
|
|
502
|
+
try:
|
|
503
|
+
httpd.serve_forever()
|
|
504
|
+
except KeyboardInterrupt:
|
|
505
|
+
print()
|