@telorun/cel 0.0.0-stage → 0.108.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 +184 -0
  7. package/dist/backend-runtime.d.ts.map +1 -0
  8. package/dist/backend-runtime.js +425 -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 +786 -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 +47 -0
  19. package/dist/cel-map-value.d.ts.map +1 -0
  20. package/dist/cel-map-value.js +85 -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 +166 -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 +54 -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 +86 -0
  34. package/dist/checker.d.ts.map +1 -0
  35. package/dist/checker.js +806 -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 +487 -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 +70 -0
  49. package/dist/duration-value.d.ts.map +1 -0
  50. package/dist/duration-value.js +149 -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 +291 -0
  61. package/dist/environment.d.ts.map +1 -0
  62. package/dist/environment.js +474 -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 +118 -0
  67. package/dist/function-registry.d.ts.map +1 -0
  68. package/dist/function-registry.js +292 -0
  69. package/dist/index.d.ts +93 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +65 -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 +133 -0
  76. package/dist/js-emitter.d.ts.map +1 -0
  77. package/dist/js-emitter.js +568 -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 +41 -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 +89 -0
  94. package/dist/member-read.d.ts.map +1 -0
  95. package/dist/member-read.js +166 -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 +46 -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 +67 -0
  127. package/dist/runtime-library.d.ts.map +1 -0
  128. package/dist/runtime-library.js +554 -0
  129. package/dist/serializer.d.ts +24 -0
  130. package/dist/serializer.d.ts.map +1 -0
  131. package/dist/serializer.js +256 -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 +62 -0
  150. package/dist/timestamp-value.d.ts.map +1 -0
  151. package/dist/timestamp-value.js +238 -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 +42 -0
  156. package/dist/type-expression.d.ts.map +1 -0
  157. package/dist/type-expression.js +154 -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 +604 -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 +97 -0
  174. package/src/cel-program.ts +103 -0
  175. package/src/cel-type.ts +359 -0
  176. package/src/cel-value.ts +361 -0
  177. package/src/check-diagnostic.ts +104 -0
  178. package/src/checker.ts +1045 -0
  179. package/src/closure-backend.ts +547 -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 +160 -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 +761 -0
  188. package/src/function-catalog.ts +140 -0
  189. package/src/function-registry.ts +341 -0
  190. package/src/index.ts +407 -0
  191. package/src/integer-arithmetic.ts +64 -0
  192. package/src/js-emitter.ts +721 -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 +197 -0
  197. package/src/macro-shape.ts +66 -0
  198. package/src/member-read.ts +167 -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 +47 -0
  208. package/src/root-references.ts +126 -0
  209. package/src/runtime-library.ts +639 -0
  210. package/src/serializer.ts +262 -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 +310 -0
  219. package/src/tree-equality.ts +130 -0
  220. package/src/type-expression.ts +182 -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,140 @@
1
+ /**
2
+ * Loading the function catalog into an environment.
3
+ *
4
+ * The catalog is the **dialect**: the functions a manifest may call beside CEL's own
5
+ * library. Like the standard library it is **data** (`signatures/function-catalog.json`)
6
+ * read through the same registration surface a host uses — here literally the public one,
7
+ * `CelEnvironment.registerFunction`, with no privileged door of any kind. A host may
8
+ * register the catalog, leave it out, replace one of its signatures or remove a name, and
9
+ * nothing in the engine behaves differently for it.
10
+ *
11
+ * **One surface, read by everything that lists the catalog.** `functionCatalog()` answers
12
+ * each function with its display signature, its category, its summary and its two flags,
13
+ * which is exactly what a `functions` listing prints; a consumer never reconstructs that
14
+ * from registrations.
15
+ *
16
+ * What a function DOES is beside it, per runtime — `catalog-runtime.ts`, keyed by the same
17
+ * dispatch key the registry resolves on. A declared signature with no behaviour there is
18
+ * refused at registration rather than at evaluation, which is the failure class the two
19
+ * artifacts exist to remove.
20
+ */
21
+
22
+ import data from "./signatures/function-catalog.json" with { type: "json" };
23
+ import type { CelCatalogHandlers } from "./catalog-runtime.js";
24
+ import { catalogImplementation, catalogLiteralCheck } from "./catalog-runtime.js";
25
+ import type { CelEnvironment } from "./environment.js";
26
+ import { parseSignature, signatureKey } from "./signature.js";
27
+
28
+ interface CatalogEntryData {
29
+ readonly name: string;
30
+ readonly signature: string;
31
+ readonly signatures: readonly string[];
32
+ readonly category: string;
33
+ readonly summary: string;
34
+ readonly deterministic: boolean;
35
+ readonly hostBacked: boolean;
36
+ readonly checksLiteralArguments?: boolean;
37
+ }
38
+
39
+ interface CatalogData {
40
+ readonly generation: number;
41
+ readonly spec: boolean;
42
+ readonly reason: string;
43
+ readonly categories: readonly string[];
44
+ readonly functions: readonly CatalogEntryData[];
45
+ }
46
+
47
+ const DATA = data as unknown as CatalogData;
48
+
49
+ export const FUNCTION_CATALOG_GENERATION = DATA.generation;
50
+
51
+ /** The categories the catalog groups its functions under, for a grouped listing. */
52
+ export function catalogCategories(): readonly string[] {
53
+ return DATA.categories;
54
+ }
55
+
56
+ /** One function of the catalog, as a listing prints it. */
57
+ export interface CatalogFunction {
58
+ readonly name: string;
59
+ /**
60
+ * The signature a human reads, which is not always one registration: an optional
61
+ * parameter is written `fn(string?): string` and registers one signature per arity.
62
+ */
63
+ readonly signature: string;
64
+ /** Every signature the function registers, in registration order. */
65
+ readonly signatures: readonly string[];
66
+ readonly category: string;
67
+ readonly summary: string;
68
+ /** False where two calls with the same arguments may answer differently. */
69
+ readonly deterministic: boolean;
70
+ /** Whether the implementation is the host's rather than the engine's. */
71
+ readonly hostBacked: boolean;
72
+ /** Whether the function refuses an argument written as a literal, at check. */
73
+ readonly checksLiteralArguments: boolean;
74
+ }
75
+
76
+ /** The catalog, as the one surface a listing reads. */
77
+ export function functionCatalog(): readonly CatalogFunction[] {
78
+ return DATA.functions.map((entry) => ({
79
+ name: entry.name,
80
+ signature: entry.signature,
81
+ signatures: entry.signatures,
82
+ category: entry.category,
83
+ summary: entry.summary,
84
+ deterministic: entry.deterministic,
85
+ hostBacked: entry.hostBacked,
86
+ checksLiteralArguments: entry.checksLiteralArguments ?? false,
87
+ }));
88
+ }
89
+
90
+ /** Every signature the catalog registers, for a validator and for a docs listing. */
91
+ export function catalogSignatures(): readonly string[] {
92
+ return DATA.functions.flatMap((entry) => entry.signatures);
93
+ }
94
+
95
+ export interface RegisterCatalogOptions {
96
+ /**
97
+ * The implementations of the nine host-backed functions. Each may be left out — the
98
+ * registration is still made, so an expression calling it still type-checks, and
99
+ * evaluating one answers an `unbound_function` error naming the function. That is what
100
+ * a consumer which only ever CHECKS (an analyzer, an editor) needs.
101
+ */
102
+ readonly handlers?: Partial<CelCatalogHandlers>;
103
+ }
104
+
105
+ /**
106
+ * Registers the catalog onto an environment, through the public registration surface.
107
+ *
108
+ * A signature the runtime table does not answer for is refused here: a declaration with
109
+ * no behaviour type-checks and then fails at evaluation, which is precisely the failure
110
+ * the declaration/behaviour split exists to catch at registration instead.
111
+ */
112
+ export function registerFunctionCatalog(
113
+ environment: CelEnvironment,
114
+ options: RegisterCatalogOptions = {},
115
+ ): void {
116
+ const handlers = options.handlers ?? {};
117
+ for (const entry of DATA.functions) {
118
+ const check = entry.checksLiteralArguments ? catalogLiteralCheck(entry.name) : undefined;
119
+ if (entry.checksLiteralArguments && check === undefined) {
120
+ throw new Error(
121
+ `the catalog declares that ${entry.name} checks its literal arguments, and no guard answers for it`,
122
+ );
123
+ }
124
+ for (const signature of entry.signatures) {
125
+ const key = signatureKey(parseSignature(signature));
126
+ const implementation = catalogImplementation(key, handlers);
127
+ if (implementation === undefined) {
128
+ throw new Error(`the catalog declares ${signature} and nothing implements ${key}`);
129
+ }
130
+ environment.registerFunction(signature, {
131
+ implementation,
132
+ deterministic: entry.deterministic,
133
+ hostBacked: entry.hostBacked,
134
+ description: entry.summary,
135
+ origin: "function-catalog",
136
+ ...(check === undefined ? {} : { checkArguments: check }),
137
+ });
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Which functions an environment has, and which one a call resolves to.
3
+ *
4
+ * **Nothing here is privileged.** The standard library registers through exactly this
5
+ * surface, so a host can replace one of its signatures or remove it — the capability
6
+ * the whole package exists for. Registration is last-wins by dispatch key
7
+ * (`signature.ts`), removal is by the same key, and both are per environment: a
8
+ * clone inherits and may then diverge without touching its parent.
9
+ *
10
+ * **Resolution answers WHY it failed**, never just "no". A call on a name nothing
11
+ * registers, a call written in the wrong form, and a call whose arguments no overload
12
+ * takes are three different mistakes with three different repairs, and the engine
13
+ * being replaced reported one sentence for all three — which is why a separate
14
+ * classifier had to re-derive the cause after the fact. The checker decides it here,
15
+ * once, with the candidates it considered.
16
+ */
17
+
18
+ import { CelEngineError } from "./check-diagnostic.js";
19
+ import type { CelType } from "./cel-type.js";
20
+ import { assignable, DYN, formatType, isDyn, typesEqual } from "./cel-type.js";
21
+ import type { CallForm, CelSignature, FunctionMetadata } from "./signature.js";
22
+ import { formatSignature, signatureKey } from "./signature.js";
23
+ import { CALL_SITE_DIRECT_ARITY } from "./runtime-library.js";
24
+ import { isIdentifierSpelling, isReservedWord } from "./reserved-words.js";
25
+
26
+ export interface RegisteredFunction {
27
+ readonly signature: CelSignature;
28
+ readonly metadata: FunctionMetadata;
29
+ }
30
+
31
+ export type ResolutionFailure =
32
+ /** No function of that name is registered, in any form. */
33
+ | { readonly reason: "unknown"; readonly candidates: readonly RegisteredFunction[] }
34
+ /** The name is registered, but only in the other call form. */
35
+ | { readonly reason: "wrong-form"; readonly candidates: readonly RegisteredFunction[] }
36
+ /** The name and form are registered; no overload takes these arguments. */
37
+ | { readonly reason: "no-overload"; readonly candidates: readonly RegisteredFunction[] };
38
+
39
+ /**
40
+ * How many names a refused call may name as candidates.
41
+ *
42
+ * It is **declared** rather than left to a message's taste, because the conformance
43
+ * vectors pin that message byte for byte: a bound and an order a second engine cannot
44
+ * reproduce would be a row no port can pass. The order is edit distance then name, both
45
+ * over the name exactly as it was written.
46
+ */
47
+ export const UNKNOWN_FUNCTION_CANDIDATES = 5;
48
+
49
+ export interface Resolution {
50
+ readonly resolved: RegisteredFunction;
51
+ /** The return type with every type parameter substituted by what this call bound. */
52
+ readonly returns: CelType;
53
+ }
54
+
55
+ export class FunctionRegistry {
56
+ private readonly byName: Map<string, RegisteredFunction[]>;
57
+
58
+ constructor(inherited?: FunctionRegistry) {
59
+ this.byName = new Map();
60
+ if (inherited) {
61
+ for (const [name, entries] of inherited.byName) this.byName.set(name, [...entries]);
62
+ }
63
+ }
64
+
65
+ /** Registers a function, replacing any registration answering the same call. */
66
+ register(signature: CelSignature, metadata: FunctionMetadata = {}): void {
67
+ // An implementation receives its arguments positionally, up to the bound, so a wider
68
+ // signature is refused HERE rather than resolved and then called with its tail dropped.
69
+ // A declared bound nothing enforces is the silent wrong answer it exists to prevent.
70
+ const arity = signature.parameters.length + (signature.form === "receiver" ? 1 : 0);
71
+ if (arity > CALL_SITE_DIRECT_ARITY) {
72
+ throw new CelEngineError(
73
+ "signature_too_wide",
74
+ `${formatSignature(signature)} takes ${arity} values and an implementation receives at most ${CALL_SITE_DIRECT_ARITY}`,
75
+ );
76
+ }
77
+ const entries = this.byName.get(signature.name) ?? [];
78
+ const key = signatureKey(signature);
79
+ const at = entries.findIndex((entry) => signatureKey(entry.signature) === key);
80
+ const entry: RegisteredFunction = { signature, metadata };
81
+ if (at === -1) entries.push(entry);
82
+ else entries[at] = entry;
83
+ this.byName.set(signature.name, entries);
84
+ }
85
+
86
+ /** Removes the one registration answering that call. Answers whether it was there. */
87
+ remove(signature: CelSignature): boolean {
88
+ const entries = this.byName.get(signature.name);
89
+ if (!entries) return false;
90
+ const key = signatureKey(signature);
91
+ const at = entries.findIndex((entry) => signatureKey(entry.signature) === key);
92
+ if (at === -1) return false;
93
+ entries.splice(at, 1);
94
+ if (entries.length === 0) this.byName.delete(signature.name);
95
+ return true;
96
+ }
97
+
98
+ /** Removes every registration of a name. Answers how many went. */
99
+ removeName(name: string): number {
100
+ const entries = this.byName.get(name);
101
+ if (!entries) return 0;
102
+ this.byName.delete(name);
103
+ return entries.length;
104
+ }
105
+
106
+ has(name: string): boolean {
107
+ return this.byName.has(name);
108
+ }
109
+
110
+ /** Every registration, in name order then registration order. */
111
+ list(): readonly RegisteredFunction[] {
112
+ return [...this.byName.keys()].sort().flatMap((name) => this.byName.get(name)!);
113
+ }
114
+
115
+ named(name: string): readonly RegisteredFunction[] {
116
+ return this.byName.get(name) ?? [];
117
+ }
118
+
119
+ /**
120
+ * The names this environment registers that would accept a call of that form and
121
+ * arity, nearest first — what a call on a name nothing registers is offered instead.
122
+ *
123
+ * **Form and arity are a filter rather than a ranking**, because a nearer name a call
124
+ * cannot be written on is not a repair: `'a'.size()` is not helped by `size`. A name
125
+ * that is not a name is filtered for the same reason — the operators register under
126
+ * their own symbols (`!`, `-`, `+`), and `no(1)` offered `!` and `-` as its two
127
+ * nearest spellings, neither of which can be written as a call at all. Among what
128
+ * passes the filter the order is edit distance then name, and the list is cut at
129
+ * {@link UNKNOWN_FUNCTION_CANDIDATES} — a declared bound and a declared order, so the
130
+ * message is the same text on every engine. Distance is counted over the name as
131
+ * written, with no case folding: a port reproduces one rule and not a host language's
132
+ * notion of lower case.
133
+ */
134
+ candidateNames(wanted: string, form: CallForm, arity: number): readonly string[] {
135
+ const matching: string[] = [];
136
+ for (const [name, entries] of this.byName) {
137
+ if (name === wanted) continue;
138
+ if (!isIdentifierSpelling(name) || isReservedWord(name)) continue;
139
+ const accepts = entries.some(
140
+ (entry) => entry.signature.form === form && entry.signature.parameters.length === arity,
141
+ );
142
+ if (accepts) matching.push(name);
143
+ }
144
+ return matching
145
+ .map((name) => ({ name, distance: editDistance(wanted, name) }))
146
+ .sort((left, right) => left.distance - right.distance || compareNames(left.name, right.name))
147
+ .slice(0, UNKNOWN_FUNCTION_CANDIDATES)
148
+ .map((candidate) => candidate.name);
149
+ }
150
+
151
+ /**
152
+ * The function a call resolves to. `receiver` is the type the call is written on,
153
+ * absent for a global call.
154
+ */
155
+ resolve(
156
+ name: string,
157
+ form: CallForm,
158
+ args: readonly CelType[],
159
+ receiver?: CelType,
160
+ ): Resolution | ResolutionFailure {
161
+ const all = this.byName.get(name);
162
+ if (!all || all.length === 0) return { reason: "unknown", candidates: [] };
163
+ const inForm = all.filter((entry) => entry.signature.form === form);
164
+ if (inForm.length === 0) return { reason: "wrong-form", candidates: all };
165
+
166
+ const arity = inForm.filter((entry) => entry.signature.parameters.length === args.length);
167
+ const viable = arity.length === 0 ? inForm : arity;
168
+ // Exact first, then a candidate `dyn` makes viable, which is what keeps an
169
+ // unlisted variable from turning one mistake into two.
170
+ const exact = this.match(viable, args, receiver, false);
171
+ if (exact) return exact;
172
+ const loose = this.matchLoosely(viable, args, receiver);
173
+ if (loose) return loose;
174
+ return { reason: "no-overload", candidates: inForm };
175
+ }
176
+
177
+ /**
178
+ * The loose pass: a `dyn` argument makes a candidate viable, and **where several become
179
+ * viable and they do not agree on a return type, the call answers `dyn`.**
180
+ *
181
+ * Taking the first one instead is a concrete type nothing established. `dyn('a') +
182
+ * dyn('b')` answered `int` — the first `+` overload — so a host that declares nothing
183
+ * about a name had `a + b` typed `int`, and a consumer comparing that against a declared
184
+ * `string` slot refused a manifest that runs correctly. The honest answer is the one the
185
+ * engine's own design already states: overloads are resolved per call site on the VALUES'
186
+ * own types, so a `dyn` operand is exactly the case where the static answer is unknown.
187
+ *
188
+ * The candidate is still carried for the caller that wants one (a listing, a flag), and a
189
+ * single viable overload still answers its own return type — `dyn` only where they differ.
190
+ */
191
+ private matchLoosely(
192
+ candidates: readonly RegisteredFunction[],
193
+ args: readonly CelType[],
194
+ receiver: CelType | undefined,
195
+ ): Resolution | undefined {
196
+ const viable: Resolution[] = [];
197
+ for (const candidate of candidates) {
198
+ const held = this.match([candidate], args, receiver, true);
199
+ if (held) viable.push(held);
200
+ }
201
+ const first = viable[0];
202
+ if (!first) return undefined;
203
+ const agree = viable.every((held) => typesEqual(held.returns, first.returns));
204
+ return agree ? first : { resolved: first.resolved, returns: DYN };
205
+ }
206
+
207
+ private match(
208
+ candidates: readonly RegisteredFunction[],
209
+ args: readonly CelType[],
210
+ receiver: CelType | undefined,
211
+ allowDyn: boolean,
212
+ ): Resolution | undefined {
213
+ for (const candidate of candidates) {
214
+ const { signature } = candidate;
215
+ if (signature.parameters.length !== args.length) continue;
216
+ const bindings = new Map<string, CelType>();
217
+ if (signature.receiver && !this.fits(receiver ?? DYN, signature.receiver, bindings, allowDyn)) continue;
218
+ if (!signature.parameters.every((want, at) => this.fits(args[at]!, want, bindings, allowDyn))) continue;
219
+ return { resolved: candidate, returns: substitute(signature.returns, bindings) };
220
+ }
221
+ return undefined;
222
+ }
223
+
224
+ /**
225
+ * Whether an argument fits a parameter, binding type parameters as it goes.
226
+ *
227
+ * **A type parameter is bound by the first argument that mentions it**, and a later
228
+ * argument does not refine it: `[] + [3, 4]` is `list<T>`, because the empty list
229
+ * fixed the element type as "unknown" and the second argument cannot retroactively
230
+ * decide what the first one held.
231
+ */
232
+ private fits(
233
+ argument: CelType,
234
+ parameter: CelType,
235
+ bindings: Map<string, CelType>,
236
+ allowDyn: boolean,
237
+ ): boolean {
238
+ if (parameter.kind === "parameter") {
239
+ const bound = bindings.get(parameter.name);
240
+ if (!bound) {
241
+ bindings.set(parameter.name, argument);
242
+ return true;
243
+ }
244
+ // A binding that holds an unresolved parameter holds no information, so a later
245
+ // concrete argument refines it: `optional.none().orValue(42)` is an int.
246
+ if (bound.kind === "parameter" && argument.kind !== "parameter") {
247
+ bindings.set(parameter.name, argument);
248
+ return true;
249
+ }
250
+ if (bound.kind === "parameter" || argument.kind === "parameter") return true;
251
+ return assignable(argument, bound) || (allowDyn && (isDyn(argument) || isDyn(bound)));
252
+ }
253
+ if (parameter.kind === "list" && argument.kind === "list") {
254
+ return this.fits(argument.element, parameter.element, bindings, allowDyn);
255
+ }
256
+ if (parameter.kind === "map" && argument.kind === "map") {
257
+ return (
258
+ this.fits(argument.key, parameter.key, bindings, allowDyn) &&
259
+ this.fits(argument.value, parameter.value, bindings, allowDyn)
260
+ );
261
+ }
262
+ if (parameter.kind === "optional" && argument.kind === "optional") {
263
+ return this.fits(argument.value, parameter.value, bindings, allowDyn);
264
+ }
265
+ if (
266
+ parameter.kind === "nominal" &&
267
+ argument.kind === "nominal" &&
268
+ parameter.name === argument.name &&
269
+ parameter.args.length === argument.args.length
270
+ ) {
271
+ // A named type's arguments are invariant, so they fit one for one — which is also
272
+ // where a signature written over `Self` binds its own type parameters.
273
+ return parameter.args.every((want, at) => this.fits(argument.args[at]!, want, bindings, allowDyn));
274
+ }
275
+ if (isDyn(argument)) return allowDyn || isDyn(parameter);
276
+ return assignable(argument, parameter);
277
+ }
278
+ }
279
+
280
+ /** The type with every bound parameter replaced; an unbound one stays as it is. */
281
+ export function substitute(type: CelType, bindings: ReadonlyMap<string, CelType>): CelType {
282
+ switch (type.kind) {
283
+ case "parameter":
284
+ return bindings.get(type.name) ?? type;
285
+ case "list": {
286
+ const element = substitute(type.element, bindings);
287
+ return element === type.element ? type : { kind: "list", element };
288
+ }
289
+ case "map": {
290
+ const key = substitute(type.key, bindings);
291
+ const value = substitute(type.value, bindings);
292
+ return key === type.key && value === type.value ? type : { kind: "map", key, value };
293
+ }
294
+ case "optional": {
295
+ const value = substitute(type.value, bindings);
296
+ return value === type.value ? type : { kind: "optional", value };
297
+ }
298
+ case "nominal": {
299
+ const args = type.args.map((argument) => substitute(argument, bindings));
300
+ return args.every((argument, at) => argument === type.args[at]) ? type : { ...type, args };
301
+ }
302
+ default:
303
+ return type;
304
+ }
305
+ }
306
+
307
+ /** Code-unit order, so a port orders two names without a locale. */
308
+ function compareNames(left: string, right: string): number {
309
+ return left < right ? -1 : left > right ? 1 : 0;
310
+ }
311
+
312
+ /**
313
+ * Levenshtein distance in UTF-16 code units — one insertion, deletion or substitution
314
+ * each costing one, which is the whole rule a port needs to reproduce.
315
+ *
316
+ * Code units rather than code points for the same reason every range in this package is
317
+ * in them: it is the one unit both ends of the engine already count in, and a name is
318
+ * compared against a name rather than cut.
319
+ */
320
+ function editDistance(from: string, to: string): number {
321
+ let previous = Array.from({ length: to.length + 1 }, (ignored, at) => at);
322
+ for (let left = 1; left <= from.length; left += 1) {
323
+ const row = new Array<number>(to.length + 1);
324
+ row[0] = left;
325
+ for (let right = 1; right <= to.length; right += 1) {
326
+ const substitution = previous[right - 1]! + (from[left - 1] === to[right - 1] ? 0 : 1);
327
+ row[right] = Math.min(substitution, previous[right]! + 1, row[right - 1]! + 1);
328
+ }
329
+ previous = row;
330
+ }
331
+ return previous[to.length]!;
332
+ }
333
+
334
+ /** How a resolution failure names what it looked at, for a message a human reads. */
335
+ export function describeCandidates(candidates: readonly RegisteredFunction[]): string {
336
+ return candidates.map((candidate) => formatSignature(candidate.signature)).join(", ");
337
+ }
338
+
339
+ export function describeArguments(args: readonly CelType[]): string {
340
+ return args.map((type) => formatType(type)).join(", ");
341
+ }