@telorun/cel 0.0.0-stage → 0.107.0

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.
Files changed (223) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +281 -2
  3. package/dist/activation.d.ts +27 -0
  4. package/dist/activation.d.ts.map +1 -0
  5. package/dist/activation.js +24 -0
  6. package/dist/backend-runtime.d.ts +146 -0
  7. package/dist/backend-runtime.d.ts.map +1 -0
  8. package/dist/backend-runtime.js +322 -0
  9. package/dist/bounded-cache.d.ts +21 -0
  10. package/dist/bounded-cache.d.ts.map +1 -0
  11. package/dist/bounded-cache.js +42 -0
  12. package/dist/catalog-runtime.d.ts +59 -0
  13. package/dist/catalog-runtime.d.ts.map +1 -0
  14. package/dist/catalog-runtime.js +785 -0
  15. package/dist/cel-expression.d.ts +32 -0
  16. package/dist/cel-expression.d.ts.map +1 -0
  17. package/dist/cel-expression.js +29 -0
  18. package/dist/cel-map-value.d.ts +34 -0
  19. package/dist/cel-map-value.d.ts.map +1 -0
  20. package/dist/cel-map-value.js +74 -0
  21. package/dist/cel-program.d.ts +44 -0
  22. package/dist/cel-program.d.ts.map +1 -0
  23. package/dist/cel-program.js +72 -0
  24. package/dist/cel-type.d.ts +131 -0
  25. package/dist/cel-type.d.ts.map +1 -0
  26. package/dist/cel-type.js +293 -0
  27. package/dist/cel-value.d.ts +159 -0
  28. package/dist/cel-value.d.ts.map +1 -0
  29. package/dist/cel-value.js +225 -0
  30. package/dist/check-diagnostic.d.ts +53 -0
  31. package/dist/check-diagnostic.d.ts.map +1 -0
  32. package/dist/check-diagnostic.js +66 -0
  33. package/dist/checker.d.ts +77 -0
  34. package/dist/checker.d.ts.map +1 -0
  35. package/dist/checker.js +721 -0
  36. package/dist/closure-backend.d.ts +21 -0
  37. package/dist/closure-backend.d.ts.map +1 -0
  38. package/dist/closure-backend.js +436 -0
  39. package/dist/comprehension-bindings.d.ts +33 -0
  40. package/dist/comprehension-bindings.d.ts.map +1 -0
  41. package/dist/comprehension-bindings.js +50 -0
  42. package/dist/comprehension-runtime.d.ts +44 -0
  43. package/dist/comprehension-runtime.d.ts.map +1 -0
  44. package/dist/comprehension-runtime.js +137 -0
  45. package/dist/declared-chain.d.ts +35 -0
  46. package/dist/declared-chain.d.ts.map +1 -0
  47. package/dist/declared-chain.js +36 -0
  48. package/dist/duration-value.d.ts +59 -0
  49. package/dist/duration-value.d.ts.map +1 -0
  50. package/dist/duration-value.js +135 -0
  51. package/dist/emitted-module.d.ts +207 -0
  52. package/dist/emitted-module.d.ts.map +1 -0
  53. package/dist/emitted-module.js +359 -0
  54. package/dist/engine-version.d.ts +3 -0
  55. package/dist/engine-version.d.ts.map +1 -0
  56. package/dist/engine-version.js +8 -0
  57. package/dist/environment-digest.d.ts +44 -0
  58. package/dist/environment-digest.d.ts.map +1 -0
  59. package/dist/environment-digest.js +98 -0
  60. package/dist/environment.d.ts +283 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +459 -0
  63. package/dist/function-catalog.d.ts +66 -0
  64. package/dist/function-catalog.d.ts.map +1 -0
  65. package/dist/function-catalog.js +77 -0
  66. package/dist/function-registry.d.ts +78 -0
  67. package/dist/function-registry.d.ts.map +1 -0
  68. package/dist/function-registry.js +189 -0
  69. package/dist/index.d.ts +91 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +59 -0
  72. package/dist/integer-arithmetic.d.ts +27 -0
  73. package/dist/integer-arithmetic.d.ts.map +1 -0
  74. package/dist/integer-arithmetic.js +58 -0
  75. package/dist/js-emitter.d.ts +132 -0
  76. package/dist/js-emitter.d.ts.map +1 -0
  77. package/dist/js-emitter.js +562 -0
  78. package/dist/json-schema-type.d.ts +182 -0
  79. package/dist/json-schema-type.d.ts.map +1 -0
  80. package/dist/json-schema-type.js +487 -0
  81. package/dist/json-text-scan.d.ts +28 -0
  82. package/dist/json-text-scan.d.ts.map +1 -0
  83. package/dist/json-text-scan.js +159 -0
  84. package/dist/lexer.d.ts +103 -0
  85. package/dist/lexer.d.ts.map +1 -0
  86. package/dist/lexer.js +458 -0
  87. package/dist/macro-check.d.ts +33 -0
  88. package/dist/macro-check.d.ts.map +1 -0
  89. package/dist/macro-check.js +162 -0
  90. package/dist/macro-shape.d.ts +24 -0
  91. package/dist/macro-shape.d.ts.map +1 -0
  92. package/dist/macro-shape.js +55 -0
  93. package/dist/member-read.d.ts +52 -0
  94. package/dist/member-read.d.ts.map +1 -0
  95. package/dist/member-read.js +125 -0
  96. package/dist/namespace-resolution.d.ts +35 -0
  97. package/dist/namespace-resolution.d.ts.map +1 -0
  98. package/dist/namespace-resolution.js +160 -0
  99. package/dist/nominal-type.d.ts +63 -0
  100. package/dist/nominal-type.d.ts.map +1 -0
  101. package/dist/nominal-type.js +98 -0
  102. package/dist/nullable-access.d.ts +38 -0
  103. package/dist/nullable-access.d.ts.map +1 -0
  104. package/dist/nullable-access.js +93 -0
  105. package/dist/parse-limits.d.ts +26 -0
  106. package/dist/parse-limits.d.ts.map +1 -0
  107. package/dist/parse-limits.js +21 -0
  108. package/dist/parser.d.ts +48 -0
  109. package/dist/parser.d.ts.map +1 -0
  110. package/dist/parser.js +503 -0
  111. package/dist/qualified-calls.d.ts +22 -0
  112. package/dist/qualified-calls.d.ts.map +1 -0
  113. package/dist/qualified-calls.js +27 -0
  114. package/dist/regular-expression.d.ts +48 -0
  115. package/dist/regular-expression.d.ts.map +1 -0
  116. package/dist/regular-expression.js +77 -0
  117. package/dist/reserved-words.d.ts +52 -0
  118. package/dist/reserved-words.d.ts.map +1 -0
  119. package/dist/reserved-words.js +77 -0
  120. package/dist/resolved-call.d.ts +33 -0
  121. package/dist/resolved-call.d.ts.map +1 -0
  122. package/dist/resolved-call.js +15 -0
  123. package/dist/root-references.d.ts +23 -0
  124. package/dist/root-references.d.ts.map +1 -0
  125. package/dist/root-references.js +120 -0
  126. package/dist/runtime-library.d.ts +56 -0
  127. package/dist/runtime-library.d.ts.map +1 -0
  128. package/dist/runtime-library.js +545 -0
  129. package/dist/serializer.d.ts +24 -0
  130. package/dist/serializer.d.ts.map +1 -0
  131. package/dist/serializer.js +240 -0
  132. package/dist/sha256.d.ts +20 -0
  133. package/dist/sha256.d.ts.map +1 -0
  134. package/dist/sha256.js +103 -0
  135. package/dist/signature.d.ts +72 -0
  136. package/dist/signature.d.ts.map +1 -0
  137. package/dist/signature.js +61 -0
  138. package/dist/signatures/function-catalog.json +788 -0
  139. package/dist/signatures/standard-library.json +229 -0
  140. package/dist/standard-library.d.ts +41 -0
  141. package/dist/standard-library.d.ts.map +1 -0
  142. package/dist/standard-library.js +85 -0
  143. package/dist/syntax-diagnostic.d.ts +61 -0
  144. package/dist/syntax-diagnostic.d.ts.map +1 -0
  145. package/dist/syntax-diagnostic.js +28 -0
  146. package/dist/syntax-tree.d.ts +160 -0
  147. package/dist/syntax-tree.d.ts.map +1 -0
  148. package/dist/syntax-tree.js +58 -0
  149. package/dist/timestamp-value.d.ts +53 -0
  150. package/dist/timestamp-value.d.ts.map +1 -0
  151. package/dist/timestamp-value.js +223 -0
  152. package/dist/tree-equality.d.ts +15 -0
  153. package/dist/tree-equality.d.ts.map +1 -0
  154. package/dist/tree-equality.js +105 -0
  155. package/dist/type-expression.d.ts +26 -0
  156. package/dist/type-expression.d.ts.map +1 -0
  157. package/dist/type-expression.js +134 -0
  158. package/dist/value-equality.d.ts +38 -0
  159. package/dist/value-equality.d.ts.map +1 -0
  160. package/dist/value-equality.js +196 -0
  161. package/dist/value-text.d.ts +25 -0
  162. package/dist/value-text.d.ts.map +1 -0
  163. package/dist/value-text.js +44 -0
  164. package/dist/zoned-calendar.d.ts +61 -0
  165. package/dist/zoned-calendar.d.ts.map +1 -0
  166. package/dist/zoned-calendar.js +143 -0
  167. package/package.json +56 -3
  168. package/src/activation.ts +32 -0
  169. package/src/backend-runtime.ts +454 -0
  170. package/src/bounded-cache.ts +45 -0
  171. package/src/catalog-runtime.ts +921 -0
  172. package/src/cel-expression.ts +53 -0
  173. package/src/cel-map-value.ts +86 -0
  174. package/src/cel-program.ts +103 -0
  175. package/src/cel-type.ts +359 -0
  176. package/src/cel-value.ts +353 -0
  177. package/src/check-diagnostic.ts +102 -0
  178. package/src/checker.ts +932 -0
  179. package/src/closure-backend.ts +502 -0
  180. package/src/comprehension-bindings.ts +66 -0
  181. package/src/comprehension-runtime.ts +157 -0
  182. package/src/declared-chain.ts +45 -0
  183. package/src/duration-value.ts +145 -0
  184. package/src/emitted-module.ts +494 -0
  185. package/src/engine-version.ts +9 -0
  186. package/src/environment-digest.ts +111 -0
  187. package/src/environment.ts +740 -0
  188. package/src/function-catalog.ts +140 -0
  189. package/src/function-registry.ts +229 -0
  190. package/src/index.ts +386 -0
  191. package/src/integer-arithmetic.ts +64 -0
  192. package/src/js-emitter.ts +713 -0
  193. package/src/json-schema-type.ts +664 -0
  194. package/src/json-text-scan.ts +163 -0
  195. package/src/lexer.ts +562 -0
  196. package/src/macro-check.ts +191 -0
  197. package/src/macro-shape.ts +66 -0
  198. package/src/member-read.ts +126 -0
  199. package/src/namespace-resolution.ts +167 -0
  200. package/src/nominal-type.ts +149 -0
  201. package/src/nullable-access.ts +95 -0
  202. package/src/parse-limits.ts +36 -0
  203. package/src/parser.ts +554 -0
  204. package/src/qualified-calls.ts +39 -0
  205. package/src/regular-expression.ts +101 -0
  206. package/src/reserved-words.ts +94 -0
  207. package/src/resolved-call.ts +34 -0
  208. package/src/root-references.ts +126 -0
  209. package/src/runtime-library.ts +615 -0
  210. package/src/serializer.ts +246 -0
  211. package/src/sha256.ts +112 -0
  212. package/src/signature.ts +127 -0
  213. package/src/signatures/function-catalog.json +788 -0
  214. package/src/signatures/standard-library.json +235 -0
  215. package/src/standard-library.ts +149 -0
  216. package/src/syntax-diagnostic.ts +72 -0
  217. package/src/syntax-tree.ts +229 -0
  218. package/src/timestamp-value.ts +294 -0
  219. package/src/tree-equality.ts +130 -0
  220. package/src/type-expression.ts +160 -0
  221. package/src/value-equality.ts +201 -0
  222. package/src/value-text.ts +45 -0
  223. package/src/zoned-calendar.ts +178 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The canonical CEL syntax tree.
3
+ *
4
+ * Every node carries `range`, the half-open `[start, end)` span of UTF-16 code
5
+ * units it covers in the source it was parsed from, so a diagnostic, a hover or a
6
+ * rename can point at the text the author wrote. A tree built by hand carries
7
+ * ranges too — the serializer and the queries read them, and a node with none
8
+ * would make either answer silently wrong.
9
+ *
10
+ * Literals hold their decoded value in a tagged form rather than as a bare
11
+ * JavaScript value: CEL's `int` and `uint` are both arbitrary-magnitude integers,
12
+ * `1` and `1u` are different expressions, and a backend must be able to tell them
13
+ * apart without consulting a runtime value class.
14
+ *
15
+ * `qcall` is the one node the parser never produces — see `namespace-resolution.ts`.
16
+ */
17
+
18
+ /** Half-open span of UTF-16 code units: `[start, end)`. */
19
+ export type SourceRange = readonly [start: number, end: number];
20
+
21
+ export type CelLiteral =
22
+ | { readonly type: "int"; readonly value: bigint }
23
+ | { readonly type: "uint"; readonly value: bigint }
24
+ | { readonly type: "double"; readonly value: number }
25
+ | { readonly type: "string"; readonly value: string }
26
+ | { readonly type: "bytes"; readonly value: Uint8Array }
27
+ | { readonly type: "bool"; readonly value: boolean }
28
+ | { readonly type: "null" };
29
+
30
+ export type CelUnaryOperator = "!" | "-";
31
+
32
+ export type CelBinaryOperator =
33
+ | "||"
34
+ | "&&"
35
+ | "=="
36
+ | "!="
37
+ | "<"
38
+ | "<="
39
+ | ">"
40
+ | ">="
41
+ | "in"
42
+ | "+"
43
+ | "-"
44
+ | "*"
45
+ | "/"
46
+ | "%";
47
+
48
+ interface Ranged {
49
+ readonly range: SourceRange;
50
+ }
51
+
52
+ export interface CelLiteralNode extends Ranged {
53
+ readonly kind: "literal";
54
+ readonly literal: CelLiteral;
55
+ }
56
+
57
+ export interface CelIdentNode extends Ranged {
58
+ readonly kind: "ident";
59
+ readonly name: string;
60
+ /**
61
+ * `.y` — the name is resolved against the environment's own declarations, never
62
+ * against a name something inside the expression bound. It is the only spelling for
63
+ * the outer `request` where a comprehension variable is also called `request`.
64
+ */
65
+ readonly absolute: boolean;
66
+ }
67
+
68
+ /**
69
+ * One element of a list literal. `optional` marks `[?x]`: the element holds an
70
+ * `optional<T>` and contributes a `T`, and an absent one leaves no element behind.
71
+ */
72
+ export interface CelListElement {
73
+ readonly value: CelNode;
74
+ readonly optional: boolean;
75
+ }
76
+
77
+ export interface CelListNode extends Ranged {
78
+ readonly kind: "list";
79
+ readonly elements: readonly CelListElement[];
80
+ }
81
+
82
+ /** One entry of a map literal. `optional` marks `{?k: v}`, as for a list element. */
83
+ export interface CelMapEntry {
84
+ readonly key: CelNode;
85
+ readonly value: CelNode;
86
+ readonly optional: boolean;
87
+ }
88
+
89
+ export interface CelMapNode extends Ranged {
90
+ readonly kind: "map";
91
+ readonly entries: readonly CelMapEntry[];
92
+ }
93
+
94
+ /** `operand.field`, `operand.?field` when `optional`, `` operand.`field` `` when `quoted`. */
95
+ export interface CelSelectNode extends Ranged {
96
+ readonly kind: "select";
97
+ readonly operand: CelNode;
98
+ /** The member's name, decoded: the text between the backticks for a quoted one. */
99
+ readonly field: string;
100
+ readonly fieldRange: SourceRange;
101
+ readonly optional: boolean;
102
+ /**
103
+ * Written between backticks, which is how a member whose name is not spelled as an
104
+ * identifier is read — `content-type`, `foo.txt`, `/api/v1`. It changes nothing about
105
+ * what is read: the name is looked up exactly as a plain member's is.
106
+ */
107
+ readonly quoted: boolean;
108
+ }
109
+
110
+ /** `operand[index]`, and `operand[?index]` when `optional`. */
111
+ export interface CelIndexNode extends Ranged {
112
+ readonly kind: "index";
113
+ readonly operand: CelNode;
114
+ readonly index: CelNode;
115
+ readonly optional: boolean;
116
+ }
117
+
118
+ /** `name(args)`. */
119
+ export interface CelCallNode extends Ranged {
120
+ readonly kind: "call";
121
+ readonly name: string;
122
+ readonly nameRange: SourceRange;
123
+ readonly args: readonly CelNode[];
124
+ }
125
+
126
+ /** `receiver.name(args)`, including every macro call — nothing expands one here. */
127
+ export interface CelReceiverCallNode extends Ranged {
128
+ readonly kind: "receiverCall";
129
+ readonly receiver: CelNode;
130
+ readonly name: string;
131
+ readonly nameRange: SourceRange;
132
+ readonly args: readonly CelNode[];
133
+ }
134
+
135
+ /** `namespace.name(args)`, where `namespace` names a module rather than a value. */
136
+ export interface CelQualifiedCallNode extends Ranged {
137
+ readonly kind: "qcall";
138
+ readonly namespace: string;
139
+ readonly namespaceRange: SourceRange;
140
+ readonly name: string;
141
+ readonly nameRange: SourceRange;
142
+ readonly args: readonly CelNode[];
143
+ }
144
+
145
+ export interface CelUnaryNode extends Ranged {
146
+ readonly kind: "unary";
147
+ readonly operator: CelUnaryOperator;
148
+ readonly operand: CelNode;
149
+ }
150
+
151
+ export interface CelBinaryNode extends Ranged {
152
+ readonly kind: "binary";
153
+ readonly operator: CelBinaryOperator;
154
+ readonly left: CelNode;
155
+ readonly right: CelNode;
156
+ }
157
+
158
+ export interface CelConditionalNode extends Ranged {
159
+ readonly kind: "conditional";
160
+ readonly condition: CelNode;
161
+ readonly whenTrue: CelNode;
162
+ readonly whenFalse: CelNode;
163
+ }
164
+
165
+ /**
166
+ * The hole error recovery leaves where an expression was expected and none could
167
+ * be read. It stands for text the parser could not make sense of, so the tree
168
+ * around it stays usable; it is never serializable and never evaluable.
169
+ */
170
+ export interface CelUnparsedNode extends Ranged {
171
+ readonly kind: "unparsed";
172
+ }
173
+
174
+ export type CelNode =
175
+ | CelLiteralNode
176
+ | CelIdentNode
177
+ | CelListNode
178
+ | CelMapNode
179
+ | CelSelectNode
180
+ | CelIndexNode
181
+ | CelCallNode
182
+ | CelReceiverCallNode
183
+ | CelQualifiedCallNode
184
+ | CelUnaryNode
185
+ | CelBinaryNode
186
+ | CelConditionalNode
187
+ | CelUnparsedNode;
188
+
189
+ /** Every child node, in source order. The single reader of a node's shape for traversal. */
190
+ export function childNodes(node: CelNode): readonly CelNode[] {
191
+ switch (node.kind) {
192
+ case "literal":
193
+ case "ident":
194
+ case "unparsed":
195
+ return [];
196
+ case "list":
197
+ return node.elements.map((element) => element.value);
198
+ case "map":
199
+ return node.entries.flatMap((entry) => [entry.key, entry.value]);
200
+ case "select":
201
+ return [node.operand];
202
+ case "index":
203
+ return [node.operand, node.index];
204
+ case "call":
205
+ return node.args;
206
+ case "receiverCall":
207
+ return [node.receiver, ...node.args];
208
+ case "qcall":
209
+ return node.args;
210
+ case "unary":
211
+ return [node.operand];
212
+ case "binary":
213
+ return [node.left, node.right];
214
+ case "conditional":
215
+ return [node.condition, node.whenTrue, node.whenFalse];
216
+ }
217
+ }
218
+
219
+ /** Depth-first pre-order walk over the whole tree. */
220
+ export function* walkTree(root: CelNode): Generator<CelNode> {
221
+ yield root;
222
+ for (const child of childNodes(root)) yield* walkTree(child);
223
+ }
224
+
225
+ /** Whether any node of the tree is an unparsed hole. */
226
+ export function hasUnparsed(root: CelNode): boolean {
227
+ for (const node of walkTree(root)) if (node.kind === "unparsed") return true;
228
+ return false;
229
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Instants: reading one, writing one, and reading a field of one in a time zone.
3
+ *
4
+ * A timestamp is whole seconds plus nanoseconds in `[0, 1e9)`, because cel-spec's own
5
+ * rows carry nanosecond instants (`string(timestamp('9999-12-31T23:59:59.999999999Z'))`)
6
+ * and no host date type holds them. The range is cel-spec's: `0001-01-01T00:00:00Z`
7
+ * through `9999-12-31T23:59:59.999999999Z`, and an instant outside it is an error
8
+ * wherever it would be built — which is exactly the answer the vectors call
9
+ * domain-keeping where the engine being replaced returns a date beyond year 9999.
10
+ *
11
+ * **A zone is named, never computed here.** A getter takes an IANA name or a fixed
12
+ * `±HH:MM` offset; the rules behind a name are the host's data, which no release of this
13
+ * engine fixes, so the only thing this file decides is how a name is applied.
14
+ */
15
+
16
+ import {
17
+ celError,
18
+ CEL_VALUE_TYPE,
19
+ isCelError,
20
+ type CelError,
21
+ type CelTimestamp,
22
+ } from "./cel-value.js";
23
+ import type { SourceRange } from "./syntax-tree.js";
24
+
25
+ /** `0001-01-01T00:00:00Z` and `9999-12-31T23:59:59Z`, in seconds since the epoch. */
26
+ export const MIN_TIMESTAMP_SECONDS = -62135596800n;
27
+ export const MAX_TIMESTAMP_SECONDS = 253402300799n;
28
+
29
+ const NANOS_PER_SECOND = 1_000_000_000n;
30
+
31
+ /** An instant from seconds and nanoseconds of any sign, normalized, or the range error. */
32
+ export function celTimestamp(
33
+ seconds: bigint,
34
+ nanos: bigint | number = 0,
35
+ range?: SourceRange,
36
+ ): CelTimestamp | CelError {
37
+ const total = BigInt(nanos);
38
+ let whole = seconds + total / NANOS_PER_SECOND;
39
+ let rest = total % NANOS_PER_SECOND;
40
+ if (rest < 0n) {
41
+ whole -= 1n;
42
+ rest += NANOS_PER_SECOND;
43
+ }
44
+ if (whole < MIN_TIMESTAMP_SECONDS || whole > MAX_TIMESTAMP_SECONDS) {
45
+ return celError("invalid_conversion", "timestamp out of range", range);
46
+ }
47
+ return { [CEL_VALUE_TYPE]: "google.protobuf.Timestamp", seconds: whole, nanos: Number(rest) };
48
+ }
49
+
50
+ /** The instant's nanoseconds since the epoch — what arithmetic and ordering compare. */
51
+ export function timestampNanos(value: CelTimestamp): bigint {
52
+ return value.seconds * NANOS_PER_SECOND + BigInt(value.nanos);
53
+ }
54
+
55
+ const RFC_3339 =
56
+ /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
57
+
58
+ /** An instant from RFC 3339 text, with any offset and any fractional precision. */
59
+ export function parseTimestamp(text: string, range?: SourceRange): CelTimestamp | CelError {
60
+ const parts = RFC_3339.exec(text);
61
+ if (!parts) {
62
+ return celError("invalid_conversion", `${JSON.stringify(text)} is not an RFC 3339 instant`, range);
63
+ }
64
+ const [, year, month, day, hour, minute, second, fraction, sign, offsetHours, offsetMinutes] = parts;
65
+ const fields = {
66
+ year: Number(year),
67
+ month: Number(month),
68
+ day: Number(day),
69
+ hour: Number(hour),
70
+ minute: Number(minute),
71
+ second: Number(second),
72
+ };
73
+ if (
74
+ fields.month < 1 ||
75
+ fields.month > 12 ||
76
+ fields.day < 1 ||
77
+ fields.day > daysInMonth(fields.year, fields.month) ||
78
+ fields.hour > 23 ||
79
+ fields.minute > 59 ||
80
+ fields.second > 59
81
+ ) {
82
+ return celError("invalid_conversion", `${JSON.stringify(text)} is not an instant`, range);
83
+ }
84
+ let seconds = secondsFromFields(fields);
85
+ if (sign) {
86
+ const offset = BigInt(Number(offsetHours) * 3600 + Number(offsetMinutes) * 60);
87
+ seconds += sign === "-" ? offset : -offset;
88
+ }
89
+ const nanos = fraction ? Number(`${fraction}000000000`.slice(0, 9)) : 0;
90
+ return celTimestamp(seconds, nanos, range);
91
+ }
92
+
93
+ /**
94
+ * RFC 3339 in UTC, with the fraction written to the precision it carries and omitted
95
+ * when the instant is whole — cel-spec's own spelling (`2009-02-13T23:31:30Z`).
96
+ */
97
+ export function formatTimestamp(value: CelTimestamp): string {
98
+ const fields = utcFields(value.seconds);
99
+ const date = `${pad(fields.year, 4)}-${pad(fields.month, 2)}-${pad(fields.day, 2)}`;
100
+ const time = `${pad(fields.hour, 2)}:${pad(fields.minute, 2)}:${pad(fields.second, 2)}`;
101
+ return `${date}T${time}${fractionText(value.nanos)}Z`;
102
+ }
103
+
104
+ function fractionText(nanos: number): string {
105
+ if (nanos === 0) return "";
106
+ return `.${pad(nanos, 9).replace(/0+$/, "")}`;
107
+ }
108
+
109
+ function pad(value: number, width: number): string {
110
+ return String(value).padStart(width, "0");
111
+ }
112
+
113
+ // --- the calendar ----------------------------------------------------------
114
+
115
+ export interface CivilFields {
116
+ readonly year: number;
117
+ readonly month: number;
118
+ readonly day: number;
119
+ readonly hour: number;
120
+ readonly minute: number;
121
+ readonly second: number;
122
+ }
123
+
124
+ function isLeapYear(year: number): boolean {
125
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
126
+ }
127
+
128
+ const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
129
+
130
+ function daysInMonth(year: number, month: number): number {
131
+ return month === 2 && isLeapYear(year) ? 29 : MONTH_DAYS[month - 1]!;
132
+ }
133
+
134
+ /** Days from the epoch to a civil date, by Howard Hinnant's `days_from_civil`. */
135
+ function daysFromCivil(year: number, month: number, day: number): number {
136
+ const shifted = month <= 2 ? year - 1 : year;
137
+ const era = Math.floor(shifted / 400);
138
+ const yearOfEra = shifted - era * 400;
139
+ const dayOfYear = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
140
+ const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
141
+ return era * 146097 + dayOfEra - 719468;
142
+ }
143
+
144
+ /** The civil date a day count names, the inverse of `daysFromCivil`. */
145
+ function civilFromDays(days: number): { year: number; month: number; day: number } {
146
+ const shifted = days + 719468;
147
+ const era = Math.floor(shifted / 146097);
148
+ const dayOfEra = shifted - era * 146097;
149
+ const yearOfEra = Math.floor(
150
+ (dayOfEra - Math.floor(dayOfEra / 1460) + Math.floor(dayOfEra / 36524) - Math.floor(dayOfEra / 146096)) / 365,
151
+ );
152
+ const year = yearOfEra + era * 400;
153
+ const dayOfYear = dayOfEra - (365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
154
+ const monthPrime = Math.floor((5 * dayOfYear + 2) / 153);
155
+ const day = dayOfYear - Math.floor((153 * monthPrime + 2) / 5) + 1;
156
+ const month = monthPrime + (monthPrime < 10 ? 3 : -9);
157
+ return { year: month <= 2 ? year + 1 : year, month, day };
158
+ }
159
+
160
+ function secondsFromFields(fields: CivilFields): bigint {
161
+ const days = daysFromCivil(fields.year, fields.month, fields.day);
162
+ return BigInt(days) * 86400n + BigInt(fields.hour * 3600 + fields.minute * 60 + fields.second);
163
+ }
164
+
165
+ function utcFields(seconds: bigint): CivilFields {
166
+ const days = Number(floorDiv(seconds, 86400n));
167
+ const rest = Number(seconds - BigInt(days) * 86400n);
168
+ const date = civilFromDays(days);
169
+ return {
170
+ ...date,
171
+ hour: Math.floor(rest / 3600),
172
+ minute: Math.floor((rest % 3600) / 60),
173
+ second: rest % 60,
174
+ };
175
+ }
176
+
177
+ function floorDiv(value: bigint, by: bigint): bigint {
178
+ const quotient = value / by;
179
+ return value % by < 0n ? quotient - 1n : quotient;
180
+ }
181
+
182
+ // --- zones -----------------------------------------------------------------
183
+
184
+ const FIXED_OFFSET = /^([+-]?)(\d{2}):(\d{2})$/;
185
+
186
+ /**
187
+ * The civil fields of an instant in a zone. A zone is an IANA name, resolved by the
188
+ * host's own tz database, or a fixed `±HH:MM` offset, which is arithmetic; an unsigned
189
+ * offset is read as ahead of UTC, as cel-spec's own rows read it.
190
+ */
191
+ export function zonedFields(
192
+ value: CelTimestamp,
193
+ zone: string | undefined,
194
+ range?: SourceRange,
195
+ ): CivilFields | CelError {
196
+ if (zone === undefined || zone === "UTC") return utcFields(value.seconds);
197
+ const fixed = FIXED_OFFSET.exec(zone);
198
+ if (fixed) {
199
+ const [, sign, hours, minutes] = fixed;
200
+ if (Number(hours) > 23 || Number(minutes) > 59) {
201
+ return celError("invalid_argument", `${JSON.stringify(zone)} is not a time zone`, range);
202
+ }
203
+ const offset = BigInt(Number(hours) * 3600 + Number(minutes) * 60);
204
+ return utcFields(value.seconds + (sign === "-" ? -offset : offset));
205
+ }
206
+ return namedZoneFields(value, zone, range);
207
+ }
208
+
209
+ /** The host's answer for an IANA zone. Its rules are the host's data, not this engine's. */
210
+ function namedZoneFields(
211
+ value: CelTimestamp,
212
+ zone: string,
213
+ range?: SourceRange,
214
+ ): CivilFields | CelError {
215
+ let parts: Intl.DateTimeFormatPart[];
216
+ try {
217
+ const format = new Intl.DateTimeFormat("en-US", {
218
+ timeZone: zone,
219
+ year: "numeric",
220
+ month: "2-digit",
221
+ day: "2-digit",
222
+ hour: "2-digit",
223
+ minute: "2-digit",
224
+ second: "2-digit",
225
+ hourCycle: "h23",
226
+ era: "short",
227
+ });
228
+ parts = format.formatToParts(new Date(Number(value.seconds) * 1000));
229
+ } catch {
230
+ return celError("invalid_argument", `${JSON.stringify(zone)} is not a time zone`, range);
231
+ }
232
+ const read = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? Number.NaN);
233
+ const era = parts.find((part) => part.type === "era")?.value;
234
+ const year = read("year");
235
+ return {
236
+ year: era === "BC" ? 1 - year : year,
237
+ month: read("month"),
238
+ day: read("day"),
239
+ hour: read("hour"),
240
+ minute: read("minute"),
241
+ second: read("second"),
242
+ };
243
+ }
244
+
245
+ /** Which field of an instant a getter answers. The names are cel-spec's. */
246
+ export type TimestampField =
247
+ | "getFullYear"
248
+ | "getMonth"
249
+ | "getDayOfYear"
250
+ | "getDayOfMonth"
251
+ | "getDate"
252
+ | "getDayOfWeek"
253
+ | "getHours"
254
+ | "getMinutes"
255
+ | "getSeconds"
256
+ | "getMilliseconds";
257
+
258
+ /**
259
+ * A field of an instant. `getMonth`, `getDayOfMonth` and `getDayOfYear` are **zero
260
+ * based** and `getDate` is one based, which is cel-spec's own split; `getDayOfWeek`
261
+ * counts from Sunday.
262
+ */
263
+ export function timestampField(
264
+ value: CelTimestamp,
265
+ field: TimestampField,
266
+ zone?: string,
267
+ range?: SourceRange,
268
+ ): bigint | CelError {
269
+ if (field === "getMilliseconds") return BigInt(Math.floor(value.nanos / 1_000_000));
270
+ const civil = zonedFields(value, zone, range);
271
+ if (isCelError(civil)) return civil;
272
+ switch (field) {
273
+ case "getFullYear":
274
+ return BigInt(civil.year);
275
+ case "getMonth":
276
+ return BigInt(civil.month - 1);
277
+ case "getDayOfMonth":
278
+ return BigInt(civil.day - 1);
279
+ case "getDate":
280
+ return BigInt(civil.day);
281
+ case "getDayOfYear":
282
+ return BigInt(daysFromCivil(civil.year, civil.month, civil.day) - daysFromCivil(civil.year, 1, 1));
283
+ case "getDayOfWeek": {
284
+ const days = daysFromCivil(civil.year, civil.month, civil.day);
285
+ return BigInt(((days % 7) + 11) % 7);
286
+ }
287
+ case "getHours":
288
+ return BigInt(civil.hour);
289
+ case "getMinutes":
290
+ return BigInt(civil.minute);
291
+ case "getSeconds":
292
+ return BigInt(civil.second);
293
+ }
294
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Whether two trees are the same expression.
3
+ *
4
+ * Ranges are **not** compared: two trees of the same expression written with
5
+ * different spacing or parentheses hold different offsets, and the round-trip
6
+ * contract (`serializer.ts`) is exactly that serializing and re-parsing gives back
7
+ * the same expression — not the same text.
8
+ *
9
+ * Numbers compare by identity rather than by `==`, so `-0.0` is not `0.0` and a
10
+ * `NaN` equals itself: both are distinct literals a round-trip must preserve.
11
+ */
12
+
13
+ import type { CelLiteral, CelNode } from "./syntax-tree.js";
14
+
15
+ function literalsEqual(left: CelLiteral, right: CelLiteral): boolean {
16
+ if (left.type !== right.type) return false;
17
+ switch (left.type) {
18
+ case "null":
19
+ return true;
20
+ case "int":
21
+ case "uint":
22
+ return left.value === (right as typeof left).value;
23
+ case "double":
24
+ return Object.is(left.value, (right as typeof left).value);
25
+ case "bool":
26
+ case "string":
27
+ return left.value === (right as typeof left).value;
28
+ case "bytes": {
29
+ const other = (right as typeof left).value;
30
+ return left.value.length === other.length && left.value.every((byte, at) => byte === other[at]);
31
+ }
32
+ }
33
+ }
34
+
35
+ function listsEqual(left: readonly CelNode[], right: readonly CelNode[]): boolean {
36
+ return left.length === right.length && left.every((node, at) => treesEqual(node, right[at]!));
37
+ }
38
+
39
+ /** Whether the two trees are the same expression, their ranges aside. */
40
+ export function treesEqual(left: CelNode, right: CelNode): boolean {
41
+ if (left.kind !== right.kind) return false;
42
+ switch (left.kind) {
43
+ case "literal":
44
+ return literalsEqual(left.literal, (right as typeof left).literal);
45
+ case "ident": {
46
+ const other = right as typeof left;
47
+ // Absolute changes what the name resolves against, so it is part of the expression;
48
+ // whether a member was written between backticks is not.
49
+ return left.name === other.name && left.absolute === other.absolute;
50
+ }
51
+ case "unparsed":
52
+ return true;
53
+ case "list": {
54
+ const other = (right as typeof left).elements;
55
+ return (
56
+ left.elements.length === other.length &&
57
+ left.elements.every(
58
+ (element, at) =>
59
+ element.optional === other[at]!.optional && treesEqual(element.value, other[at]!.value),
60
+ )
61
+ );
62
+ }
63
+ case "map": {
64
+ const other = (right as typeof left).entries;
65
+ return (
66
+ left.entries.length === other.length &&
67
+ left.entries.every(
68
+ (entry, at) =>
69
+ entry.optional === other[at]!.optional &&
70
+ treesEqual(entry.key, other[at]!.key) &&
71
+ treesEqual(entry.value, other[at]!.value),
72
+ )
73
+ );
74
+ }
75
+ case "select": {
76
+ const other = right as typeof left;
77
+ return (
78
+ left.field === other.field &&
79
+ left.optional === other.optional &&
80
+ treesEqual(left.operand, other.operand)
81
+ );
82
+ }
83
+ case "index": {
84
+ const other = right as typeof left;
85
+ return (
86
+ left.optional === other.optional &&
87
+ treesEqual(left.operand, other.operand) &&
88
+ treesEqual(left.index, other.index)
89
+ );
90
+ }
91
+ case "call": {
92
+ const other = right as typeof left;
93
+ return left.name === other.name && listsEqual(left.args, other.args);
94
+ }
95
+ case "receiverCall": {
96
+ const other = right as typeof left;
97
+ return (
98
+ left.name === other.name &&
99
+ treesEqual(left.receiver, other.receiver) &&
100
+ listsEqual(left.args, other.args)
101
+ );
102
+ }
103
+ case "qcall": {
104
+ const other = right as typeof left;
105
+ return (
106
+ left.namespace === other.namespace && left.name === other.name && listsEqual(left.args, other.args)
107
+ );
108
+ }
109
+ case "unary": {
110
+ const other = right as typeof left;
111
+ return left.operator === other.operator && treesEqual(left.operand, other.operand);
112
+ }
113
+ case "binary": {
114
+ const other = right as typeof left;
115
+ return (
116
+ left.operator === other.operator &&
117
+ treesEqual(left.left, other.left) &&
118
+ treesEqual(left.right, other.right)
119
+ );
120
+ }
121
+ case "conditional": {
122
+ const other = right as typeof left;
123
+ return (
124
+ treesEqual(left.condition, other.condition) &&
125
+ treesEqual(left.whenTrue, other.whenTrue) &&
126
+ treesEqual(left.whenFalse, other.whenFalse)
127
+ );
128
+ }
129
+ }
130
+ }