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/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""FlowScript: an event-driven scripting language whose programs are flowcharts.
|
|
2
|
+
|
|
3
|
+
Compile a script, run it (from source or bytecode), and draw it:
|
|
4
|
+
|
|
5
|
+
import flowscript
|
|
6
|
+
|
|
7
|
+
program = flowscript.compile(source) # flowscript.CompileError on errors
|
|
8
|
+
machine = flowscript.Machine(program, externs={"log": print})
|
|
9
|
+
machine.start()
|
|
10
|
+
machine.send("tick")
|
|
11
|
+
svg = flowscript.to_svg(program, state=machine.state)
|
|
12
|
+
|
|
13
|
+
See https://github.com/RubenNorgaard/flowscript for the language (SPEC.md) and the C VM.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .api import compile, load, Machine, CompileError, Diagnostic, Fault
|
|
17
|
+
from .svg import to_svg
|
|
18
|
+
from .bytecode import Program, disassemble, disassemble_text
|
|
19
|
+
from .lexer import FlowError
|
|
20
|
+
from .parser import parse, parse_expression
|
|
21
|
+
from .printer import to_source, expr_to_str
|
|
22
|
+
from .compiler import compile_source, compile_ast
|
|
23
|
+
from .mermaid import to_mermaid
|
|
24
|
+
from .sizes import size_report, format_report
|
|
25
|
+
from .vm import VM
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# the host API
|
|
29
|
+
"compile", "load", "Machine", "to_svg", "Program", "CompileError", "Diagnostic", "Fault",
|
|
30
|
+
# tools
|
|
31
|
+
"to_source", "to_mermaid", "size_report", "format_report", "disassemble", "disassemble_text",
|
|
32
|
+
# lower level
|
|
33
|
+
"FlowError", "parse", "parse_expression", "expr_to_str", "compile_source", "compile_ast", "VM",
|
|
34
|
+
]
|
|
35
|
+
__version__ = "0.1.0"
|
flowscript/__main__.py
ADDED
flowscript/api.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""The host-facing API: compile, load, run.
|
|
2
|
+
|
|
3
|
+
import flowscript
|
|
4
|
+
|
|
5
|
+
program = flowscript.compile(source) # raises flowscript.CompileError
|
|
6
|
+
program.warnings # [Diagnostic(line, col, message)]
|
|
7
|
+
data = program.to_bytes() # the .fsb image (also: program.save(path))
|
|
8
|
+
|
|
9
|
+
program = flowscript.load(data) # bytes, a path or a binary file
|
|
10
|
+
|
|
11
|
+
machine = flowscript.Machine(program, externs={"param": params.get, ...}, clock=time.time)
|
|
12
|
+
machine.start() # boot + enter the initial state
|
|
13
|
+
machine.send("tick") # post an event and run until idle
|
|
14
|
+
machine.state, machine["aggression"] # current state, a global
|
|
15
|
+
|
|
16
|
+
Values crossing the boundary are plain Python: int, float, str and timezone-aware
|
|
17
|
+
``datetime`` (FlowScript ``time``). Hosts never see the VM's internal representations.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import datetime as _dt
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from .bytecode import Program, FAULTS
|
|
24
|
+
from .compiler import compile_ast
|
|
25
|
+
from .lexer import FlowError
|
|
26
|
+
from .parser import parse
|
|
27
|
+
from . import values as V
|
|
28
|
+
from .vm import VM
|
|
29
|
+
|
|
30
|
+
__all__ = ["compile", "load", "Machine", "CompileError", "Diagnostic", "Fault"]
|
|
31
|
+
|
|
32
|
+
_EPOCH = _dt.datetime(1970, 1, 1, tzinfo=_dt.timezone.utc)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CompileError(FlowError):
|
|
36
|
+
"""The source has an error. ``line`` and ``col`` (1-based, 0 = unknown) locate it."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Diagnostic:
|
|
40
|
+
"""A compiler warning: the program compiles, but probably not as intended."""
|
|
41
|
+
__slots__ = ("line", "col", "message")
|
|
42
|
+
|
|
43
|
+
def __init__(self, line, col, message):
|
|
44
|
+
self.line, self.col, self.message = line, col, message
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return "Diagnostic(line=%d, %r)" % (self.line, self.message)
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
return "line %d: %s" % (self.line, self.message)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Fault:
|
|
54
|
+
"""A run-time fault: the handler it happened in was aborted, the machine carries on."""
|
|
55
|
+
__slots__ = ("code", "name", "pc", "line", "handler")
|
|
56
|
+
|
|
57
|
+
def __init__(self, code, pc, line, handler):
|
|
58
|
+
self.code, self.pc, self.line, self.handler = code, pc, line, handler
|
|
59
|
+
self.name = FAULTS.get(code, "fault %d" % code)
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
where = ("line %d" % self.line) if self.line else ("pc %d" % self.pc)
|
|
63
|
+
return "Fault(%d %s, %s%s)" % (self.code, self.name, where,
|
|
64
|
+
", in %s" % self.handler if self.handler else "")
|
|
65
|
+
|
|
66
|
+
__str__ = __repr__
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------------------ compile / load
|
|
70
|
+
def compile(source, *, filename=None):
|
|
71
|
+
"""Compile FlowScript source to a ``Program``.
|
|
72
|
+
|
|
73
|
+
The program keeps what hosts need besides the bytecode: ``source``, ``ast`` (for
|
|
74
|
+
``flowscript.to_svg``), ``warnings`` (a list of ``Diagnostic``) and debug information
|
|
75
|
+
(source lines of faults, global names). Raises ``CompileError``.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
ast = parse(source)
|
|
79
|
+
prog, dbg = compile_ast(ast)
|
|
80
|
+
except FlowError as e:
|
|
81
|
+
err = CompileError(e.msg, e.line, e.col)
|
|
82
|
+
err.filename = filename
|
|
83
|
+
raise err from None
|
|
84
|
+
prog.source = source
|
|
85
|
+
prog.ast = ast
|
|
86
|
+
prog.debug_info = dbg
|
|
87
|
+
prog.warnings = [Diagnostic(w["line"], w.get("col", 0), w["msg"]) for w in dbg.warnings]
|
|
88
|
+
return prog
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load(data):
|
|
92
|
+
"""A ``Program`` from bytecode: bytes/bytearray/memoryview, a path, or a binary file object.
|
|
93
|
+
|
|
94
|
+
Raises ``ValueError`` if the data is not a valid FlowScript image.
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(data, (bytes, bytearray, memoryview)):
|
|
97
|
+
raw = bytes(data)
|
|
98
|
+
elif isinstance(data, (str, os.PathLike)):
|
|
99
|
+
with open(data, "rb") as f:
|
|
100
|
+
raw = f.read()
|
|
101
|
+
elif hasattr(data, "read"):
|
|
102
|
+
raw = data.read()
|
|
103
|
+
else:
|
|
104
|
+
raise TypeError("load() takes bytes, a path or a binary file, not %s" % type(data).__name__)
|
|
105
|
+
return Program.from_bytes(raw)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------------------ values
|
|
109
|
+
def _to_py(v):
|
|
110
|
+
if isinstance(v, V.Str):
|
|
111
|
+
return v.text
|
|
112
|
+
if isinstance(v, V.Time):
|
|
113
|
+
return _EPOCH + _dt.timedelta(milliseconds=v.ms)
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _to_vm(v):
|
|
118
|
+
if isinstance(v, _dt.datetime):
|
|
119
|
+
if v.tzinfo is None:
|
|
120
|
+
raise ValueError("FlowScript times are UTC: pass a timezone-aware datetime")
|
|
121
|
+
delta = v - _EPOCH
|
|
122
|
+
return V.Time((delta.days * 86400 + delta.seconds) * 1000 + delta.microseconds // 1000)
|
|
123
|
+
if isinstance(v, bool):
|
|
124
|
+
return int(v)
|
|
125
|
+
return v
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------------------ machine
|
|
129
|
+
class Machine:
|
|
130
|
+
"""A running FlowScript program.
|
|
131
|
+
|
|
132
|
+
program: a ``Program`` (``compile``/``load``) or bytecode bytes.
|
|
133
|
+
externs: {name: callable(*args)} for the program's ``extern`` declarations. Arguments and
|
|
134
|
+
return values are int, float, str or timezone-aware datetime; an int is accepted
|
|
135
|
+
where a float is declared. Every extern must be provided (``ValueError`` otherwise).
|
|
136
|
+
clock: callable() -> seconds since 1970 (as ``time.time``) or a datetime; feeds ``now()``.
|
|
137
|
+
Default: ``time.time``.
|
|
138
|
+
on_event: callable(name, args) for every ``raise_event`` (args as Python values), before
|
|
139
|
+
the event is queued for the script itself.
|
|
140
|
+
on_fault: callable(Fault). Faults are also kept in ``faults`` (the last 100).
|
|
141
|
+
on_timer: None: timers run inside the machine, driven by ``advance(ms)``. Or
|
|
142
|
+
callable(name, start, ms, periodic): the host runs the timers (start=False stops
|
|
143
|
+
one) and calls ``timer_expired(name)`` when one fires.
|
|
144
|
+
limits: VM limits (stack_size, queue_size, budget, str_slots, str_len, ...), defaulting
|
|
145
|
+
to the C VM's defaults so a script that runs here also runs there.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, program, *, externs=None, clock=None, on_event=None, on_fault=None,
|
|
149
|
+
on_timer=None, **limits):
|
|
150
|
+
if isinstance(program, (bytes, bytearray, memoryview)):
|
|
151
|
+
program = load(program)
|
|
152
|
+
self.program = program
|
|
153
|
+
externs = dict(externs or {})
|
|
154
|
+
missing = [name for name, *_ in program.externs if name not in externs]
|
|
155
|
+
if missing:
|
|
156
|
+
raise ValueError("no host function for extern(s): %s" % ", ".join(missing))
|
|
157
|
+
unknown = sorted(set(externs) - {e[0] for e in program.externs})
|
|
158
|
+
if unknown:
|
|
159
|
+
raise ValueError("the program declares no extern(s) %s" % ", ".join(unknown))
|
|
160
|
+
self.faults = []
|
|
161
|
+
self._on_fault = on_fault
|
|
162
|
+
self._on_event = on_event
|
|
163
|
+
self._on_timer = on_timer
|
|
164
|
+
if clock is None:
|
|
165
|
+
import time
|
|
166
|
+
clock = time.time
|
|
167
|
+
self._clock = clock
|
|
168
|
+
names = list(program.global_names) if getattr(program, "global_names", None) else []
|
|
169
|
+
self._globals = {n: i for i, n in enumerate(names) if n}
|
|
170
|
+
self.vm = VM(program, {name: self._wrap(fn) for name, fn in externs.items()},
|
|
171
|
+
on_emit=self._emit, on_fault=self._fault,
|
|
172
|
+
on_timer=self._timer if on_timer else None, clock=self._now, **limits)
|
|
173
|
+
|
|
174
|
+
# -- glue ---------------------------------------------------------------------------
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _wrap(fn):
|
|
177
|
+
return lambda vm, args: _to_vm(fn(*[_to_py(a) for a in args]))
|
|
178
|
+
|
|
179
|
+
def _now(self, vm):
|
|
180
|
+
t = self._clock()
|
|
181
|
+
if isinstance(t, _dt.datetime):
|
|
182
|
+
return _to_vm(t).ms
|
|
183
|
+
return int(round(t * 1000))
|
|
184
|
+
|
|
185
|
+
def _emit(self, vm, name, args):
|
|
186
|
+
if self._on_event:
|
|
187
|
+
self._on_event(name, [_to_py(a) for a in args])
|
|
188
|
+
|
|
189
|
+
def _timer(self, vm, timer, start, ms, periodic):
|
|
190
|
+
self._on_timer(self.program.events[self.program.timers[timer]][0], bool(start), ms, bool(periodic))
|
|
191
|
+
|
|
192
|
+
def _fault(self, vm, code, pc):
|
|
193
|
+
line = 0
|
|
194
|
+
lines = getattr(self.program, "lines", None) or []
|
|
195
|
+
for p, ln, _node in lines:
|
|
196
|
+
if p > pc:
|
|
197
|
+
break
|
|
198
|
+
line = ln
|
|
199
|
+
handler = None
|
|
200
|
+
dbg = getattr(self.program, "debug_info", None)
|
|
201
|
+
if dbg is not None and pc >= 0:
|
|
202
|
+
r = dbg.routine_at(pc)
|
|
203
|
+
handler = r["name"] if r else None
|
|
204
|
+
f = Fault(code, pc, line if pc >= 0 else 0, handler)
|
|
205
|
+
self.faults.append(f)
|
|
206
|
+
del self.faults[:-100]
|
|
207
|
+
if self._on_fault:
|
|
208
|
+
self._on_fault(f)
|
|
209
|
+
|
|
210
|
+
# -- running ------------------------------------------------------------------------
|
|
211
|
+
def start(self):
|
|
212
|
+
"""Run ``on boot`` and enter the initial state. Returns the machine."""
|
|
213
|
+
self.vm.start()
|
|
214
|
+
self.vm.run()
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def post(self, event, *args):
|
|
218
|
+
"""Queue an event (not run yet). False if the queue or string memory was full."""
|
|
219
|
+
return self.vm.post(event, *[_to_vm(a) for a in args])
|
|
220
|
+
|
|
221
|
+
def run(self, max_steps=1000000):
|
|
222
|
+
"""Run queued work until idle; returns the number of steps."""
|
|
223
|
+
return self.vm.run(max_steps)
|
|
224
|
+
|
|
225
|
+
def send(self, event, *args):
|
|
226
|
+
"""Post an event and run until idle. False if it could not be queued."""
|
|
227
|
+
ok = self.post(event, *args)
|
|
228
|
+
self.run()
|
|
229
|
+
return ok
|
|
230
|
+
|
|
231
|
+
def advance(self, ms):
|
|
232
|
+
"""Built-in timers: let ``ms`` milliseconds pass.
|
|
233
|
+
|
|
234
|
+
Time moves from one timer expiry to the next and the handlers run in between, so
|
|
235
|
+
every expiry fires in order (``every 1s`` fires three times in ``advance(3000)``)
|
|
236
|
+
and timers that handlers start or stop take effect at the right moment.
|
|
237
|
+
"""
|
|
238
|
+
left = int(ms)
|
|
239
|
+
while left > 0:
|
|
240
|
+
due = [t[3] for t in self.vm.timers if t[0]]
|
|
241
|
+
step = min([left] + [max(1, d) for d in due])
|
|
242
|
+
self.vm.tick(step)
|
|
243
|
+
self.vm.run()
|
|
244
|
+
left -= step
|
|
245
|
+
|
|
246
|
+
def timer_expired(self, name):
|
|
247
|
+
"""Host-run timers: the timer ``name`` fired. Runs its handler; False if it was stopped."""
|
|
248
|
+
r = self.vm.timer_expired(self.vm.timer_index(name))
|
|
249
|
+
self.vm.run()
|
|
250
|
+
return r
|
|
251
|
+
|
|
252
|
+
def reset(self):
|
|
253
|
+
"""Back to the initial values (globals, no state, empty queue). Call ``start()`` again."""
|
|
254
|
+
self.vm.reset()
|
|
255
|
+
|
|
256
|
+
# -- state ----------------------------------------------------------------------------
|
|
257
|
+
@property
|
|
258
|
+
def state(self):
|
|
259
|
+
"""Name of the current state, or None (before ``start()``, or a program without states)."""
|
|
260
|
+
return self.vm.state_name()
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def idle(self):
|
|
264
|
+
return not self.vm.has_work() and not self.vm.active
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def globals(self):
|
|
268
|
+
"""{name: value} of the program's variables (state variables as ``state.name``)."""
|
|
269
|
+
return {n: _to_py(self.vm.globals[i]) for n, i in self._globals.items()}
|
|
270
|
+
|
|
271
|
+
def __getitem__(self, name):
|
|
272
|
+
return _to_py(self.vm.globals[self._global_index(name)])
|
|
273
|
+
|
|
274
|
+
def __setitem__(self, name, value):
|
|
275
|
+
self.vm.set_global(self._global_index(name), _to_vm(value))
|
|
276
|
+
|
|
277
|
+
def _global_index(self, name):
|
|
278
|
+
if name not in self._globals:
|
|
279
|
+
if not self._globals:
|
|
280
|
+
raise KeyError("%r: this program has no global names (compile it, or load "
|
|
281
|
+
"bytecode compiled with debug information)" % name)
|
|
282
|
+
raise KeyError("no global variable %r" % name)
|
|
283
|
+
return self._globals[name]
|
|
284
|
+
|
|
285
|
+
def __repr__(self):
|
|
286
|
+
return "<Machine %s state=%s>" % (self.program.name or "program", self.state)
|