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