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.
@@ -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)