@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,494 @@
1
+ /**
2
+ * An emitted module: its text, its key, its integrity header, and the one seam a host
3
+ * stores it through.
4
+ *
5
+ * **The runtime is injected, never imported.** The module's default export is a factory
6
+ * taking the runtime support library and answering one synchronous function per
7
+ * expression, and the text names no specifier of any kind. An emitted module that imported
8
+ * `@telorun/cel` would be loadable only where that specifier resolves — which rules out a
9
+ * `data:` URL, a cache directory mounted somewhere else, and a host whose resolver is not
10
+ * Node's — and, worse, it would silently accept a runtime of another version, which is
11
+ * precisely what the key cannot see from the outside.
12
+ *
13
+ * **The key is wider than the source text, and the header is wider than the key.** The key
14
+ * is a hash over the emitter's format generation, the engine version, the environment
15
+ * digest and the canonical ordered list of expression sources. A key is a claim about a
16
+ * *name*; the header is a fact about the *bytes*, and it carries five fields because the
17
+ * provenance three answer only part of the question:
18
+ *
19
+ * - `format`, `engine`, `environment` — **provenance**. These are identical for every module
20
+ * one engine writes against one environment, so they catch a cache root shared by two
21
+ * engines and a stale environment, and **nothing else**. Two different modules of one
22
+ * engine carry byte-identical provenance.
23
+ * - `key` — the module's **identity**: the key this text was written for. It catches a store
24
+ * that answered one key's lookup with another key's text, which the provenance fields
25
+ * cannot see at all and which an expression count can only catch by luck.
26
+ * - `body` — the digest of the text **following the header line**, which covers every byte
27
+ * that is or could be read as code, the `integrity` export included. It catches a text
28
+ * truncated after its header and a text edited after it was written — and the banner says
29
+ * *edit the expression, not this*, which is evidence that someone will edit one. A text with
30
+ * an intact header, an exported factory and a matching function count **runs**, whatever its
31
+ * body says; that is the one failure here no retry undoes.
32
+ *
33
+ * `body` cannot live in the `integrity` export, because it covers that export: a digest in the
34
+ * bytes it digests has no fixed point. So the header LINE carries all five and the export
35
+ * carries the four that are checkable without the text — which is also the split between the
36
+ * two paths. **Every mismatch on the store-read path is a recompile** that names itself in
37
+ * `refused`; the load path, which has an object and not the bytes, refuses with
38
+ * `emitted_module_rejected`.
39
+ *
40
+ * **There is no loader, and there is no filesystem.** A loader would have to reach a
41
+ * filesystem (which this package may not), or be `eval` (which the closure backend exists
42
+ * to avoid), or be a `data:` URL import (fine under Node, refused under a browser's content
43
+ * security policy). Loading is the host's half; the engine writes the text, verifies what
44
+ * comes back, and wires the loaded factory to the runtime.
45
+ */
46
+
47
+ import type { CelProgram } from "./cel-program.js";
48
+ import { programOfStep } from "./cel-program.js";
49
+ import { celNone, celSome, celUint, isCelError, isCelOptional, asyncValueRefused, celError } from "./cel-value.js";
50
+ import { CelEngineError } from "./check-diagnostic.js";
51
+ import { celMapFromEntries } from "./cel-map-value.js";
52
+ import {
53
+ boolOperand,
54
+ callSiteOf,
55
+ hasMember,
56
+ optionalEntry,
57
+ readHostValue,
58
+ readName,
59
+ readNameChain,
60
+ readThrough,
61
+ searchNameChain,
62
+ type CelStep,
63
+ type CompileTarget,
64
+ } from "./backend-runtime.js";
65
+ import {
66
+ celAll,
67
+ celExists,
68
+ celExistsOne,
69
+ celFilter,
70
+ celMapComprehension,
71
+ } from "./comprehension-runtime.js";
72
+ import { ENGINE_VERSION } from "./engine-version.js";
73
+ import { ModuleEmitter, RUNTIME_BINDINGS, textSource, type RuntimeBinding } from "./js-emitter.js";
74
+ import { celIterable } from "./member-read.js";
75
+ import { optionalOfNonZero } from "./runtime-library.js";
76
+ import { sha256OfText } from "./sha256.js";
77
+ import type { CelNode } from "./syntax-tree.js";
78
+
79
+ /**
80
+ * **Bumped on any change to the text the emitter writes for any tree** — not only when the
81
+ * module's shape changes. It is the first component of the key and the first field of the
82
+ * header, so a module written under one generation is never handed to an engine expecting
83
+ * another.
84
+ *
85
+ * The wider rule is the one that holds, because the narrower one asks whoever makes the
86
+ * change to decide that a text difference is semantically neutral — and that judgement is
87
+ * exactly what produces the unrecoverable failure. The case in point is this emitter's own
88
+ * first defect: temporaries numbered per function let a comprehension body's `let t0` shadow
89
+ * the `t0` its caller held a bound value in, so `cel.bind(n, 2, xs.map(e, e + n))` answered
90
+ * `[2, 4, 6]` for `[3, 4, 5]`. No signature moved, no library entry moved, no shape changed —
91
+ * and a module cached before the fix would have gone on answering `[2, 4, 6]` forever.
92
+ *
93
+ * `tests/emitter-text.test.ts` is what makes the rule checkable: it pins the digest of the
94
+ * code the emitter writes for a corpus drawn from the package's own total enumerations, beside
95
+ * this number, so a change to that text fails naming the bump it owes.
96
+ */
97
+ export const EMITTER_FORMAT_GENERATION = 1;
98
+
99
+ /** The prefix of the one line a stored module's header is read from. */
100
+ const HEADER_PREFIX = "//@telo.cel ";
101
+
102
+ /**
103
+ * What an emitted module declares about itself in its `integrity` export: its provenance and
104
+ * its own key. These are the four a loaded module can be held to, because checking them needs
105
+ * nothing but the object the host imported.
106
+ */
107
+ export interface EmittedIdentity {
108
+ readonly format: number;
109
+ readonly engine: string;
110
+ readonly environment: string;
111
+ /** The cache key this text was written for. */
112
+ readonly key: string;
113
+ }
114
+
115
+ /**
116
+ * What the header LINE declares: the identity, plus the digest of the body it precedes. The
117
+ * body digest is here and nowhere else — a digest cannot live in the bytes it covers — so it
118
+ * is verified on the one path that has the bytes in hand.
119
+ */
120
+ export interface EmittedHeader extends EmittedIdentity {
121
+ /** The digest of every byte after the header line. */
122
+ readonly body: string;
123
+ }
124
+
125
+ /** One expression to emit: its source text, and the tree it read into. */
126
+ export interface EmittedExpression {
127
+ readonly source: string;
128
+ readonly root: CelNode;
129
+ }
130
+
131
+ export interface EmittedModule {
132
+ /** The cache key: 64 hexadecimal characters a host names its stored copy by. */
133
+ readonly key: string;
134
+ readonly header: EmittedHeader;
135
+ readonly text: string;
136
+ /** The expression sources, in the order the factory answers functions for them. */
137
+ readonly sources: readonly string[];
138
+ }
139
+
140
+ /**
141
+ * Where a host keeps emitted module text, keyed by the key the engine computed. It is the
142
+ * engine's only seam to a store, and the engine touches no filesystem itself.
143
+ *
144
+ * **A write is atomic** — written elsewhere and renamed into place — and that is the store's
145
+ * contract rather than a suggestion: several hosts share one cache root. It is still how a
146
+ * half-written entry is *avoided*, and avoiding one is better than detecting it. But the
147
+ * guarantee no longer rests on it: the header's `body` digest **detects** a truncated or
148
+ * edited text, so a host that gets the write wrong costs a recompile rather than running
149
+ * whatever the bytes happen to say.
150
+ *
151
+ * It is **synchronous**, because emission is: a store over a filesystem writes and renames
152
+ * synchronously, and a store over anything slower is a map the host filled before it asked.
153
+ */
154
+ export interface EmittedModuleStore {
155
+ read(key: string): string | undefined;
156
+ write(key: string, text: string): void;
157
+ }
158
+
159
+ /** A module taken from the store, or emitted because the store had nothing usable. */
160
+ export interface StoredEmittedModule extends EmittedModule {
161
+ /** Why the stored copy was not used, where there was one and it was refused. */
162
+ readonly refused?: string;
163
+ /** Whether the text was emitted now rather than read from the store. */
164
+ readonly emitted: boolean;
165
+ }
166
+
167
+ /** The runtime object an emitted module's factory is handed. */
168
+ export type EmittedRuntime = Readonly<Record<RuntimeBinding, unknown>>;
169
+
170
+ /** What an emitted module's default export is. */
171
+ export type EmittedFactory = (runtime: EmittedRuntime) => readonly CelStep[];
172
+
173
+ // --- the key and the header ------------------------------------------------
174
+
175
+ /** The four fields the `integrity` export carries, in one fixed order. */
176
+ function identityJson(identity: EmittedIdentity): string {
177
+ return (
178
+ `{"format":${identity.format},"engine":${JSON.stringify(identity.engine)},` +
179
+ `"environment":${JSON.stringify(identity.environment)},"key":${JSON.stringify(identity.key)}}`
180
+ );
181
+ }
182
+
183
+ /** The five fields the header line carries. */
184
+ function headerJson(header: EmittedHeader): string {
185
+ return `${identityJson(header).slice(0, -1)},"body":${JSON.stringify(header.body)}}`;
186
+ }
187
+
188
+ /**
189
+ * The key for a set of expressions against an environment.
190
+ *
191
+ * The four components are hashed in one order, each on its own line with its source
192
+ * escaped, so no expression's text can be read as another component. Source alone would
193
+ * serve the wrong module for exactly the capability this package exists for — a host that
194
+ * replaced a standard function has the same text and a different environment.
195
+ */
196
+ export function emittedModuleKey(
197
+ environmentDigest: string,
198
+ sources: readonly string[],
199
+ ): string {
200
+ return sha256OfText(
201
+ [
202
+ `format ${EMITTER_FORMAT_GENERATION}`,
203
+ `engine ${ENGINE_VERSION}`,
204
+ `environment ${environmentDigest}`,
205
+ ...sources.map((source) => `expression ${JSON.stringify(source)}`),
206
+ ].join("\n"),
207
+ );
208
+ }
209
+
210
+ /** What an emission against this environment, for these sources, declares about itself. */
211
+ export function emittedModuleIdentity(
212
+ environmentDigest: string,
213
+ sources: readonly string[],
214
+ ): EmittedIdentity {
215
+ return {
216
+ format: EMITTER_FORMAT_GENERATION,
217
+ engine: ENGINE_VERSION,
218
+ environment: environmentDigest,
219
+ key: emittedModuleKey(environmentDigest, sources),
220
+ };
221
+ }
222
+
223
+ /**
224
+ * The header line's own JSON and the body it precedes — found by position rather than by
225
+ * splitting the text into lines, because the body can be a megabyte and is digested whole.
226
+ */
227
+ function headerAndBody(text: string): { readonly json: string; readonly body: string } | undefined {
228
+ const leading = text.startsWith(HEADER_PREFIX);
229
+ const found = leading ? 0 : text.indexOf(`\n${HEADER_PREFIX}`);
230
+ if (!leading && found < 0) return undefined;
231
+ const from = leading ? 0 : found + 1;
232
+ const ends = text.indexOf("\n", from);
233
+ return {
234
+ json: text.slice(from + HEADER_PREFIX.length, ends < 0 ? text.length : ends),
235
+ // Exactly what the emission digested: every byte after the header line's newline.
236
+ body: ends < 0 ? "" : text.slice(ends + 1),
237
+ };
238
+ }
239
+
240
+ /**
241
+ * The header a stored text declares, or nothing where it declares none.
242
+ *
243
+ * It is read from the **text**, before anything is loaded: a text whose header is wrong is
244
+ * never handed to a loader at all, which is the cheapest place to refuse and the only place
245
+ * that works for a text that would not even parse.
246
+ */
247
+ export function readEmittedHeader(text: string): EmittedHeader | undefined {
248
+ const held = headerAndBody(text);
249
+ if (!held) return undefined;
250
+ let parsed: unknown;
251
+ try {
252
+ parsed = JSON.parse(held.json) as unknown;
253
+ } catch {
254
+ return undefined;
255
+ }
256
+ return asHeader(parsed);
257
+ }
258
+
259
+ function asIdentity(parsed: unknown): EmittedIdentity | undefined {
260
+ if (typeof parsed !== "object" || parsed === null) return undefined;
261
+ const held = parsed as Record<string, unknown>;
262
+ if (typeof held.format !== "number") return undefined;
263
+ if (typeof held.engine !== "string" || typeof held.environment !== "string") return undefined;
264
+ if (typeof held.key !== "string") return undefined;
265
+ return {
266
+ format: held.format,
267
+ engine: held.engine,
268
+ environment: held.environment,
269
+ key: held.key,
270
+ };
271
+ }
272
+
273
+ function asHeader(parsed: unknown): EmittedHeader | undefined {
274
+ const identity = asIdentity(parsed);
275
+ if (!identity) return undefined;
276
+ const body = (parsed as Record<string, unknown>).body;
277
+ return typeof body === "string" ? { ...identity, body } : undefined;
278
+ }
279
+
280
+ /**
281
+ * Why a stored text is not the module that was asked for, or nothing where it is.
282
+ *
283
+ * This is the **store-read path**, the one that has the bytes, so it is where the body digest
284
+ * is verified as well as the identity — and every answer is a recompile, never a refusal the
285
+ * caller has to handle. Each names what differed, because a host that logs a recompile wants
286
+ * to know whether it was a stale cache, two engines sharing a root, a store that answered the
287
+ * wrong lookup, or a text somebody edited.
288
+ */
289
+ export function emittedModuleRefusal(text: string, expected: EmittedIdentity): string | undefined {
290
+ const verdict = verifyStoredText(text, expected);
291
+ return "refused" in verdict ? verdict.refused : undefined;
292
+ }
293
+
294
+ /**
295
+ * The header a stored text proved, or why it proved none. One pass: the caller that accepts
296
+ * the text wants the header it read rather than a second read of the same bytes.
297
+ */
298
+ function verifyStoredText(
299
+ text: string,
300
+ expected: EmittedIdentity,
301
+ ): { readonly header: EmittedHeader } | { readonly refused: string } {
302
+ const held = headerAndBody(text);
303
+ if (!held) return { refused: "the stored text carries no integrity header" };
304
+ let parsed: unknown;
305
+ try {
306
+ parsed = JSON.parse(held.json) as unknown;
307
+ } catch {
308
+ return { refused: "the stored text's integrity header is not readable" };
309
+ }
310
+ const header = asHeader(parsed);
311
+ if (!header) return { refused: "the stored text's integrity header is not an emitted module's" };
312
+ const mismatch = identityRefusal(header, expected);
313
+ if (mismatch) return { refused: mismatch };
314
+ const body = sha256OfText(held.body);
315
+ if (body !== header.body) {
316
+ return {
317
+ refused: `the stored module's body does not match the digest its header declares (${header.body} against ${body})`,
318
+ };
319
+ }
320
+ return { header };
321
+ }
322
+
323
+ /** Why an identity is not the one asked for, or nothing where it is. */
324
+ function identityRefusal(held: EmittedIdentity, expected: EmittedIdentity): string | undefined {
325
+ if (held.format !== expected.format) {
326
+ return `the stored module was emitted in format generation ${held.format} and this engine emits ${expected.format}`;
327
+ }
328
+ if (held.engine !== expected.engine) {
329
+ return `the stored module was emitted by engine ${held.engine} and this engine is ${expected.engine}`;
330
+ }
331
+ if (held.environment !== expected.environment) {
332
+ return `the stored module was emitted against environment ${held.environment} and this environment is ${expected.environment}`;
333
+ }
334
+ if (held.key !== expected.key) {
335
+ return `the stored module declares key ${held.key} and the module asked for is ${expected.key}`;
336
+ }
337
+ return undefined;
338
+ }
339
+
340
+ // --- emission --------------------------------------------------------------
341
+
342
+ /**
343
+ * One module for a set of expressions. Byte-identical for the same expressions in the same
344
+ * order against the same environment: nothing here reads a clock, a counter outside the
345
+ * emission, or the iteration order of anything the caller did not order.
346
+ */
347
+ export function emitModule(
348
+ target: CompileTarget,
349
+ environmentDigest: string,
350
+ expressions: readonly EmittedExpression[],
351
+ ): EmittedModule {
352
+ const emitter = new ModuleEmitter(target);
353
+ const functions = expressions.map((expression) => emitter.emitFunction(expression.root));
354
+ const sources = expressions.map((expression) => expression.source);
355
+ const identity = emittedModuleIdentity(environmentDigest, sources);
356
+ // The BODY is everything after the header line, and the header declares its digest — so
357
+ // it is built first, and the two banner lines are all the header does not cover.
358
+ const body = [
359
+ ...sources.map((source, at) => `// ${at}: ${textSource(source)}`),
360
+ `export const integrity = ${identityJson(identity)};`,
361
+ "export default function (runtime) {",
362
+ ` const { ${RUNTIME_BINDINGS.join(", ")} } = runtime;`,
363
+ ...emitter.hoistedLines().map((held) => ` ${held}`),
364
+ " return [",
365
+ ...functions.map((held) => ` ${held},`),
366
+ " ];",
367
+ "}",
368
+ "",
369
+ ].join("\n");
370
+ const header: EmittedHeader = { ...identity, body: sha256OfText(body) };
371
+ const text = [
372
+ "// @telorun/cel: an emitted module. Generated - edit the expression, not this.",
373
+ "// The runtime is the factory's argument; this module imports nothing.",
374
+ `${HEADER_PREFIX}${headerJson(header)}`,
375
+ body,
376
+ ].join("\n");
377
+ return { key: identity.key, header, text, sources };
378
+ }
379
+
380
+ /**
381
+ * The module for a set of expressions, from the store where it holds a copy whose header
382
+ * matches, and emitted and written where it does not.
383
+ *
384
+ * The header check is what makes this safe to call on a shared cache root: a stored text
385
+ * that does not prove it was emitted by this engine, in this format, against this
386
+ * environment is replaced rather than run.
387
+ */
388
+ export function storedEmittedModule(
389
+ target: CompileTarget,
390
+ environmentDigest: string,
391
+ sources: readonly string[],
392
+ /** The trees, read only where there is something to emit — a hit parses nothing. */
393
+ expressions: () => readonly EmittedExpression[],
394
+ store: EmittedModuleStore,
395
+ ): StoredEmittedModule {
396
+ const identity = emittedModuleIdentity(environmentDigest, sources);
397
+ const held = store.read(identity.key);
398
+ const verdict = held === undefined ? undefined : verifyStoredText(held, identity);
399
+ if (verdict && "header" in verdict) {
400
+ // The stored text proved itself whole, so it is the module — carrying the header read
401
+ // back from the bytes rather than one reconstructed, since those bytes are what loads.
402
+ return { key: identity.key, header: verdict.header, text: held!, sources, emitted: false };
403
+ }
404
+ const written = emitModule(target, environmentDigest, expressions());
405
+ store.write(written.key, written.text);
406
+ return { ...written, ...(verdict ? { refused: verdict.refused } : {}), emitted: true };
407
+ }
408
+
409
+ // --- what a loaded module is handed, and what it answers --------------------
410
+
411
+ /**
412
+ * The runtime support library an emitted module's factory takes: every operation the
413
+ * emitted code calls, bound to this environment.
414
+ *
415
+ * Each entry is the function the **closure backend** calls for the same operation —
416
+ * literally the same reference — which is what makes "the two backends answer identically"
417
+ * a property of the wiring rather than a hope the tests confirm.
418
+ */
419
+ export function emitterRuntime(target: CompileTarget): EmittedRuntime {
420
+ return {
421
+ asyncValueRefused,
422
+ boolOperand,
423
+ callSite: (name: string, form: "global" | "receiver", range: [number, number]) =>
424
+ callSiteOf(target, name, form, range),
425
+ celAll,
426
+ celError,
427
+ celExists,
428
+ celExistsOne,
429
+ celFilter,
430
+ celIterable,
431
+ celMapComprehension,
432
+ celMapFromEntries,
433
+ celSome,
434
+ celUint,
435
+ constants: target.constants,
436
+ hasMember,
437
+ isCelError,
438
+ isCelOptional,
439
+ none: celNone(),
440
+ optionalEntry,
441
+ optionalOfNonZero,
442
+ readHostValue,
443
+ readName,
444
+ readNameChain,
445
+ readThrough,
446
+ searchNameChain,
447
+ };
448
+ }
449
+
450
+ /**
451
+ * The programs a loaded emitted module answers, after its header is verified.
452
+ *
453
+ * This is the **load path**: it has an object the host imported and not the bytes, so it
454
+ * checks the four fields the `integrity` export carries. A module that declares no header,
455
+ * declares another format, engine or environment, declares **another key** — which is a host
456
+ * that imported bytes this engine never hashed — exports no factory, or answers a different
457
+ * number of functions than the expressions asked for is refused with
458
+ * `emitted_module_rejected`. The function count stays as the cheap backstop; the `key` field
459
+ * is what actually distinguishes two modules of one engine.
460
+ *
461
+ * The body digest is **not** checked here, and cannot be: it covers the `integrity` export,
462
+ * so it lives only in the header line and is verified where the bytes are
463
+ * (`emittedModuleRefusal`, on the store-read path). Nothing here imports or evaluates
464
+ * anything; the module arrived already loaded, by whatever means the host loads one.
465
+ */
466
+ export function programsFromEmittedModule(
467
+ loaded: { readonly integrity?: unknown; readonly default?: unknown },
468
+ expected: EmittedModule,
469
+ runtime: EmittedRuntime,
470
+ ): readonly CelProgram[] {
471
+ const identity = asIdentity(loaded.integrity);
472
+ if (!identity) {
473
+ throw new CelEngineError(
474
+ "emitted_module_rejected",
475
+ "the loaded module declares no integrity header, so it is not an emitted CEL module",
476
+ );
477
+ }
478
+ const refusal = identityRefusal(identity, expected.header);
479
+ if (refusal) throw new CelEngineError("emitted_module_rejected", refusal);
480
+ if (typeof loaded.default !== "function") {
481
+ throw new CelEngineError(
482
+ "emitted_module_rejected",
483
+ "an emitted module's default export is the factory that takes the runtime",
484
+ );
485
+ }
486
+ const steps = (loaded.default as EmittedFactory)(runtime);
487
+ if (!Array.isArray(steps) || steps.length !== expected.sources.length) {
488
+ throw new CelEngineError(
489
+ "emitted_module_rejected",
490
+ `the loaded module answers ${Array.isArray(steps) ? steps.length : "no"} functions and ${expected.sources.length} expressions were emitted`,
491
+ );
492
+ }
493
+ return expected.sources.map((source, at) => programOfStep(source, steps[at]!));
494
+ }
@@ -0,0 +1,9 @@
1
+ // GENERATED by scripts/generate-telo-version.mjs — do not edit.
2
+ //
3
+ // Which build of this engine an emitted module was written by: the telo version
4
+ // this build implements, with `+unreleased` while that release is still pending.
5
+ // It is a component of every emitted module's cache key and a field of its
6
+ // integrity header, so a module one build wrote is never run by another.
7
+
8
+ /** This build of the CEL engine, as an emitted module names it. */
9
+ export const ENGINE_VERSION = "0.107.0";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The digest of an environment — one of the four things an emitted module's cache key is
3
+ * over.
4
+ *
5
+ * It is taken over the environment's **resolved listing**, never over its registration
6
+ * history: a function registered and then replaced leaves one entry, and two environments
7
+ * built by different orders of the same registrations are the same environment. Keying on
8
+ * the history instead would fragment the cache into one entry per way of arriving at the
9
+ * same answers, which is the same defect as a key that is too narrow wearing the other
10
+ * face.
11
+ *
12
+ * So every line below is a fact about what the environment *is*: every function signature
13
+ * surviving registration and removal, every named type with its base and parameters, every
14
+ * variable with its name and type, every namespace with the functions it declares, and
15
+ * every option value. The lines are sorted, so the order they were produced in cannot
16
+ * reach the digest.
17
+ *
18
+ * **What the digest is for, in this engine.** Overloads are resolved at evaluation on the
19
+ * values' own types (`backend-runtime.ts`), so a replaced library does not change the
20
+ * emitted *text* the way it would in an engine that baked a resolution into the output.
21
+ * What it changes is the runtime object the module is handed — and the module's integrity
22
+ * header repeats this digest, so a module can only ever be run against the environment it
23
+ * was written for. The wide key is therefore what makes the header a proof rather than a
24
+ * hope, and over-keying is the safe direction: a key too wide costs a recompile, a key too
25
+ * narrow runs the wrong code.
26
+ *
27
+ * **One limit, written down rather than hidden:** an option written explicitly as its own
28
+ * default is a different line from one left out, so a host that passes
29
+ * `standardLibrary: true` and a host that relies on the default are two cache entries for
30
+ * one environment. Normalizing that would mean teaching this file every option's default —
31
+ * a second copy of what the constructor already decides, and the kind of copy that drifts.
32
+ * The cost is one extra emission; the cost of the copy going stale is a wrong key.
33
+ */
34
+
35
+ import type { CelEnvironmentOptions, Definitions } from "./environment.js";
36
+ import { sha256OfText } from "./sha256.js";
37
+
38
+ /** What the digest reads of an environment — its listing and its options, nothing else. */
39
+ export interface DigestedEnvironment {
40
+ definitions(): Definitions;
41
+ readonly options: CelEnvironmentOptions;
42
+ }
43
+
44
+ /** JSON with every object's keys in order, so two equal values are one text. */
45
+ function canonicalJson(value: unknown): string {
46
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
47
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
48
+ const entries = Object.keys(value as object)
49
+ .sort(compareText)
50
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`);
51
+ return `{${entries.join(",")}}`;
52
+ }
53
+
54
+ function compareText(left: string, right: string): number {
55
+ return left < right ? -1 : left > right ? 1 : 0;
56
+ }
57
+
58
+ /**
59
+ * How one option contributes. A **function**-valued option (the schema resolver) is
60
+ * recorded as present rather than inspected: its answers are already in the variable types
61
+ * the listing carries, which is the resolved form of exactly what it decided. An option
62
+ * nothing supplied and one supplied as `undefined` are one environment, so both read
63
+ * `absent`.
64
+ */
65
+ function optionText(value: unknown): string {
66
+ if (value === undefined) return "absent";
67
+ if (typeof value === "function") return "present";
68
+ return canonicalJson(value);
69
+ }
70
+
71
+ /** Every line of the environment's resolved listing, canonically sorted. */
72
+ export function environmentListing(environment: DigestedEnvironment): readonly string[] {
73
+ const definitions = environment.definitions();
74
+ const lines: string[] = [];
75
+ for (const key of Object.keys(environment.options)) {
76
+ lines.push(`option ${key} ${optionText((environment.options as Record<string, unknown>)[key])}`);
77
+ }
78
+ for (const variable of definitions.variables) {
79
+ lines.push(`variable ${variable.name} ${variable.typeName} ${variable.constant}`);
80
+ }
81
+ for (const held of definitions.functions) {
82
+ lines.push(
83
+ [
84
+ "function",
85
+ held.signature,
86
+ held.receiverType ?? "-",
87
+ held.deterministic,
88
+ held.hostBacked,
89
+ canonicalJson(held.throws ?? null),
90
+ held.origin ?? "-",
91
+ ].join(" "),
92
+ );
93
+ }
94
+ for (const held of definitions.types) {
95
+ lines.push(`type ${held.name} ${held.base} ${canonicalJson(held.parameters)}`);
96
+ }
97
+ for (const held of definitions.namespaces) {
98
+ // Openness is part of the surface, not a convenience: it decides whether an undeclared
99
+ // name checks clean, so two environments differing only in it answer differently and must
100
+ // not share an emitted module.
101
+ lines.push(
102
+ `namespace ${held.name} ${held.open ? "open" : "closed"} ${canonicalJson([...held.functions].sort(compareText))}`,
103
+ );
104
+ }
105
+ return lines.sort(compareText);
106
+ }
107
+
108
+ /** The digest of an environment: 64 hexadecimal characters over its sorted listing. */
109
+ export function environmentDigest(environment: DigestedEnvironment): string {
110
+ return sha256OfText(environmentListing(environment).join("\n"));
111
+ }