calc-flow-python 4.0.0__cp313-abi3-win_amd64.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.
- calc_flow/__init__.py +169 -0
- calc_flow/_native.pyd +0 -0
- calc_flow/_native.pyi +366 -0
- calc_flow/array.py +1324 -0
- calc_flow/capabilities.py +775 -0
- calc_flow/config.py +219 -0
- calc_flow/errors.py +25 -0
- calc_flow/join_spec.py +156 -0
- calc_flow/pipeline.py +1123 -0
- calc_flow/py.typed +0 -0
- calc_flow/runtime.py +979 -0
- calc_flow/store.py +138 -0
- calc_flow/symbolic/__init__.py +65 -0
- calc_flow/symbolic/_generated_rolling_kernels.py +23 -0
- calc_flow/symbolic/analyzer.py +2280 -0
- calc_flow/symbolic/domains.py +77 -0
- calc_flow/symbolic/errors.py +58 -0
- calc_flow/symbolic/expr.py +662 -0
- calc_flow/symbolic/lower/__init__.py +31 -0
- calc_flow/symbolic/lower/planners.py +1270 -0
- calc_flow/symbolic/lower/program.py +840 -0
- calc_flow/symbolic/lower/segments.py +836 -0
- calc_flow/symbolic/lower/strategies.py +1472 -0
- calc_flow/symbolic/nodes.py +603 -0
- calc_flow/symbolic/ops.py +1155 -0
- calc_flow/symbolic/optimizer.py +600 -0
- calc_flow/symbolic/program.py +377 -0
- calc_flow/symbolic/types.py +110 -0
- calc_flow/symbolic/windows.py +153 -0
- calc_flow/udf.py +19 -0
- calc_flow_python-4.0.0.dist-info/METADATA +376 -0
- calc_flow_python-4.0.0.dist-info/RECORD +35 -0
- calc_flow_python-4.0.0.dist-info/WHEEL +4 -0
- calc_flow_python-4.0.0.dist-info/licenses/LICENSE +202 -0
- calc_flow_python-4.0.0.dist-info/sboms/calc-flow-python.cyclonedx.json +10081 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""Canonical declaration values and immutable node identity.
|
|
2
|
+
|
|
3
|
+
Implements the frozen ``calc_flow.symbolic.declaration.v1`` byte format from
|
|
4
|
+
``.codex/artifacts/specs/symbolic-computation-contract.md`` (D2), including
|
|
5
|
+
the versioned primitive reference, the normalized attribute map, and the
|
|
6
|
+
deterministic node digest. Node construction is declaration-only: no data
|
|
7
|
+
object, callable, or execution path is accepted anywhere in this module.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import struct
|
|
16
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Final
|
|
19
|
+
|
|
20
|
+
from calc_flow.symbolic.domains import type_name
|
|
21
|
+
|
|
22
|
+
ENCODING_VERSION = "calc_flow.symbolic.declaration.v1"
|
|
23
|
+
|
|
24
|
+
_MAGIC = ENCODING_VERSION.encode("ascii") + b"\x00"
|
|
25
|
+
_NODE_TAG = 0x20
|
|
26
|
+
_INT64_MIN = -(2**63)
|
|
27
|
+
_UINT64_MAX = 2**64 - 1
|
|
28
|
+
_CANONICAL_NAN = struct.unpack(">d", b"\x7f\xf8\x00\x00\x00\x00\x00\x00")[0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _u64(value: int, /) -> bytes:
|
|
32
|
+
return value.to_bytes(8, "big")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _text(value: str, /) -> bytes:
|
|
36
|
+
encoded = value.encode("utf-8")
|
|
37
|
+
return _u64(len(encoded)) + encoded
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class CNull:
|
|
42
|
+
"""The canonical ``null`` value (tag ``0x00``)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class CBool:
|
|
47
|
+
"""A canonical boolean value; ``false`` is ``0x01`` and ``true`` ``0x02``."""
|
|
48
|
+
|
|
49
|
+
value: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class CInt:
|
|
54
|
+
"""A canonical integer in the portable JSON range (tag ``0x03``)."""
|
|
55
|
+
|
|
56
|
+
value: int
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if type(self.value) is not int:
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"canonical integers must be Python int; got {type_name(self.value)}"
|
|
62
|
+
)
|
|
63
|
+
if not _INT64_MIN <= self.value <= _UINT64_MAX:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"canonical integers must fit the portable JSON range"
|
|
66
|
+
f" [-2^63, 2^64 - 1]; got {self.value}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class CFloat:
|
|
72
|
+
"""A canonical IEEE binary64 value (tag ``0x04``).
|
|
73
|
+
|
|
74
|
+
Every NaN is canonicalized to the single quiet-NaN bit pattern; every
|
|
75
|
+
other value keeps its exact bits, so signed zero and infinity sign are
|
|
76
|
+
retained.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
value: float
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
if type(self.value) is not float:
|
|
83
|
+
raise TypeError(
|
|
84
|
+
f"canonical floats must be Python float; got {type_name(self.value)}"
|
|
85
|
+
)
|
|
86
|
+
if math.isnan(self.value):
|
|
87
|
+
object.__setattr__(self, "value", _CANONICAL_NAN)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class CStr:
|
|
92
|
+
"""A canonical UTF-8 string value (tag ``0x05``)."""
|
|
93
|
+
|
|
94
|
+
value: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class CBytes:
|
|
99
|
+
"""A canonical byte string value (tag ``0x06``).
|
|
100
|
+
|
|
101
|
+
The declaration language itself rejects bytes; the tag exists so the
|
|
102
|
+
encoder is byte-exact against the v1 golden vectors.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
value: bytes
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class CEnum:
|
|
110
|
+
"""A canonical enum reference by family and variant (tag ``0x07``)."""
|
|
111
|
+
|
|
112
|
+
family: str
|
|
113
|
+
variant: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class CSeq:
|
|
118
|
+
"""A canonical sequence retaining declaration order (tag ``0x08``)."""
|
|
119
|
+
|
|
120
|
+
items: tuple[CValue, ...]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True, slots=True)
|
|
124
|
+
class CMap:
|
|
125
|
+
"""A canonical map with keys sorted by raw UTF-8 bytes (tag ``0x09``)."""
|
|
126
|
+
|
|
127
|
+
entries: tuple[tuple[str, CValue], ...]
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_mapping(cls, mapping: Mapping[str, CValue], /) -> CMap:
|
|
131
|
+
entries = tuple(
|
|
132
|
+
sorted(
|
|
133
|
+
mapping.items(),
|
|
134
|
+
key=lambda entry: entry[0].encode("utf-8"),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return cls(entries)
|
|
138
|
+
|
|
139
|
+
def get(self, key: str, /) -> CValue | None:
|
|
140
|
+
for name, value in self.entries:
|
|
141
|
+
if name == key:
|
|
142
|
+
return value
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True, slots=True)
|
|
147
|
+
class CShape:
|
|
148
|
+
"""A canonical shape with known or symbolic dimensions (tag ``0x0a``).
|
|
149
|
+
|
|
150
|
+
Known dimensions must be non-negative so encoding never reaches a
|
|
151
|
+
low-level unsigned-conversion failure.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
dims: tuple[CValue, ...]
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
for dimension in self.dims:
|
|
158
|
+
if isinstance(dimension, CInt) and dimension.value < 0:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"canonical shape dimensions must be non-negative known"
|
|
161
|
+
f" sizes or symbolic identifiers; got {dimension.value}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class CDType:
|
|
167
|
+
"""A canonical Arrow/provider dtype spelling (tag ``0x0b``)."""
|
|
168
|
+
|
|
169
|
+
name: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
CValue = (
|
|
173
|
+
CNull
|
|
174
|
+
| CBool
|
|
175
|
+
| CInt
|
|
176
|
+
| CFloat
|
|
177
|
+
| CStr
|
|
178
|
+
| CBytes
|
|
179
|
+
| CEnum
|
|
180
|
+
| CSeq
|
|
181
|
+
| CMap
|
|
182
|
+
| CShape
|
|
183
|
+
| CDType
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
type ValueEncoder = Callable[[CValue], bytes]
|
|
187
|
+
type ValueFormatter = Callable[[CValue], str]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _encode_cbool(value: CBool, /) -> bytes:
|
|
191
|
+
return b"\x02" if value.value else b"\x01"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _encode_cint(value: CInt, /) -> bytes:
|
|
195
|
+
if value.value < 0:
|
|
196
|
+
return b"\x03\x01" + _u64(-value.value)
|
|
197
|
+
return b"\x03\x00" + _u64(value.value)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _encode_cseq(value: CSeq, /) -> bytes:
|
|
201
|
+
return (
|
|
202
|
+
b"\x08"
|
|
203
|
+
+ _u64(len(value.items))
|
|
204
|
+
+ b"".join(encode_value(item) for item in value.items)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _encode_cmap(value: CMap, /) -> bytes:
|
|
209
|
+
return (
|
|
210
|
+
b"\x09"
|
|
211
|
+
+ _u64(len(value.entries))
|
|
212
|
+
+ b"".join(_text(key) + encode_value(item) for key, item in value.entries)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _encode_cshape(value: CShape, /) -> bytes:
|
|
217
|
+
dimensions = b"".join(_encode_dimension(dim) for dim in value.dims)
|
|
218
|
+
return b"\x0a" + _u64(len(value.dims)) + dimensions
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
_VALUE_ENCODERS: Final[dict[type, ValueEncoder]] = {
|
|
222
|
+
CNull: lambda _value: b"\x00",
|
|
223
|
+
CBool: _encode_cbool,
|
|
224
|
+
CInt: _encode_cint,
|
|
225
|
+
CFloat: lambda value: b"\x04" + struct.pack(">d", value.value),
|
|
226
|
+
CStr: lambda value: b"\x05" + _text(value.value),
|
|
227
|
+
CBytes: lambda value: b"\x06" + _u64(len(value.value)) + value.value,
|
|
228
|
+
CEnum: lambda value: b"\x07" + _text(value.family) + _text(value.variant),
|
|
229
|
+
CSeq: _encode_cseq,
|
|
230
|
+
CMap: _encode_cmap,
|
|
231
|
+
CShape: _encode_cshape,
|
|
232
|
+
CDType: lambda value: b"\x0b" + _text(value.name),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def encode_value(value: CValue, /) -> bytes:
|
|
237
|
+
"""Encode one canonical declaration value to its exact v1 bytes."""
|
|
238
|
+
|
|
239
|
+
encoder = _VALUE_ENCODERS.get(type(value))
|
|
240
|
+
if encoder is None:
|
|
241
|
+
raise TypeError(f"unsupported canonical value; got {type_name(value)}")
|
|
242
|
+
return encoder(value)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _encode_dimension(dimension: CValue, /) -> bytes:
|
|
246
|
+
if isinstance(dimension, CInt):
|
|
247
|
+
if dimension.value < 0:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
"canonical shape dimensions must be non-negative known"
|
|
250
|
+
f" sizes or symbolic identifiers; got {dimension.value}"
|
|
251
|
+
)
|
|
252
|
+
return b"\x00" + _u64(dimension.value)
|
|
253
|
+
if isinstance(dimension, CStr):
|
|
254
|
+
return b"\x01" + _text(dimension.value)
|
|
255
|
+
raise TypeError(
|
|
256
|
+
f"canonical dimensions must be CInt or CStr; got {type_name(dimension)}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True, slots=True)
|
|
261
|
+
class OpRef:
|
|
262
|
+
"""The versioned primitive identity of one declaration node."""
|
|
263
|
+
|
|
264
|
+
name: str
|
|
265
|
+
version: int = 1
|
|
266
|
+
|
|
267
|
+
def __post_init__(self) -> None:
|
|
268
|
+
if type(self.name) is not str:
|
|
269
|
+
raise TypeError(f"OpRef.name must be a string; got {type_name(self.name)}")
|
|
270
|
+
if type(self.version) is not int:
|
|
271
|
+
raise TypeError(
|
|
272
|
+
f"OpRef.version must be an integer; got {type_name(self.version)}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
277
|
+
class Node:
|
|
278
|
+
"""One immutable declaration node with its precomputed canonical bytes.
|
|
279
|
+
|
|
280
|
+
``node_bytes`` is the exact ``NODE_BYTES`` encoding and ``digest`` the
|
|
281
|
+
lowercase hexadecimal SHA-256 of ``MAGIC || 0x01 || BYTES(node_bytes)``.
|
|
282
|
+
Structural identity is ``digest`` plus exact ``node_bytes`` equality.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
op: OpRef
|
|
286
|
+
args: tuple[Node, ...]
|
|
287
|
+
attrs: CMap
|
|
288
|
+
node_bytes: bytes
|
|
289
|
+
digest: str
|
|
290
|
+
|
|
291
|
+
def attr(self, key: str, /) -> CValue | None:
|
|
292
|
+
return self.attrs.get(key)
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
return f"Node({self.op.name}@{self.op.version}, digest={self.digest})"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(frozen=True, slots=True)
|
|
299
|
+
class PrimitiveSpec:
|
|
300
|
+
"""Catalog entry naming the accepted attributes and their defaults."""
|
|
301
|
+
|
|
302
|
+
allowed: frozenset[str]
|
|
303
|
+
defaults: CMap
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
_NO_ATTRS: dict[str, PrimitiveSpec] = {
|
|
307
|
+
name: PrimitiveSpec(frozenset(), CMap(()))
|
|
308
|
+
for name in (
|
|
309
|
+
"add",
|
|
310
|
+
"and",
|
|
311
|
+
"coalesce",
|
|
312
|
+
"eq",
|
|
313
|
+
"exp",
|
|
314
|
+
"filter",
|
|
315
|
+
"ge",
|
|
316
|
+
"gt",
|
|
317
|
+
"le",
|
|
318
|
+
"log",
|
|
319
|
+
"lt",
|
|
320
|
+
"matmul",
|
|
321
|
+
"mul",
|
|
322
|
+
"ne",
|
|
323
|
+
"neg",
|
|
324
|
+
"not",
|
|
325
|
+
"or",
|
|
326
|
+
"sqrt",
|
|
327
|
+
"sub",
|
|
328
|
+
"truediv",
|
|
329
|
+
"where",
|
|
330
|
+
"abs",
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_PRIMITIVES: dict[str, PrimitiveSpec] = {
|
|
335
|
+
"table_input": PrimitiveSpec(
|
|
336
|
+
frozenset({"name", "schema", "entity_by", "event_time", "sequence_by"}),
|
|
337
|
+
CMap.from_mapping(
|
|
338
|
+
{
|
|
339
|
+
"entity_by": CSeq(()),
|
|
340
|
+
"event_time": CNull(),
|
|
341
|
+
"sequence_by": CSeq(()),
|
|
342
|
+
}
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
"parameter": PrimitiveSpec(
|
|
346
|
+
frozenset(
|
|
347
|
+
{"name", "kind", "mutability", "schema", "backend", "dtype", "shape"}
|
|
348
|
+
),
|
|
349
|
+
CMap(()),
|
|
350
|
+
),
|
|
351
|
+
"stream_join": PrimitiveSpec(
|
|
352
|
+
frozenset(
|
|
353
|
+
{
|
|
354
|
+
"left_keys",
|
|
355
|
+
"right_keys",
|
|
356
|
+
"left_event_time",
|
|
357
|
+
"right_event_time",
|
|
358
|
+
"before_micros",
|
|
359
|
+
"after_micros",
|
|
360
|
+
"max_state_rows_per_side",
|
|
361
|
+
"max_state_bytes_per_side",
|
|
362
|
+
"max_matches_per_input_batch",
|
|
363
|
+
"left_prefix",
|
|
364
|
+
"right_prefix",
|
|
365
|
+
"output_entity_by",
|
|
366
|
+
"output_event_time",
|
|
367
|
+
"output_sequence_by",
|
|
368
|
+
}
|
|
369
|
+
),
|
|
370
|
+
CMap.from_mapping(
|
|
371
|
+
{
|
|
372
|
+
"left_prefix": CStr("left"),
|
|
373
|
+
"right_prefix": CStr("right"),
|
|
374
|
+
}
|
|
375
|
+
),
|
|
376
|
+
),
|
|
377
|
+
"column_ref": PrimitiveSpec(frozenset({"name"}), CMap(())),
|
|
378
|
+
"literal": PrimitiveSpec(frozenset({"value"}), CMap(())),
|
|
379
|
+
"clip": PrimitiveSpec(frozenset({"lower", "upper"}), CMap(())),
|
|
380
|
+
"cast": PrimitiveSpec(frozenset({"data_type"}), CMap(())),
|
|
381
|
+
"lag": PrimitiveSpec(
|
|
382
|
+
frozenset({"periods"}),
|
|
383
|
+
CMap.from_mapping({"periods": CInt(1)}),
|
|
384
|
+
),
|
|
385
|
+
"delta": PrimitiveSpec(
|
|
386
|
+
frozenset({"periods"}),
|
|
387
|
+
CMap.from_mapping({"periods": CInt(1)}),
|
|
388
|
+
),
|
|
389
|
+
"ewma": PrimitiveSpec(
|
|
390
|
+
frozenset({"span", "min_periods"}),
|
|
391
|
+
CMap.from_mapping({"min_periods": CInt(1)}),
|
|
392
|
+
),
|
|
393
|
+
**{
|
|
394
|
+
name: PrimitiveSpec(
|
|
395
|
+
frozenset({"frame", "min_periods"}),
|
|
396
|
+
CMap.from_mapping({"min_periods": CInt(1)}),
|
|
397
|
+
)
|
|
398
|
+
for name in ("count", "sum", "mean", "min", "max")
|
|
399
|
+
},
|
|
400
|
+
**{
|
|
401
|
+
name: PrimitiveSpec(
|
|
402
|
+
frozenset({"frame", "min_periods", "ddof"}),
|
|
403
|
+
CMap.from_mapping({"min_periods": CInt(1), "ddof": CInt(1)}),
|
|
404
|
+
)
|
|
405
|
+
for name in ("variance", "stddev", "covariance", "correlation")
|
|
406
|
+
},
|
|
407
|
+
**{
|
|
408
|
+
name: PrimitiveSpec(
|
|
409
|
+
frozenset(
|
|
410
|
+
{
|
|
411
|
+
"grouping",
|
|
412
|
+
"direction",
|
|
413
|
+
"tie_method",
|
|
414
|
+
"null_placement",
|
|
415
|
+
"min_samples",
|
|
416
|
+
}
|
|
417
|
+
),
|
|
418
|
+
CMap.from_mapping(
|
|
419
|
+
{
|
|
420
|
+
"direction": CEnum("direction", "ascending"),
|
|
421
|
+
"tie_method": CEnum("rank_tie_method", "average"),
|
|
422
|
+
"null_placement": CEnum("null_placement", "exclude"),
|
|
423
|
+
"min_samples": CInt(1),
|
|
424
|
+
}
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
for name in ("rank", "percentile")
|
|
428
|
+
},
|
|
429
|
+
"demean": PrimitiveSpec(
|
|
430
|
+
frozenset({"grouping", "min_samples"}),
|
|
431
|
+
CMap.from_mapping({"min_samples": CInt(1)}),
|
|
432
|
+
),
|
|
433
|
+
"zscore": PrimitiveSpec(
|
|
434
|
+
frozenset({"grouping", "min_samples", "ddof"}),
|
|
435
|
+
CMap.from_mapping({"min_samples": CInt(1), "ddof": CInt(0)}),
|
|
436
|
+
),
|
|
437
|
+
"winsorize": PrimitiveSpec(
|
|
438
|
+
frozenset({"grouping", "min_samples", "lower", "upper"}),
|
|
439
|
+
CMap.from_mapping({"min_samples": CInt(1)}),
|
|
440
|
+
),
|
|
441
|
+
**{
|
|
442
|
+
name: PrimitiveSpec(
|
|
443
|
+
frozenset({"grouping", "count", "include_ties", "min_samples"}),
|
|
444
|
+
CMap.from_mapping({"include_ties": CBool(True), "min_samples": CInt(1)}),
|
|
445
|
+
)
|
|
446
|
+
for name in ("top", "bottom")
|
|
447
|
+
},
|
|
448
|
+
"mean_fill": PrimitiveSpec(
|
|
449
|
+
frozenset({"grouping", "min_samples"}),
|
|
450
|
+
CMap.from_mapping({"min_samples": CInt(1)}),
|
|
451
|
+
),
|
|
452
|
+
"project": PrimitiveSpec(frozenset({"columns"}), CMap(())),
|
|
453
|
+
"with_columns": PrimitiveSpec(frozenset({"names"}), CMap(())),
|
|
454
|
+
"attach_columns": PrimitiveSpec(frozenset({"names"}), CMap(())),
|
|
455
|
+
"from_columns": PrimitiveSpec(frozenset({"columns", "backend"}), CMap(())),
|
|
456
|
+
"window_tumbling": PrimitiveSpec(
|
|
457
|
+
frozenset({"event_time", "size_micros", "group_by"}),
|
|
458
|
+
CMap.from_mapping({"group_by": CSeq(())}),
|
|
459
|
+
),
|
|
460
|
+
"window_hopping": PrimitiveSpec(
|
|
461
|
+
frozenset({"event_time", "size_micros", "slide_micros", "group_by"}),
|
|
462
|
+
CMap.from_mapping({"group_by": CSeq(())}),
|
|
463
|
+
),
|
|
464
|
+
**_NO_ATTRS,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def build(
|
|
469
|
+
name: str,
|
|
470
|
+
args: Sequence[Node],
|
|
471
|
+
attrs: Mapping[str, CValue],
|
|
472
|
+
/,
|
|
473
|
+
*,
|
|
474
|
+
version: int = 1,
|
|
475
|
+
) -> Node:
|
|
476
|
+
"""Build one normalized node with materialized defaults and its digest."""
|
|
477
|
+
|
|
478
|
+
spec = _PRIMITIVES.get(name)
|
|
479
|
+
if spec is None:
|
|
480
|
+
raise ValueError(f"unknown symbolic primitive {name!r}")
|
|
481
|
+
unknown = frozenset(attrs) - spec.allowed
|
|
482
|
+
if unknown:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f"primitive {name!r} does not accept attributes {sorted(unknown)}"
|
|
485
|
+
)
|
|
486
|
+
merged: dict[str, CValue] = dict(spec.defaults.entries)
|
|
487
|
+
merged.update(attrs)
|
|
488
|
+
attr_map = CMap.from_mapping(merged)
|
|
489
|
+
children = tuple(args)
|
|
490
|
+
body = (
|
|
491
|
+
bytes((_NODE_TAG,))
|
|
492
|
+
+ _text(name)
|
|
493
|
+
+ _text(str(version))
|
|
494
|
+
+ _u64(len(children))
|
|
495
|
+
+ b"".join(bytes.fromhex(child.digest) for child in children)
|
|
496
|
+
+ encode_value(attr_map)
|
|
497
|
+
)
|
|
498
|
+
digest = hashlib.sha256(_MAGIC + b"\x01" + _u64(len(body)) + body).hexdigest()
|
|
499
|
+
return Node(OpRef(name, version), children, attr_map, body, digest)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _int_literal(value: int, /) -> CValue:
|
|
503
|
+
if not _INT64_MIN <= value <= _UINT64_MAX:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
"calc_flow.symbolic.literal.value: invalid_literal: integer"
|
|
506
|
+
" literals must fit the portable JSON range [-2^63, 2^64 - 1]"
|
|
507
|
+
)
|
|
508
|
+
return CInt(value)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _float_literal(value: float, /) -> CValue:
|
|
512
|
+
if not math.isfinite(value):
|
|
513
|
+
raise ValueError(
|
|
514
|
+
"calc_flow.symbolic.literal.value: invalid_literal:"
|
|
515
|
+
" floating-point literals must be finite"
|
|
516
|
+
)
|
|
517
|
+
return CFloat(value)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
_LITERAL_HANDLERS: Final[dict[type, Callable[[object], CValue]]] = {
|
|
521
|
+
type(None): lambda _value: CNull(),
|
|
522
|
+
bool: lambda value: CBool(value),
|
|
523
|
+
int: _int_literal,
|
|
524
|
+
float: _float_literal,
|
|
525
|
+
str: lambda value: CStr(value),
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def literal_value(value: object, /) -> CValue:
|
|
530
|
+
"""Convert one strict JSON scalar to its canonical value.
|
|
531
|
+
|
|
532
|
+
Non-finite floats and integers outside the portable JSON range are
|
|
533
|
+
rejected before any node is built.
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
handler = _LITERAL_HANDLERS.get(type(value))
|
|
537
|
+
if handler is None:
|
|
538
|
+
raise ValueError(
|
|
539
|
+
"calc_flow.symbolic.literal.value: invalid_literal: declaration"
|
|
540
|
+
f" literals must be strict JSON scalars; got {type_name(value)}"
|
|
541
|
+
)
|
|
542
|
+
return handler(value)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _format_cseq(value: CSeq, /) -> str:
|
|
546
|
+
return "[" + ", ".join(format_value(item) for item in value.items) + "]"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _format_cmap(value: CMap, /) -> str:
|
|
550
|
+
return (
|
|
551
|
+
"{"
|
|
552
|
+
+ ", ".join(f"{key}={format_value(item)}" for key, item in value.entries)
|
|
553
|
+
+ "}"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
_VALUE_FORMATTERS: Final[dict[type, ValueFormatter]] = {
|
|
558
|
+
CNull: lambda _value: "null",
|
|
559
|
+
CBool: lambda value: "true" if value.value else "false",
|
|
560
|
+
CInt: lambda value: str(value.value),
|
|
561
|
+
CFloat: lambda value: repr(value.value),
|
|
562
|
+
CStr: lambda value: json.dumps(value.value),
|
|
563
|
+
CDType: lambda value: value.name,
|
|
564
|
+
CBytes: lambda value: f"bytes({value.value.hex()})",
|
|
565
|
+
CEnum: lambda value: f"{value.family}.{value.variant}",
|
|
566
|
+
CSeq: _format_cseq,
|
|
567
|
+
CMap: _format_cmap,
|
|
568
|
+
CShape: lambda value: (
|
|
569
|
+
"shape(" + ", ".join(format_value(dim) for dim in value.dims) + ")"
|
|
570
|
+
),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def format_value(value: CValue, /) -> str:
|
|
575
|
+
"""Render one canonical value deterministically for ``explain`` output."""
|
|
576
|
+
|
|
577
|
+
formatter = _VALUE_FORMATTERS.get(type(value))
|
|
578
|
+
if formatter is None:
|
|
579
|
+
raise TypeError(f"unsupported canonical value; got {type_name(value)}")
|
|
580
|
+
return formatter(value)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def explain_node(node: Node, /) -> str:
|
|
584
|
+
"""Render the complete declaration tree deterministically."""
|
|
585
|
+
|
|
586
|
+
def header(current: Node) -> str:
|
|
587
|
+
attrs = ", ".join(
|
|
588
|
+
f"{key}={format_value(item)}" for key, item in current.attrs.entries
|
|
589
|
+
)
|
|
590
|
+
rendered = f"[{attrs}]" if attrs else ""
|
|
591
|
+
return (
|
|
592
|
+
f"{current.op.name}@{current.op.version}{rendered} digest={current.digest}"
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
lines = [header(node)]
|
|
596
|
+
|
|
597
|
+
def walk(current: Node, depth: int) -> None:
|
|
598
|
+
for index, child in enumerate(current.args):
|
|
599
|
+
lines.append(" " * depth + f"[{index}] {header(child)}")
|
|
600
|
+
walk(child, depth + 1)
|
|
601
|
+
|
|
602
|
+
walk(node, 1)
|
|
603
|
+
return "\n".join(lines)
|