@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.
- package/LICENSE +17 -0
- package/README.md +281 -2
- package/dist/activation.d.ts +27 -0
- package/dist/activation.d.ts.map +1 -0
- package/dist/activation.js +24 -0
- package/dist/backend-runtime.d.ts +146 -0
- package/dist/backend-runtime.d.ts.map +1 -0
- package/dist/backend-runtime.js +322 -0
- package/dist/bounded-cache.d.ts +21 -0
- package/dist/bounded-cache.d.ts.map +1 -0
- package/dist/bounded-cache.js +42 -0
- package/dist/catalog-runtime.d.ts +59 -0
- package/dist/catalog-runtime.d.ts.map +1 -0
- package/dist/catalog-runtime.js +785 -0
- package/dist/cel-expression.d.ts +32 -0
- package/dist/cel-expression.d.ts.map +1 -0
- package/dist/cel-expression.js +29 -0
- package/dist/cel-map-value.d.ts +34 -0
- package/dist/cel-map-value.d.ts.map +1 -0
- package/dist/cel-map-value.js +74 -0
- package/dist/cel-program.d.ts +44 -0
- package/dist/cel-program.d.ts.map +1 -0
- package/dist/cel-program.js +72 -0
- package/dist/cel-type.d.ts +131 -0
- package/dist/cel-type.d.ts.map +1 -0
- package/dist/cel-type.js +293 -0
- package/dist/cel-value.d.ts +159 -0
- package/dist/cel-value.d.ts.map +1 -0
- package/dist/cel-value.js +225 -0
- package/dist/check-diagnostic.d.ts +53 -0
- package/dist/check-diagnostic.d.ts.map +1 -0
- package/dist/check-diagnostic.js +66 -0
- package/dist/checker.d.ts +77 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +721 -0
- package/dist/closure-backend.d.ts +21 -0
- package/dist/closure-backend.d.ts.map +1 -0
- package/dist/closure-backend.js +436 -0
- package/dist/comprehension-bindings.d.ts +33 -0
- package/dist/comprehension-bindings.d.ts.map +1 -0
- package/dist/comprehension-bindings.js +50 -0
- package/dist/comprehension-runtime.d.ts +44 -0
- package/dist/comprehension-runtime.d.ts.map +1 -0
- package/dist/comprehension-runtime.js +137 -0
- package/dist/declared-chain.d.ts +35 -0
- package/dist/declared-chain.d.ts.map +1 -0
- package/dist/declared-chain.js +36 -0
- package/dist/duration-value.d.ts +59 -0
- package/dist/duration-value.d.ts.map +1 -0
- package/dist/duration-value.js +135 -0
- package/dist/emitted-module.d.ts +207 -0
- package/dist/emitted-module.d.ts.map +1 -0
- package/dist/emitted-module.js +359 -0
- package/dist/engine-version.d.ts +3 -0
- package/dist/engine-version.d.ts.map +1 -0
- package/dist/engine-version.js +8 -0
- package/dist/environment-digest.d.ts +44 -0
- package/dist/environment-digest.d.ts.map +1 -0
- package/dist/environment-digest.js +98 -0
- package/dist/environment.d.ts +283 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +459 -0
- package/dist/function-catalog.d.ts +66 -0
- package/dist/function-catalog.d.ts.map +1 -0
- package/dist/function-catalog.js +77 -0
- package/dist/function-registry.d.ts +78 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +189 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/integer-arithmetic.d.ts +27 -0
- package/dist/integer-arithmetic.d.ts.map +1 -0
- package/dist/integer-arithmetic.js +58 -0
- package/dist/js-emitter.d.ts +132 -0
- package/dist/js-emitter.d.ts.map +1 -0
- package/dist/js-emitter.js +562 -0
- package/dist/json-schema-type.d.ts +182 -0
- package/dist/json-schema-type.d.ts.map +1 -0
- package/dist/json-schema-type.js +487 -0
- package/dist/json-text-scan.d.ts +28 -0
- package/dist/json-text-scan.d.ts.map +1 -0
- package/dist/json-text-scan.js +159 -0
- package/dist/lexer.d.ts +103 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +458 -0
- package/dist/macro-check.d.ts +33 -0
- package/dist/macro-check.d.ts.map +1 -0
- package/dist/macro-check.js +162 -0
- package/dist/macro-shape.d.ts +24 -0
- package/dist/macro-shape.d.ts.map +1 -0
- package/dist/macro-shape.js +55 -0
- package/dist/member-read.d.ts +52 -0
- package/dist/member-read.d.ts.map +1 -0
- package/dist/member-read.js +125 -0
- package/dist/namespace-resolution.d.ts +35 -0
- package/dist/namespace-resolution.d.ts.map +1 -0
- package/dist/namespace-resolution.js +160 -0
- package/dist/nominal-type.d.ts +63 -0
- package/dist/nominal-type.d.ts.map +1 -0
- package/dist/nominal-type.js +98 -0
- package/dist/nullable-access.d.ts +38 -0
- package/dist/nullable-access.d.ts.map +1 -0
- package/dist/nullable-access.js +93 -0
- package/dist/parse-limits.d.ts +26 -0
- package/dist/parse-limits.d.ts.map +1 -0
- package/dist/parse-limits.js +21 -0
- package/dist/parser.d.ts +48 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +503 -0
- package/dist/qualified-calls.d.ts +22 -0
- package/dist/qualified-calls.d.ts.map +1 -0
- package/dist/qualified-calls.js +27 -0
- package/dist/regular-expression.d.ts +48 -0
- package/dist/regular-expression.d.ts.map +1 -0
- package/dist/regular-expression.js +77 -0
- package/dist/reserved-words.d.ts +52 -0
- package/dist/reserved-words.d.ts.map +1 -0
- package/dist/reserved-words.js +77 -0
- package/dist/resolved-call.d.ts +33 -0
- package/dist/resolved-call.d.ts.map +1 -0
- package/dist/resolved-call.js +15 -0
- package/dist/root-references.d.ts +23 -0
- package/dist/root-references.d.ts.map +1 -0
- package/dist/root-references.js +120 -0
- package/dist/runtime-library.d.ts +56 -0
- package/dist/runtime-library.d.ts.map +1 -0
- package/dist/runtime-library.js +545 -0
- package/dist/serializer.d.ts +24 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +240 -0
- package/dist/sha256.d.ts +20 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +103 -0
- package/dist/signature.d.ts +72 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +61 -0
- package/dist/signatures/function-catalog.json +788 -0
- package/dist/signatures/standard-library.json +229 -0
- package/dist/standard-library.d.ts +41 -0
- package/dist/standard-library.d.ts.map +1 -0
- package/dist/standard-library.js +85 -0
- package/dist/syntax-diagnostic.d.ts +61 -0
- package/dist/syntax-diagnostic.d.ts.map +1 -0
- package/dist/syntax-diagnostic.js +28 -0
- package/dist/syntax-tree.d.ts +160 -0
- package/dist/syntax-tree.d.ts.map +1 -0
- package/dist/syntax-tree.js +58 -0
- package/dist/timestamp-value.d.ts +53 -0
- package/dist/timestamp-value.d.ts.map +1 -0
- package/dist/timestamp-value.js +223 -0
- package/dist/tree-equality.d.ts +15 -0
- package/dist/tree-equality.d.ts.map +1 -0
- package/dist/tree-equality.js +105 -0
- package/dist/type-expression.d.ts +26 -0
- package/dist/type-expression.d.ts.map +1 -0
- package/dist/type-expression.js +134 -0
- package/dist/value-equality.d.ts +38 -0
- package/dist/value-equality.d.ts.map +1 -0
- package/dist/value-equality.js +196 -0
- package/dist/value-text.d.ts +25 -0
- package/dist/value-text.d.ts.map +1 -0
- package/dist/value-text.js +44 -0
- package/dist/zoned-calendar.d.ts +61 -0
- package/dist/zoned-calendar.d.ts.map +1 -0
- package/dist/zoned-calendar.js +143 -0
- package/package.json +56 -3
- package/src/activation.ts +32 -0
- package/src/backend-runtime.ts +454 -0
- package/src/bounded-cache.ts +45 -0
- package/src/catalog-runtime.ts +921 -0
- package/src/cel-expression.ts +53 -0
- package/src/cel-map-value.ts +86 -0
- package/src/cel-program.ts +103 -0
- package/src/cel-type.ts +359 -0
- package/src/cel-value.ts +353 -0
- package/src/check-diagnostic.ts +102 -0
- package/src/checker.ts +932 -0
- package/src/closure-backend.ts +502 -0
- package/src/comprehension-bindings.ts +66 -0
- package/src/comprehension-runtime.ts +157 -0
- package/src/declared-chain.ts +45 -0
- package/src/duration-value.ts +145 -0
- package/src/emitted-module.ts +494 -0
- package/src/engine-version.ts +9 -0
- package/src/environment-digest.ts +111 -0
- package/src/environment.ts +740 -0
- package/src/function-catalog.ts +140 -0
- package/src/function-registry.ts +229 -0
- package/src/index.ts +386 -0
- package/src/integer-arithmetic.ts +64 -0
- package/src/js-emitter.ts +713 -0
- package/src/json-schema-type.ts +664 -0
- package/src/json-text-scan.ts +163 -0
- package/src/lexer.ts +562 -0
- package/src/macro-check.ts +191 -0
- package/src/macro-shape.ts +66 -0
- package/src/member-read.ts +126 -0
- package/src/namespace-resolution.ts +167 -0
- package/src/nominal-type.ts +149 -0
- package/src/nullable-access.ts +95 -0
- package/src/parse-limits.ts +36 -0
- package/src/parser.ts +554 -0
- package/src/qualified-calls.ts +39 -0
- package/src/regular-expression.ts +101 -0
- package/src/reserved-words.ts +94 -0
- package/src/resolved-call.ts +34 -0
- package/src/root-references.ts +126 -0
- package/src/runtime-library.ts +615 -0
- package/src/serializer.ts +246 -0
- package/src/sha256.ts +112 -0
- package/src/signature.ts +127 -0
- package/src/signatures/function-catalog.json +788 -0
- package/src/signatures/standard-library.json +235 -0
- package/src/standard-library.ts +149 -0
- package/src/syntax-diagnostic.ts +72 -0
- package/src/syntax-tree.ts +229 -0
- package/src/timestamp-value.ts +294 -0
- package/src/tree-equality.ts +130 -0
- package/src/type-expression.ts +160 -0
- package/src/value-equality.ts +201 -0
- package/src/value-text.ts +45 -0
- 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
|
+
}
|