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/values.py
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"""Typed values: int, float, str and time. Shared by the compiler (constant folding), the
|
|
2
|
+
reference VM and the trace format; c/flowscript.c implements the same rules bit for bit.
|
|
3
|
+
|
|
4
|
+
int 32-bit signed, wrap-around (intops.py)
|
|
5
|
+
float IEEE 754 double
|
|
6
|
+
str immutable byte string (UTF-8 by convention), at most STR_LEN bytes at run time
|
|
7
|
+
time milliseconds since 1970-01-01T00:00:00Z (int64, UTC, proleptic Gregorian)
|
|
8
|
+
|
|
9
|
+
Text conversions (str(x), float(s), time(s), the trace format) are defined here with
|
|
10
|
+
plain integer/double arithmetic so that both VMs produce identical text without a C
|
|
11
|
+
library: see fmt_float() and parse_float().
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
import ctypes.util
|
|
16
|
+
import math
|
|
17
|
+
import struct
|
|
18
|
+
|
|
19
|
+
from .intops import wrap
|
|
20
|
+
|
|
21
|
+
INT, FLOAT, STR, TIME = 0, 1, 2, 3
|
|
22
|
+
TYPE_NAMES = ["int", "float", "str", "time"]
|
|
23
|
+
TYPE_CODES = {n: i for i, n in enumerate(TYPE_NAMES)}
|
|
24
|
+
NONE_TYPE = 4 # "returns nothing" in the extern table
|
|
25
|
+
|
|
26
|
+
DAY_MS = 86400000
|
|
27
|
+
STR_LEN = 64 # default max bytes of a run-time string (FS_STR_LEN)
|
|
28
|
+
STR_SLOTS = 16 # default run-time string slots (FS_STR_SLOTS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValueFault(Exception):
|
|
32
|
+
"""A run-time value error; .code is the VM fault code."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, code):
|
|
35
|
+
super().__init__(code)
|
|
36
|
+
self.code = code
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
TYPE_MISMATCH, OUT_OF_STRINGS, STRING_TOO_LONG, BAD_VALUE = 10, 11, 12, 13
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Str:
|
|
43
|
+
"""A string value. `slot` is the VM string slot holding it (None: constant)."""
|
|
44
|
+
__slots__ = ("b", "slot")
|
|
45
|
+
|
|
46
|
+
def __init__(self, b, slot=None):
|
|
47
|
+
self.b = b if isinstance(b, bytes) else str(b).encode("utf-8")
|
|
48
|
+
self.slot = slot
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def text(self):
|
|
52
|
+
return self.b.decode("utf-8", "replace")
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other):
|
|
55
|
+
return isinstance(other, Str) and other.b == self.b
|
|
56
|
+
|
|
57
|
+
def __hash__(self):
|
|
58
|
+
return hash(self.b)
|
|
59
|
+
|
|
60
|
+
def __repr__(self):
|
|
61
|
+
return "Str(%r)" % self.b
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Time:
|
|
65
|
+
__slots__ = ("ms",)
|
|
66
|
+
|
|
67
|
+
def __init__(self, ms):
|
|
68
|
+
self.ms = wrap64(ms)
|
|
69
|
+
|
|
70
|
+
def __eq__(self, other):
|
|
71
|
+
return isinstance(other, Time) and other.ms == self.ms
|
|
72
|
+
|
|
73
|
+
def __hash__(self):
|
|
74
|
+
return hash(("t", self.ms))
|
|
75
|
+
|
|
76
|
+
def __repr__(self):
|
|
77
|
+
return "Time(%s)" % fmt_time(self.ms)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def wrap64(v):
|
|
81
|
+
v &= 0xFFFFFFFFFFFFFFFF
|
|
82
|
+
return v - (1 << 64) if v & (1 << 63) else v
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def type_of(v):
|
|
86
|
+
if isinstance(v, bool) or isinstance(v, int):
|
|
87
|
+
return INT
|
|
88
|
+
if isinstance(v, float):
|
|
89
|
+
return FLOAT
|
|
90
|
+
if isinstance(v, Str):
|
|
91
|
+
return STR
|
|
92
|
+
if isinstance(v, Time):
|
|
93
|
+
return TIME
|
|
94
|
+
raise TypeError("not a FlowScript value: %r" % (v,))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def zero(t):
|
|
98
|
+
return [0, 0.0, Str(b""), Time(0)][t]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---- floats -----------------------------------------------------------------------------
|
|
102
|
+
# Exact powers of ten (10^0 .. 10^22 are exact doubles).
|
|
103
|
+
P10 = [float(10 ** i) for i in range(23)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _scale_up(m, k):
|
|
107
|
+
"""m * 10^k, k >= 0, as the C VM computes it."""
|
|
108
|
+
while k > 22:
|
|
109
|
+
m = m * 1e22
|
|
110
|
+
k -= 22
|
|
111
|
+
return m * P10[k]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _scale_down(m, k):
|
|
115
|
+
"""m / 10^k, k >= 0."""
|
|
116
|
+
while k > 22:
|
|
117
|
+
m = m / 1e22
|
|
118
|
+
k -= 22
|
|
119
|
+
return m / P10[k]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _decimal_exponent(a):
|
|
123
|
+
"""d with 10^d <= a < 10^(d+1) (approximately; corrected by the caller), a > 0 finite."""
|
|
124
|
+
d = 0
|
|
125
|
+
if a >= 1.0:
|
|
126
|
+
while d < 400 and _scale_up(1.0, d + 1) <= a:
|
|
127
|
+
d += 1
|
|
128
|
+
else:
|
|
129
|
+
while d > -400 and _scale_up(a, -d) < 1.0:
|
|
130
|
+
d -= 1
|
|
131
|
+
return d
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _digits15(a, d):
|
|
135
|
+
"""round(a * 10^(14-d)) as an int (15 significant digits when d is right)."""
|
|
136
|
+
s = 14 - d
|
|
137
|
+
m = _scale_up(a, s) if s >= 0 else _scale_down(a, -s)
|
|
138
|
+
return int(m + 0.5)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def fmt_float(x):
|
|
142
|
+
"""Canonical text of a double: 15 significant digits, Python-like layout."""
|
|
143
|
+
if x != x:
|
|
144
|
+
return "nan"
|
|
145
|
+
if x in (math.inf, -math.inf):
|
|
146
|
+
return "inf" if x > 0 else "-inf"
|
|
147
|
+
if x == 0.0:
|
|
148
|
+
return "-0.0" if math.copysign(1.0, x) < 0 else "0.0"
|
|
149
|
+
neg = x < 0
|
|
150
|
+
a = -x if neg else x
|
|
151
|
+
d = _decimal_exponent(a)
|
|
152
|
+
n = _digits15(a, d)
|
|
153
|
+
if n >= 10 ** 15:
|
|
154
|
+
n = (n + 5) // 10
|
|
155
|
+
d += 1
|
|
156
|
+
elif n < 10 ** 14:
|
|
157
|
+
d -= 1
|
|
158
|
+
n = _digits15(a, d)
|
|
159
|
+
if n >= 10 ** 15:
|
|
160
|
+
n = (n + 5) // 10
|
|
161
|
+
d += 1
|
|
162
|
+
digits = str(n).rstrip("0") or "0"
|
|
163
|
+
if -4 <= d < 15:
|
|
164
|
+
if d >= 0:
|
|
165
|
+
whole = digits[:d + 1].ljust(d + 1, "0")
|
|
166
|
+
frac = digits[d + 1:] or "0"
|
|
167
|
+
out = whole + "." + frac
|
|
168
|
+
else:
|
|
169
|
+
out = "0." + "0" * (-d - 1) + digits
|
|
170
|
+
else:
|
|
171
|
+
mant = digits[0] + ("." + digits[1:] if len(digits) > 1 else "")
|
|
172
|
+
out = "%se%s%02d" % (mant, "-" if d < 0 else "+", abs(d))
|
|
173
|
+
return "-" + out if neg else out
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_float(b):
|
|
177
|
+
"""bytes -> float, or None if not a number. [+-]digits[.digits][(e|E)[+-]digits], nan, inf."""
|
|
178
|
+
s = b.decode("latin-1") if isinstance(b, bytes) else b
|
|
179
|
+
i, n = 0, len(s)
|
|
180
|
+
neg = False
|
|
181
|
+
if i < n and s[i] in "+-":
|
|
182
|
+
neg = s[i] == "-"
|
|
183
|
+
i += 1
|
|
184
|
+
rest = s[i:]
|
|
185
|
+
if rest in ("nan", "inf"):
|
|
186
|
+
v = math.nan if rest == "nan" else math.inf
|
|
187
|
+
return -v if neg and rest == "inf" else v
|
|
188
|
+
# digits: the first 18 significant ones are kept in mant; exp is the power of ten to apply
|
|
189
|
+
mant = 0
|
|
190
|
+
ndig = 0
|
|
191
|
+
exp = 0
|
|
192
|
+
seen = 0
|
|
193
|
+
while i < n and "0" <= s[i] <= "9":
|
|
194
|
+
seen += 1
|
|
195
|
+
if ndig < 18:
|
|
196
|
+
mant = mant * 10 + (ord(s[i]) - 48)
|
|
197
|
+
if mant:
|
|
198
|
+
ndig += 1
|
|
199
|
+
else:
|
|
200
|
+
exp += 1
|
|
201
|
+
i += 1
|
|
202
|
+
if i < n and s[i] == ".":
|
|
203
|
+
i += 1
|
|
204
|
+
while i < n and "0" <= s[i] <= "9":
|
|
205
|
+
seen += 1
|
|
206
|
+
if ndig < 18:
|
|
207
|
+
mant = mant * 10 + (ord(s[i]) - 48)
|
|
208
|
+
if mant:
|
|
209
|
+
ndig += 1
|
|
210
|
+
exp -= 1
|
|
211
|
+
i += 1
|
|
212
|
+
if not seen:
|
|
213
|
+
return None
|
|
214
|
+
if i < n and s[i] in "eE":
|
|
215
|
+
i += 1
|
|
216
|
+
eneg = False
|
|
217
|
+
if i < n and s[i] in "+-":
|
|
218
|
+
eneg = s[i] == "-"
|
|
219
|
+
i += 1
|
|
220
|
+
if i >= n or not ("0" <= s[i] <= "9"):
|
|
221
|
+
return None
|
|
222
|
+
e = 0
|
|
223
|
+
while i < n and "0" <= s[i] <= "9":
|
|
224
|
+
if e < 100000:
|
|
225
|
+
e = e * 10 + (ord(s[i]) - 48)
|
|
226
|
+
i += 1
|
|
227
|
+
exp += -e if eneg else e
|
|
228
|
+
if i != n:
|
|
229
|
+
return None
|
|
230
|
+
v = float(mant)
|
|
231
|
+
if mant:
|
|
232
|
+
if exp > 0:
|
|
233
|
+
v = _scale_up(v, min(exp, 400))
|
|
234
|
+
elif exp < 0:
|
|
235
|
+
v = _scale_down(v, min(-exp, 400))
|
|
236
|
+
return -v if neg else v
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def parse_int(b):
|
|
240
|
+
"""bytes -> int32, or None. [+-]digits, must fit in int32."""
|
|
241
|
+
s = b.decode("latin-1") if isinstance(b, bytes) else b
|
|
242
|
+
i, n = 0, len(s)
|
|
243
|
+
neg = False
|
|
244
|
+
if i < n and s[i] in "+-":
|
|
245
|
+
neg = s[i] == "-"
|
|
246
|
+
i += 1
|
|
247
|
+
if i >= n:
|
|
248
|
+
return None
|
|
249
|
+
v = 0
|
|
250
|
+
while i < n:
|
|
251
|
+
if not ("0" <= s[i] <= "9"):
|
|
252
|
+
return None
|
|
253
|
+
v = v * 10 + (ord(s[i]) - 48)
|
|
254
|
+
if v > 2 ** 31:
|
|
255
|
+
return None
|
|
256
|
+
i += 1
|
|
257
|
+
v = -v if neg else v
|
|
258
|
+
if not -2 ** 31 <= v <= 2 ** 31 - 1:
|
|
259
|
+
return None
|
|
260
|
+
return v
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def float_to_int(x):
|
|
264
|
+
if x != x or x >= 2147483648.0 or x <= -2147483649.0:
|
|
265
|
+
raise ValueFault(BAD_VALUE)
|
|
266
|
+
return int(x) # truncates toward zero, like the C cast
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# libm, so that the reference VM gets the same bits as the C VM on the same machine
|
|
270
|
+
try:
|
|
271
|
+
_libm = ctypes.CDLL(ctypes.util.find_library("m") or None)
|
|
272
|
+
for _f in ("floor", "ceil", "sqrt", "exp", "log", "fmod"):
|
|
273
|
+
getattr(_libm, _f).restype = ctypes.c_double
|
|
274
|
+
getattr(_libm, _f).argtypes = [ctypes.c_double]
|
|
275
|
+
_libm.fmod.argtypes = [ctypes.c_double, ctypes.c_double]
|
|
276
|
+
_libm.pow.restype = ctypes.c_double
|
|
277
|
+
_libm.pow.argtypes = [ctypes.c_double, ctypes.c_double]
|
|
278
|
+
except (OSError, AttributeError): # pragma: no cover
|
|
279
|
+
_libm = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _m(name, *args):
|
|
283
|
+
if _libm is not None:
|
|
284
|
+
return getattr(_libm, name)(*args)
|
|
285
|
+
try: # pragma: no cover
|
|
286
|
+
return {"floor": math.floor, "ceil": math.ceil, "sqrt": math.sqrt, "exp": math.exp,
|
|
287
|
+
"log": math.log, "fmod": math.fmod, "pow": math.pow}[name](*args) * 1.0
|
|
288
|
+
except OverflowError: # pragma: no cover
|
|
289
|
+
return math.inf
|
|
290
|
+
except (ValueError, ZeroDivisionError): # pragma: no cover
|
|
291
|
+
return math.nan
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def fdiv(a, b):
|
|
295
|
+
if b == 0.0:
|
|
296
|
+
if a != a or a == 0.0:
|
|
297
|
+
return math.nan
|
|
298
|
+
return math.copysign(math.inf, a) * math.copysign(1.0, b)
|
|
299
|
+
return a / b
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def fround(x):
|
|
303
|
+
"""Half away from zero, exactly (no a + 0.5 rounding error)."""
|
|
304
|
+
if x != x or x in (math.inf, -math.inf):
|
|
305
|
+
return x
|
|
306
|
+
a = abs(x)
|
|
307
|
+
t = _m("floor", a)
|
|
308
|
+
if a - t >= 0.5:
|
|
309
|
+
t += 1.0
|
|
310
|
+
return math.copysign(t, x)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---- time -------------------------------------------------------------------------------
|
|
314
|
+
def days_from_civil(y, m, d):
|
|
315
|
+
"""Howard Hinnant's algorithm (integer, works for any int64-range year)."""
|
|
316
|
+
y -= m <= 2
|
|
317
|
+
era = y // 400 # floor division (C: adjust the truncating division)
|
|
318
|
+
yoe = y - era * 400
|
|
319
|
+
doy = (153 * (m + (-3 if m > 2 else 9)) + 2) // 5 + d - 1
|
|
320
|
+
doe = yoe * 365 + yoe // 4 - yoe // 100 + doy
|
|
321
|
+
return era * 146097 + doe - 719468
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def civil_from_days(z):
|
|
325
|
+
z += 719468
|
|
326
|
+
era = z // 146097
|
|
327
|
+
doe = z - era * 146097
|
|
328
|
+
yoe = (doe - doe // 1460 + doe // 36524 - doe // 146096) // 365
|
|
329
|
+
y = yoe + era * 400
|
|
330
|
+
doy = doe - (365 * yoe + yoe // 4 - yoe // 100)
|
|
331
|
+
mp = (5 * doy + 2) // 153
|
|
332
|
+
d = doy - (153 * mp + 2) // 5 + 1
|
|
333
|
+
m = mp + (3 if mp < 10 else -9)
|
|
334
|
+
return y + (m <= 2), m, d
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _days_in_month(y, m):
|
|
338
|
+
if m == 2:
|
|
339
|
+
return 29 if (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 else 28
|
|
340
|
+
return 30 if m in (4, 6, 9, 11) else 31
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def make_time(y, mo, d, h=0, mi=0, s=0):
|
|
344
|
+
if not (1 <= y <= 9999 and 1 <= mo <= 12 and 1 <= d <= _days_in_month(y, mo)
|
|
345
|
+
and 0 <= h <= 23 and 0 <= mi <= 59 and 0 <= s <= 59):
|
|
346
|
+
raise ValueFault(BAD_VALUE)
|
|
347
|
+
return Time(days_from_civil(y, mo, d) * DAY_MS + ((h * 60 + mi) * 60 + s) * 1000)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def split_time(ms):
|
|
351
|
+
"""-> (year, month, day, hour, minute, second, millisecond, weekday 0=Monday)"""
|
|
352
|
+
days = ms // DAY_MS
|
|
353
|
+
rem = ms - days * DAY_MS
|
|
354
|
+
y, m, d = civil_from_days(days)
|
|
355
|
+
wd = (days + 3) % 7 # 1970-01-01 was a Thursday
|
|
356
|
+
return y, m, d, rem // 3600000, rem // 60000 % 60, rem // 1000 % 60, rem % 1000, wd
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def fmt_time(ms):
|
|
360
|
+
y, mo, d, h, mi, s, milli, _wd = split_time(ms)
|
|
361
|
+
ys = ("-" if y < 0 else "") + str(abs(y)).rjust(4, "0")
|
|
362
|
+
return "%s-%02d-%02dT%02d:%02d:%02d.%03dZ" % (ys, mo, d, h, mi, s, milli)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def parse_time(b):
|
|
366
|
+
"""ISO 8601: YYYY-MM-DD[(T| )HH:MM[:SS[.f{1,3}]]][Z|(+|-)HH:MM] -> ms, or None."""
|
|
367
|
+
s = b.decode("latin-1") if isinstance(b, bytes) else b
|
|
368
|
+
pos = [0]
|
|
369
|
+
|
|
370
|
+
def num(k):
|
|
371
|
+
i = pos[0]
|
|
372
|
+
part = s[i:i + k]
|
|
373
|
+
if len(part) != k or not all("0" <= c <= "9" for c in part):
|
|
374
|
+
raise ValueError
|
|
375
|
+
pos[0] = i + k
|
|
376
|
+
return int(part)
|
|
377
|
+
|
|
378
|
+
def lit(c):
|
|
379
|
+
if s[pos[0]:pos[0] + 1] != c:
|
|
380
|
+
raise ValueError
|
|
381
|
+
pos[0] += 1
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
y = num(4)
|
|
385
|
+
lit("-")
|
|
386
|
+
mo = num(2)
|
|
387
|
+
lit("-")
|
|
388
|
+
d = num(2)
|
|
389
|
+
h = mi = sec = milli = 0
|
|
390
|
+
if s[pos[0]:pos[0] + 1] in ("T", " ") and pos[0] < len(s):
|
|
391
|
+
pos[0] += 1
|
|
392
|
+
h = num(2)
|
|
393
|
+
lit(":")
|
|
394
|
+
mi = num(2)
|
|
395
|
+
if s[pos[0]:pos[0] + 1] == ":":
|
|
396
|
+
pos[0] += 1
|
|
397
|
+
sec = num(2)
|
|
398
|
+
if s[pos[0]:pos[0] + 1] == ".":
|
|
399
|
+
pos[0] += 1
|
|
400
|
+
k = 0
|
|
401
|
+
while k < 3 and pos[0] < len(s) and "0" <= s[pos[0]] <= "9":
|
|
402
|
+
milli = milli * 10 + int(s[pos[0]])
|
|
403
|
+
pos[0] += 1
|
|
404
|
+
k += 1
|
|
405
|
+
if k == 0:
|
|
406
|
+
raise ValueError
|
|
407
|
+
milli *= (1, 100, 10, 1)[k]
|
|
408
|
+
offset = 0
|
|
409
|
+
c = s[pos[0]:pos[0] + 1]
|
|
410
|
+
if c == "Z":
|
|
411
|
+
pos[0] += 1
|
|
412
|
+
elif c in ("+", "-"):
|
|
413
|
+
pos[0] += 1
|
|
414
|
+
oh = num(2)
|
|
415
|
+
lit(":")
|
|
416
|
+
om = num(2)
|
|
417
|
+
if oh > 23 or om > 59:
|
|
418
|
+
raise ValueError
|
|
419
|
+
offset = (oh * 60 + om) * 60000 * (1 if c == "+" else -1)
|
|
420
|
+
if pos[0] != len(s):
|
|
421
|
+
raise ValueError
|
|
422
|
+
t = make_time(y, mo, d, h, mi, sec)
|
|
423
|
+
except (ValueError, ValueFault):
|
|
424
|
+
return None
|
|
425
|
+
return t.ms + milli - offset
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ---- strings ----------------------------------------------------------------------------
|
|
429
|
+
def quote(b):
|
|
430
|
+
"""Canonical text of a string value in traces: "..." with escapes."""
|
|
431
|
+
out = ['"']
|
|
432
|
+
for c in b:
|
|
433
|
+
if c == 0x22:
|
|
434
|
+
out.append('\\"')
|
|
435
|
+
elif c == 0x5C:
|
|
436
|
+
out.append("\\\\")
|
|
437
|
+
elif c == 0x0A:
|
|
438
|
+
out.append("\\n")
|
|
439
|
+
elif c == 0x09:
|
|
440
|
+
out.append("\\t")
|
|
441
|
+
elif 0x20 <= c < 0x7F:
|
|
442
|
+
out.append(chr(c))
|
|
443
|
+
else:
|
|
444
|
+
out.append("\\x%02x" % c)
|
|
445
|
+
out.append('"')
|
|
446
|
+
return "".join(out)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def show(v):
|
|
450
|
+
"""Canonical text of any value (traces, debugger, str())."""
|
|
451
|
+
t = type_of(v)
|
|
452
|
+
if t == INT:
|
|
453
|
+
return str(v)
|
|
454
|
+
if t == FLOAT:
|
|
455
|
+
return fmt_float(v)
|
|
456
|
+
if t == STR:
|
|
457
|
+
return quote(v.b)
|
|
458
|
+
return fmt_time(v.ms)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def to_text(v):
|
|
462
|
+
"""str(x): like show(), but a string is itself (not quoted)."""
|
|
463
|
+
t = type_of(v)
|
|
464
|
+
if t == STR:
|
|
465
|
+
return v.b
|
|
466
|
+
return show(v).encode("ascii")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def unquote(text):
|
|
470
|
+
"""Inverse of quote() for scenario files / the debugger. Returns bytes or None."""
|
|
471
|
+
if len(text) < 2 or text[0] != '"' or text[-1] != '"':
|
|
472
|
+
return None
|
|
473
|
+
body = text[1:-1].encode("utf-8")
|
|
474
|
+
out = bytearray()
|
|
475
|
+
i = 0
|
|
476
|
+
while i < len(body):
|
|
477
|
+
c = body[i]
|
|
478
|
+
if c == 0x5C:
|
|
479
|
+
if i + 1 >= len(body):
|
|
480
|
+
return None
|
|
481
|
+
e = body[i + 1]
|
|
482
|
+
if e == 0x78: # \xNN
|
|
483
|
+
h = body[i + 2:i + 4]
|
|
484
|
+
if len(h) != 2 or any(ch not in b"0123456789abcdefABCDEF" for ch in h):
|
|
485
|
+
return None
|
|
486
|
+
out.append(int(h, 16))
|
|
487
|
+
i += 4
|
|
488
|
+
continue
|
|
489
|
+
m = {0x6E: 10, 0x74: 9, 0x22: 34, 0x5C: 92}.get(e)
|
|
490
|
+
if m is None:
|
|
491
|
+
return None
|
|
492
|
+
out.append(m)
|
|
493
|
+
i += 2
|
|
494
|
+
continue
|
|
495
|
+
out.append(c)
|
|
496
|
+
i += 1
|
|
497
|
+
return bytes(out)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def parse_literal(t, text):
|
|
501
|
+
"""A value of type t from its text (scenario files, debugger). Raises ValueError."""
|
|
502
|
+
text = text.strip()
|
|
503
|
+
if t == INT:
|
|
504
|
+
v = int(text, 0)
|
|
505
|
+
return wrap(v)
|
|
506
|
+
if t == FLOAT:
|
|
507
|
+
v = parse_float(text.encode())
|
|
508
|
+
if v is None:
|
|
509
|
+
raise ValueError("not a float: %r" % text)
|
|
510
|
+
return v
|
|
511
|
+
if t == STR:
|
|
512
|
+
b = unquote(text)
|
|
513
|
+
if b is None:
|
|
514
|
+
b = text.encode("utf-8")
|
|
515
|
+
return Str(b)
|
|
516
|
+
digits = text[1:] if text.startswith("-") else text
|
|
517
|
+
if digits and all("0" <= c <= "9" for c in digits):
|
|
518
|
+
return Time(int(text))
|
|
519
|
+
ms = parse_time(text.encode())
|
|
520
|
+
if ms is None:
|
|
521
|
+
raise ValueError("not a time: %r (use YYYY-MM-DDTHH:MM:SSZ or ms)" % text)
|
|
522
|
+
return Time(ms)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---- operators (same semantics as the VM opcodes) ----------------------------------------
|
|
526
|
+
def substr(b, start, count):
|
|
527
|
+
n = len(b)
|
|
528
|
+
if start < 0:
|
|
529
|
+
start = 0
|
|
530
|
+
if start > n:
|
|
531
|
+
start = n
|
|
532
|
+
if count < 0:
|
|
533
|
+
count = 0
|
|
534
|
+
end = start + count if count < n - start else n
|
|
535
|
+
return b[start:end]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def find(b, sub):
|
|
539
|
+
return b.find(sub)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def sat32(v):
|
|
543
|
+
return max(-2 ** 31, min(2 ** 31 - 1, v))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def fcompare(op, a, b):
|
|
547
|
+
return int({"==": a == b, "!=": a != b, "<": a < b, "<=": a <= b, ">": a > b, ">=": a >= b}[op])
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# ---- builtin functions --------------------------------------------------------------------
|
|
551
|
+
# id -> (name, param types, return type). The ids are part of the bytecode (BUILTIN n);
|
|
552
|
+
# c/flowscript.c has the same table. Overloads have their own id; the compiler picks one.
|
|
553
|
+
I, F, S, T = INT, FLOAT, STR, TIME
|
|
554
|
+
BUILTINS = [
|
|
555
|
+
("float", (I,), F), ("float", (S,), F), ("float", (T,), F),
|
|
556
|
+
("int", (F,), I), ("int", (S,), I),
|
|
557
|
+
("str", (I,), S), ("str", (F,), S), ("str", (T,), S),
|
|
558
|
+
("time", (S,), T), ("time", (I, I, I), T), ("time", (I, I, I, I, I, I), T),
|
|
559
|
+
("now", (), T), ("date", (T,), T),
|
|
560
|
+
("year", (T,), I), ("month", (T,), I), ("day", (T,), I), ("hour", (T,), I),
|
|
561
|
+
("minute", (T,), I), ("second", (T,), I), ("millisecond", (T,), I), ("weekday", (T,), I),
|
|
562
|
+
("len", (S,), I), ("substr", (S, I, I), S), ("find", (S, S), I),
|
|
563
|
+
("abs", (I,), I), ("abs", (F,), F),
|
|
564
|
+
("min", (I, I), I), ("max", (I, I), I), ("min", (F, F), F), ("max", (F, F), F),
|
|
565
|
+
("floor", (F,), F), ("ceil", (F,), F), ("round", (F,), F), ("sqrt", (F,), F),
|
|
566
|
+
("exp", (F,), F), ("log", (F,), F), ("pow", (F, F), F),
|
|
567
|
+
]
|
|
568
|
+
BUILTIN_NAMES = sorted(set(b[0] for b in BUILTINS))
|
|
569
|
+
IMPURE = {"now"} # never folded at compile time
|
|
570
|
+
# conversions that are the identity (no code): int(int), float(float), str(str), time(time)
|
|
571
|
+
IDENTITY = {("int", I), ("float", F), ("str", S), ("time", T)}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def call_builtin(idx, args, now=None):
|
|
575
|
+
"""Evaluate builtin `idx`. Returns int/float/bytes/Time (bytes: a new string's text).
|
|
576
|
+
|
|
577
|
+
Raises ValueFault for invalid conversions. `now` is a callable returning ms.
|
|
578
|
+
"""
|
|
579
|
+
name, _ptypes, _ret = BUILTINS[idx]
|
|
580
|
+
a = args
|
|
581
|
+
if idx == 0:
|
|
582
|
+
return float(a[0])
|
|
583
|
+
if idx == 1:
|
|
584
|
+
v = parse_float(a[0].b)
|
|
585
|
+
if v is None:
|
|
586
|
+
raise ValueFault(BAD_VALUE)
|
|
587
|
+
return v
|
|
588
|
+
if idx == 2:
|
|
589
|
+
return a[0].ms / 1000.0
|
|
590
|
+
if idx == 3:
|
|
591
|
+
return float_to_int(a[0])
|
|
592
|
+
if idx == 4:
|
|
593
|
+
v = parse_int(a[0].b)
|
|
594
|
+
if v is None:
|
|
595
|
+
raise ValueFault(BAD_VALUE)
|
|
596
|
+
return v
|
|
597
|
+
if idx in (5, 6, 7):
|
|
598
|
+
return to_text(a[0])
|
|
599
|
+
if idx == 8:
|
|
600
|
+
ms = parse_time(a[0].b)
|
|
601
|
+
if ms is None:
|
|
602
|
+
raise ValueFault(BAD_VALUE)
|
|
603
|
+
return Time(ms)
|
|
604
|
+
if idx in (9, 10):
|
|
605
|
+
return make_time(*a)
|
|
606
|
+
if idx == 11:
|
|
607
|
+
return Time(now() if now else 0)
|
|
608
|
+
if idx == 12:
|
|
609
|
+
return Time(a[0].ms // DAY_MS * DAY_MS)
|
|
610
|
+
if 13 <= idx <= 20:
|
|
611
|
+
return wrap(split_time(a[0].ms)[idx - 13])
|
|
612
|
+
if idx == 21:
|
|
613
|
+
return len(a[0].b)
|
|
614
|
+
if idx == 22:
|
|
615
|
+
return substr(a[0].b, a[1], a[2])
|
|
616
|
+
if idx == 23:
|
|
617
|
+
return find(a[0].b, a[1].b)
|
|
618
|
+
if idx == 24:
|
|
619
|
+
return wrap(abs(a[0]))
|
|
620
|
+
if idx == 25:
|
|
621
|
+
return math.copysign(a[0], 1.0)
|
|
622
|
+
if idx in (26, 28):
|
|
623
|
+
return a[1] if a[1] < a[0] else a[0]
|
|
624
|
+
if idx in (27, 29):
|
|
625
|
+
return a[1] if a[1] > a[0] else a[0]
|
|
626
|
+
if idx == 30:
|
|
627
|
+
return _m("floor", a[0])
|
|
628
|
+
if idx == 31:
|
|
629
|
+
return _m("ceil", a[0])
|
|
630
|
+
if idx == 32:
|
|
631
|
+
return fround(a[0])
|
|
632
|
+
if idx in (33, 34, 35):
|
|
633
|
+
return _m(name, a[0])
|
|
634
|
+
if idx == 36:
|
|
635
|
+
return _m("pow", a[0], a[1])
|
|
636
|
+
raise ValueFault(5)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def binary(op, a, b):
|
|
640
|
+
"""Evaluate a binary operator on two values of matching types (after promotion).
|
|
641
|
+
|
|
642
|
+
Returns int/float/Time or bytes (a new string from +). Raises ValueFault / intops.DivZero.
|
|
643
|
+
"""
|
|
644
|
+
from . import intops
|
|
645
|
+
ta, tb = type_of(a), type_of(b)
|
|
646
|
+
if ta == INT and tb == INT:
|
|
647
|
+
return intops.BINARY[op](a, b)
|
|
648
|
+
if ta == FLOAT and tb == FLOAT:
|
|
649
|
+
if op == "+":
|
|
650
|
+
return a + b
|
|
651
|
+
if op == "-":
|
|
652
|
+
return a - b
|
|
653
|
+
if op == "*":
|
|
654
|
+
return a * b
|
|
655
|
+
if op == "/":
|
|
656
|
+
return fdiv(a, b)
|
|
657
|
+
if op == "%":
|
|
658
|
+
return _m("fmod", a, b)
|
|
659
|
+
if op in ("==", "!=", "<", "<=", ">", ">="):
|
|
660
|
+
return fcompare(op, a, b)
|
|
661
|
+
if ta == STR and tb == STR:
|
|
662
|
+
if op == "+":
|
|
663
|
+
return a.b + b.b
|
|
664
|
+
if op in ("==", "!=", "<", "<=", ">", ">="):
|
|
665
|
+
return fcompare(op, a.b, b.b)
|
|
666
|
+
if ta == TIME and tb == INT and op in ("+", "-"):
|
|
667
|
+
return Time(a.ms + b if op == "+" else a.ms - b)
|
|
668
|
+
if ta == INT and tb == TIME and op == "+":
|
|
669
|
+
return Time(b.ms + a)
|
|
670
|
+
if ta == TIME and tb == TIME:
|
|
671
|
+
if op == "-":
|
|
672
|
+
return sat32(wrap64(a.ms - b.ms))
|
|
673
|
+
if op in ("==", "!=", "<", "<=", ">", ">="):
|
|
674
|
+
return fcompare(op, a.ms, b.ms)
|
|
675
|
+
raise ValueFault(TYPE_MISMATCH)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def binary_type(op, ta, tb):
|
|
679
|
+
"""Result type of `a op b` after int->float promotion, or None if not allowed.
|
|
680
|
+
|
|
681
|
+
Returns (result type, promote_left, promote_right)."""
|
|
682
|
+
pl = pr = False
|
|
683
|
+
if {ta, tb} == {INT, FLOAT}:
|
|
684
|
+
pl, pr = ta == INT, tb == INT
|
|
685
|
+
ta = tb = FLOAT
|
|
686
|
+
cmp = op in ("==", "!=", "<", "<=", ">", ">=")
|
|
687
|
+
if ta == tb == INT:
|
|
688
|
+
return INT, pl, pr
|
|
689
|
+
if ta == tb == FLOAT and op in ("+", "-", "*", "/", "%") + ("==", "!=", "<", "<=", ">", ">="):
|
|
690
|
+
return (INT if cmp else FLOAT), pl, pr
|
|
691
|
+
if ta == tb == STR and (op == "+" or cmp):
|
|
692
|
+
return (INT if cmp else STR), pl, pr
|
|
693
|
+
if ta == TIME and tb == INT and op in ("+", "-"):
|
|
694
|
+
return TIME, pl, pr
|
|
695
|
+
if ta == INT and tb == TIME and op == "+":
|
|
696
|
+
return TIME, pl, pr
|
|
697
|
+
if ta == tb == TIME and (op == "-" or cmp):
|
|
698
|
+
return INT, pl, pr
|
|
699
|
+
return None
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def pack_value(v, string_index):
|
|
703
|
+
"""Global initial value in the image: u8 type + 8 bytes payload."""
|
|
704
|
+
t = type_of(v)
|
|
705
|
+
if t == INT:
|
|
706
|
+
return struct.pack("<Bq", INT, v)
|
|
707
|
+
if t == FLOAT:
|
|
708
|
+
return struct.pack("<Bd", FLOAT, v)
|
|
709
|
+
if t == STR:
|
|
710
|
+
return struct.pack("<Bq", STR, string_index(v.b))
|
|
711
|
+
return struct.pack("<Bq", TIME, v.ms)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def unpack_value(data, off, strings):
|
|
715
|
+
t = data[off]
|
|
716
|
+
if t == INT:
|
|
717
|
+
return wrap(struct.unpack_from("<q", data, off + 1)[0])
|
|
718
|
+
if t == FLOAT:
|
|
719
|
+
return struct.unpack_from("<d", data, off + 1)[0]
|
|
720
|
+
if t == STR:
|
|
721
|
+
return Str(strings[struct.unpack_from("<q", data, off + 1)[0]].encode("utf-8", "surrogateescape"))
|
|
722
|
+
if t == TIME:
|
|
723
|
+
return Time(struct.unpack_from("<q", data, off + 1)[0])
|
|
724
|
+
raise ValueError("bad global type %d" % t)
|