bonsai-js 1.0.0 → 1.1.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 (48) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +24 -4
  3. package/dist/artifact.d.mts +47 -0
  4. package/dist/artifact.mjs +139 -0
  5. package/dist/artifact.mjs.map +1 -0
  6. package/dist/check/bind.mjs +117 -0
  7. package/dist/check/bind.mjs.map +1 -0
  8. package/dist/check/checker.mjs +14 -116
  9. package/dist/check/checker.mjs.map +1 -1
  10. package/dist/compile/compiler.mjs +2 -3
  11. package/dist/compile/compiler.mjs.map +1 -1
  12. package/dist/declared.mjs +8 -1
  13. package/dist/declared.mjs.map +1 -1
  14. package/dist/digest.mjs +109 -0
  15. package/dist/digest.mjs.map +1 -0
  16. package/dist/environment.d.mts +1 -1
  17. package/dist/environment.mjs +52 -309
  18. package/dist/environment.mjs.map +1 -1
  19. package/dist/errors.d.mts +21 -2
  20. package/dist/errors.mjs +17 -1
  21. package/dist/errors.mjs.map +1 -1
  22. package/dist/features.mjs +3 -3
  23. package/dist/features.mjs.map +1 -1
  24. package/dist/functions/builtins.mjs +2 -2
  25. package/dist/functions/define.mjs +13 -6
  26. package/dist/functions/define.mjs.map +1 -1
  27. package/dist/index.d.mts +3 -2
  28. package/dist/index.mjs +3 -2
  29. package/dist/lite/index.d.mts +3 -2
  30. package/dist/lite/index.mjs +3 -2
  31. package/dist/lite/index.mjs.map +1 -1
  32. package/dist/load.mjs +308 -0
  33. package/dist/load.mjs.map +1 -0
  34. package/dist/program.mjs +323 -0
  35. package/dist/program.mjs.map +1 -0
  36. package/dist/query/index.mjs +1 -1
  37. package/dist/runtime/index.d.mts +61 -0
  38. package/dist/runtime/index.mjs +95 -0
  39. package/dist/runtime/index.mjs.map +1 -0
  40. package/dist/service/index.mjs +8 -5
  41. package/dist/service/index.mjs.map +1 -1
  42. package/dist/syntax/parser.mjs +1 -1
  43. package/dist/syntax/printer.mjs +1 -1
  44. package/dist/types.mjs +103 -23
  45. package/dist/types.mjs.map +1 -1
  46. package/dist/version.mjs +7 -0
  47. package/dist/version.mjs.map +1 -0
  48. package/package.json +10 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ From 1.0.0, entries are generated at release time from the bump files in
7
7
  [`.bumpy/`](./.bumpy/); entries up to 0.5.0 follow
8
8
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
9
9
 
10
+
11
+ ## 1.1.0
12
+ <sub>2026-10-24</sub>
13
+
14
+ - *(minor)*
15
+ Add `bonsai-js/runtime` and `toArtifact()`: check and compile rules where the full toolchain runs, and evaluate them in the browser without the checker (about 44 KB minified and gzipped), with exactly the results the compiled programs give. A runtime whose definitions differ refuses an artifact with `BonsaiArtifactError` (code `ARTIFACT`) and a `reason`.
16
+ - *(patch)*
17
+ Charge the step budget for each union member tried when a result is checked against its expected type or a host function's declared type. A list result checked against a large enum could previously run far past `maxSteps` and `timeout`.
18
+ - *(patch)*
19
+ Check and cache a program against a private copy of its `expect` type. A type object with a `toJSON` method could previously be served a cached program checked against a different type, and a type object changed after compiling changed what the program's results were checked against. The language service's `expect` option is kept the same way. Types built with `t` are used as given.
20
+
10
21
  ## 1.0.0
11
22
  <sub>2026-10-19</sub>
12
23
 
package/README.md CHANGED
@@ -62,7 +62,7 @@ Try it in the [Playground](https://danfry1.github.io/bonsai-js/playground).
62
62
  | Evaluation budget for untrusted input (as documented) | Yes, on by default | Parse-time limits only | No expression budget | No | No | No | No |
63
63
  | SQL or MongoDB translation | Yes | No | No | No | No | No | No |
64
64
 
65
- Bonsai is the largest of these in the browser (about 56 KB from `bonsai-js/lite`, without explaining and partial evaluation) and the newest. If rules must also run in Go or Java, CEL or ZEN fit better; if rules are JSON produced by a form builder and size matters most, json-logic-js does. The [full comparison](https://danfry1.github.io/bonsai-js/guide/comparison) covers more libraries, throughput, and the method behind every number.
65
+ Bonsai is the largest of these in the browser (about 56 KB from `bonsai-js/lite`, without explaining and partial evaluation, and about 44 KB from `bonsai-js/runtime` for rules checked and compiled on a server) and the newest. If rules must also run in Go or Java, CEL or ZEN fit better; if rules are JSON produced by a form builder and size matters most, json-logic-js does. The [full comparison](https://danfry1.github.io/bonsai-js/guide/comparison) covers more libraries, throughput, and the method behind every number.
66
66
 
67
67
  ## Install
68
68
 
@@ -74,7 +74,7 @@ Upgrading from 0.x? The [migration guide](https://danfry1.github.io/bonsai-js/gu
74
74
 
75
75
  Requires Node.js 22 or newer, current Bun, or a modern browser. The package is ESM; `require('bonsai-js')` works where Node can load ES modules synchronously (22.12 and newer); for the TypeScript settings that allow it, see [Install](https://danfry1.github.io/bonsai-js/guide/install#typescript-settings).
76
76
 
77
- In a browser bundle, an app that evaluates expressions adds about 68 KB minified and gzipped: the parser, type checker, compiler, and built-in library, with explaining and partial evaluation included. `bonsai-js/lite` leaves those two out, for about 56 KB. `bonsai-js/query` and `bonsai-js/service` are separate entry points; the language service on its own is about 46 KB, since it includes the type checker. Unused modules are tree-shaken: importing only `t` adds under 1 KB, and all the error classes about 1.6 KB. CI enforces these budgets (`bun run check:size`).
77
+ In a browser bundle, an app that evaluates expressions adds about 68 KB minified and gzipped: the parser, type checker, compiler, and built-in library, with explaining and partial evaluation included. `bonsai-js/lite` leaves those two out, for about 56 KB, and `bonsai-js/runtime`, which evaluates rules checked and compiled elsewhere, leaves out the checker too, for about 44 KB. `bonsai-js/query` and `bonsai-js/service` are separate entry points; the language service on its own is about 46 KB, since it includes the type checker. Unused modules are tree-shaken: importing only `t` adds under 1 KB, and all the error classes about 1.6 KB. CI enforces these budgets (`bun run check:size`).
78
78
 
79
79
  ## The language in one screen
80
80
 
@@ -94,7 +94,7 @@ startOfDay(order.placedAt, "Europe/Berlin") // calendar math in time
94
94
  // comments work too
95
95
  ```
96
96
 
97
- The full reference is [docs/language.md](https://github.com/danfry1/bonsai-js/blob/v1.0.0/docs/language.md). The rules that matter most:
97
+ The full reference is [docs/language.md](https://github.com/danfry1/bonsai-js/blob/v1.1.0/docs/language.md). The rules that matter most:
98
98
 
99
99
  - There is one absent value, `null`. Missing properties and host `undefined` read as `null`, so `x == null` means "missing or null". A key holding `undefined` is absent everywhere: `{ a: 1, b: undefined }` equals `{ a: 1 }`, and `"b" in` it is false.
100
100
  - `==` compares by value (lists and maps deeply). There is no type coercion: `1 == "1"` is `false` and `"a" + 1` is an error.
@@ -201,7 +201,7 @@ Every limit except `timeout` is on by default; `limits` changes the budget. `0`
201
201
  | `maxPatternLength` | 4,096 | regular expression patterns passed to `matches` |
202
202
  | `timeout` | none | wall-clock milliseconds per evaluation |
203
203
 
204
- The step budget is deterministic: the same expression over the same data uses the same steps on every machine, and at the default budget any expression finishes or fails within about 100 ms on Node (about 150 ms for the slowest shapes we have found). Host functions are trusted code: limits cannot interrupt a synchronous host function that is already running, and a getter or Proxy you put in the context runs when it is read. Set a `timeout` when expressions call slow host functions. See [SECURITY.md](https://github.com/danfry1/bonsai-js/blob/main/SECURITY.md) and [docs/threat-model.md](https://github.com/danfry1/bonsai-js/blob/v1.0.0/docs/threat-model.md).
204
+ The step budget is deterministic: the same expression over the same data uses the same steps on every machine, and at the default budget any expression finishes or fails within about 100 ms on Node (about 150 ms for the slowest shapes we have found). Host functions are trusted code: limits cannot interrupt a synchronous host function that is already running, and a getter or Proxy you put in the context runs when it is read. Set a `timeout` when expressions call slow host functions. See [SECURITY.md](https://github.com/danfry1/bonsai-js/blob/main/SECURITY.md) and [docs/threat-model.md](https://github.com/danfry1/bonsai-js/blob/v1.1.0/docs/threat-model.md).
205
205
 
206
206
  ## Built-in functions
207
207
 
@@ -299,6 +299,26 @@ toMongo(filter, { row: 'order', fields: columns, known: { minTotal: 100 } }) //
299
299
 
300
300
  The query selects exactly the records for which the filter evaluates to `true` in Bonsai, including null handling (SQL's three-valued logic is made two-valued) and records where the filter would fail (they are excluded). One exception: in Postgres, a record whose arithmetic overflows a double (or whose nonzero product rounds to zero) makes the whole query fail with `value out of range`, an error rather than a different set of rows, where SQLite excludes the record; keep stored numbers within range to avoid it (see the query guide's caveats). Only declared columns can be queried, values are always parameters, and anything without an exact database equivalent throws a `BonsaiTranslationError` pointing at it. This is checked by differential tests against real SQLite, Postgres, and a MongoDB query engine.
301
301
 
302
+ ## Rules in the browser
303
+
304
+ Check and compile where the full toolchain runs, then evaluate in the browser with `bonsai-js/runtime`, which does not bundle the checker. `toArtifact()` turns a compiled program into plain JSON:
305
+
306
+ ```ts
307
+ import { bonsai, t, toArtifact } from 'bonsai-js'
308
+ import { runtime } from 'bonsai-js/runtime'
309
+
310
+ const variables = { user: t.object({ age: t.number() }) }
311
+
312
+ // On the server, when the rule is saved.
313
+ const artifact = JSON.stringify(toArtifact(bonsai({ variables }).compile('user.age >= 18', { expect: t.boolean() })))
314
+
315
+ // In the browser.
316
+ const rule = runtime({ variables }).load(JSON.parse(artifact), { expect: t.boolean() })
317
+ rule.evaluateSync({ user: { age: 36 } }) // => true
318
+ ```
319
+
320
+ An artifact records the checker's choices that affect results and a digest of the definitions they depend on, so it evaluates exactly as the compiled program does, or a runtime with different definitions refuses it with a `BonsaiArtifactError` saying why. See [Runtime](https://danfry1.github.io/bonsai-js/api/runtime).
321
+
302
322
  ## Syntax trees and visual editors
303
323
 
304
324
  `env.parse(source)` returns a JSON syntax tree with source offsets on every node, and `print(tree)` turns a tree back into source. Printing is deterministic and round-trips: `print(env.parse(print(tree)))` is the same text, with the fewest parentheses that keep the meaning. That is what a visual rule builder needs: parse, edit the tree, print, save.
@@ -0,0 +1,47 @@
1
+ import { Type } from "./types.mjs";
2
+ //#region src/artifact.d.ts
3
+ /** The artifact format this release writes and reads. */
4
+ export declare const ARTIFACT_FORMAT = 1;
5
+ /**
6
+ * A call whose checked candidates are narrower than its function's overloads:
7
+ * `[nameStart, name, ...candidate overload indices]`.
8
+ */
9
+ export type Plan = readonly [nameStart: number, name: string, ...candidates: number[]];
10
+ /**
11
+ * A checked, compiled expression as plain JSON data, for `bonsai-js/runtime`
12
+ * to load where the checker is not bundled. The source stays the record: a
13
+ * full environment can always compile it again.
14
+ */
15
+ export interface Artifact {
16
+ /** Format version. */
17
+ readonly bonsai: typeof ARTIFACT_FORMAT;
18
+ /** The bonsai-js version that produced it (informational). */
19
+ readonly by: string;
20
+ readonly source: string;
21
+ /** The checker's overload choices where they are narrower than all overloads, by call. */
22
+ readonly plans: readonly Plan[];
23
+ /** The type the program was compiled to produce, if any. */
24
+ readonly expect?: Type;
25
+ /** Whether the result must be checked against `expect` at run time. */
26
+ readonly guard: boolean;
27
+ /** Whether evaluation may call an async host function. */
28
+ readonly async: boolean;
29
+ /** Binds the artifact to the definitions the checker saw; see artifactDigest. */
30
+ readonly digest: string;
31
+ }
32
+ /**
33
+ * A compiled program as plain JSON data, for `bonsai-js/runtime` to load and
34
+ * evaluate without the checker. It records the source, the checker's overload
35
+ * choices, the expected type, and a digest of the definitions they depend on,
36
+ * so a runtime with different definitions refuses it instead of running it
37
+ * differently. Throws TypeError for anything that is not a program compiled
38
+ * by an environment, and RangeError when the expected type is too large for a
39
+ * runtime to load (more than 100,000 type nodes).
40
+ *
41
+ * ```ts
42
+ * const rule = env.compile('user.age >= 18', { expect: t.boolean() })
43
+ * const artifact = toArtifact(rule) // store it, or send it to the browser
44
+ * ```
45
+ */
46
+ export declare function toArtifact(program: object): Artifact;
47
+ //#endregion
@@ -0,0 +1,139 @@
1
+ import { typeData } from "./types.mjs";
2
+ import { forEachChild } from "./syntax/ast.mjs";
3
+ import { compiledFacts } from "./declared.mjs";
4
+ import { encodeFunction, encodeType, hash64, lengthPrefixed } from "./digest.mjs";
5
+ import { VERSION } from "./version.mjs";
6
+ /**
7
+ * The most type nodes an artifact's `expect` may hold, counted along every
8
+ * path (shared subtrees count each time they are reached), so a type that
9
+ * repeats one subtree cannot hide its size. Its own constant rather than
10
+ * maxNodes: a large `t.enum` compiles on a default environment and must load
11
+ * on a default runtime.
12
+ */
13
+ const MAX_EXPECT_NODES = 1e5;
14
+ /** Throws RangeError when a type holds more than MAX_EXPECT_NODES nodes along all its paths. */
15
+ function assertExpectSize(type, what) {
16
+ let count = 0;
17
+ const stack = [type];
18
+ while (stack.length > 0) {
19
+ const next = stack.pop();
20
+ if (++count > 1e5) throw new RangeError(`${what} has more than ${MAX_EXPECT_NODES} type nodes`);
21
+ if (next.kind === "list") stack.push(next.element);
22
+ else if (next.kind === "union") stack.push(...next.types);
23
+ else if (next.kind === "map") {
24
+ stack.push(...Object.values(next.fields));
25
+ if (next.rest !== void 0) stack.push(next.rest);
26
+ }
27
+ }
28
+ }
29
+ /** Names of the functions a bound tree calls and the context variables it reads. */
30
+ function treeNames(root) {
31
+ const functions = /* @__PURE__ */ new Set();
32
+ const variables = /* @__PURE__ */ new Set();
33
+ const stack = [root];
34
+ while (stack.length > 0) {
35
+ const node = stack.pop();
36
+ if (node.type === "Call") functions.add(node.name);
37
+ else if (node.type === "Variable") variables.add(node.name);
38
+ forEachChild(node, (child) => {
39
+ stack.push(child);
40
+ });
41
+ }
42
+ return {
43
+ functions: [...functions].sort(),
44
+ variables: [...variables].sort()
45
+ };
46
+ }
47
+ /**
48
+ * The digest of an artifact: its fields (all but `digest`), every function the
49
+ * bound tree calls, and the declared type of every variable it reads, since
50
+ * the checker's overload choices and result guard depend on all of them. The
51
+ * producer and the runtime compute it the same way, so a stale or edited
52
+ * artifact fails to load instead of running with choices made for something
53
+ * else.
54
+ */
55
+ function artifactDigest(fields, root, lookup, variables) {
56
+ const names = treeNames(root);
57
+ const plans = fields.plans.map(([start, name, ...candidates]) => lengthPrefixed(`${start}:${lengthPrefixed(name)}${candidates.join(",")}`));
58
+ const functions = names.functions.map((name) => {
59
+ const def = lookup(name);
60
+ return def === void 0 ? lengthPrefixed(`?${name}`) : lengthPrefixed(encodeFunction(def));
61
+ });
62
+ const declared = names.variables.map((name) => {
63
+ const type = variables !== void 0 && Object.hasOwn(variables, name) ? variables[name] : void 0;
64
+ return lengthPrefixed(`${lengthPrefixed(name)}${type === void 0 ? "?" : encodeType(type)}`);
65
+ });
66
+ return hash64([
67
+ `bonsai${fields.bonsai}`,
68
+ lengthPrefixed(fields.by),
69
+ lengthPrefixed(fields.source),
70
+ lengthPrefixed(plans.join("")),
71
+ fields.expect === void 0 ? "-" : lengthPrefixed(encodeType(fields.expect)),
72
+ fields.guard ? "g" : "-",
73
+ fields.async ? "a" : "-",
74
+ lengthPrefixed(functions.join("")),
75
+ lengthPrefixed(declared.join(""))
76
+ ].join(""));
77
+ }
78
+ const artifacts = /* @__PURE__ */ new WeakMap();
79
+ /**
80
+ * A compiled program as plain JSON data, for `bonsai-js/runtime` to load and
81
+ * evaluate without the checker. It records the source, the checker's overload
82
+ * choices, the expected type, and a digest of the definitions they depend on,
83
+ * so a runtime with different definitions refuses it instead of running it
84
+ * differently. Throws TypeError for anything that is not a program compiled
85
+ * by an environment, and RangeError when the expected type is too large for a
86
+ * runtime to load (more than 100,000 type nodes).
87
+ *
88
+ * ```ts
89
+ * const rule = env.compile('user.age >= 18', { expect: t.boolean() })
90
+ * const artifact = toArtifact(rule) // store it, or send it to the browser
91
+ * ```
92
+ */
93
+ function toArtifact(program) {
94
+ const known = artifacts.get(program);
95
+ if (known !== void 0) return known;
96
+ const facts = compiledFacts(program);
97
+ if (facts === void 0) throw new TypeError("Not a program compiled by a Bonsai environment");
98
+ const artifact = makeArtifact(facts);
99
+ artifacts.set(program, artifact);
100
+ return artifact;
101
+ }
102
+ function makeArtifact({ source, analysis, expect, lookup, variables }) {
103
+ if (expect !== void 0) assertExpectSize(expect, "The expected type");
104
+ const plans = [];
105
+ for (const [call, plan] of analysis.calls) {
106
+ if (plan.candidates.length === plan.def.overloads.length) continue;
107
+ plans.push([
108
+ call.nameStart,
109
+ call.name,
110
+ ...plan.candidates
111
+ ]);
112
+ }
113
+ plans.sort((a, b) => a[0] - b[0]);
114
+ const fields = {
115
+ bonsai: 1,
116
+ by: VERSION,
117
+ source,
118
+ plans,
119
+ ...expect === void 0 ? {} : { expect: typeData(expect) },
120
+ guard: expect !== void 0 && analysis.checkResult,
121
+ async: analysis.async
122
+ };
123
+ const digest = artifactDigest(fields, analysis.root, lookup, variables);
124
+ return deepFreeze({
125
+ ...fields,
126
+ digest
127
+ });
128
+ }
129
+ function deepFreeze(value) {
130
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
131
+ Object.freeze(value);
132
+ for (const child of Object.values(value)) deepFreeze(child);
133
+ }
134
+ return value;
135
+ }
136
+ //#endregion
137
+ export { MAX_EXPECT_NODES, artifactDigest, assertExpectSize, toArtifact, treeNames };
138
+
139
+ //# sourceMappingURL=artifact.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.mjs","names":[],"sources":["../src/artifact.ts"],"sourcesContent":["import { compiledFacts, type CompiledFacts } from './declared.js'\nimport { encodeFunction, encodeType, hash64, lengthPrefixed } from './digest.js'\nimport type { FunctionDef } from './functions/define.js'\nimport { forEachChild, type Node } from './syntax/ast.js'\nimport { typeData, type Type } from './types.js'\nimport { VERSION } from './version.js'\n\n/** The artifact format this release writes and reads. */\nexport const ARTIFACT_FORMAT = 1\n\n/**\n * The most type nodes an artifact's `expect` may hold, counted along every\n * path (shared subtrees count each time they are reached), so a type that\n * repeats one subtree cannot hide its size. Its own constant rather than\n * maxNodes: a large `t.enum` compiles on a default environment and must load\n * on a default runtime.\n */\nexport const MAX_EXPECT_NODES = 100_000\n\n/**\n * A call whose checked candidates are narrower than its function's overloads:\n * `[nameStart, name, ...candidate overload indices]`.\n */\nexport type Plan = readonly [nameStart: number, name: string, ...candidates: number[]]\n\n/**\n * A checked, compiled expression as plain JSON data, for `bonsai-js/runtime`\n * to load where the checker is not bundled. The source stays the record: a\n * full environment can always compile it again.\n */\nexport interface Artifact {\n /** Format version. */\n readonly bonsai: typeof ARTIFACT_FORMAT\n /** The bonsai-js version that produced it (informational). */\n readonly by: string\n readonly source: string\n /** The checker's overload choices where they are narrower than all overloads, by call. */\n readonly plans: readonly Plan[]\n /** The type the program was compiled to produce, if any. */\n readonly expect?: Type\n /** Whether the result must be checked against `expect` at run time. */\n readonly guard: boolean\n /** Whether evaluation may call an async host function. */\n readonly async: boolean\n /** Binds the artifact to the definitions the checker saw; see artifactDigest. */\n readonly digest: string\n}\n\n/** Throws RangeError when a type holds more than MAX_EXPECT_NODES nodes along all its paths. */\nexport function assertExpectSize(type: Type, what: string): void {\n let count = 0\n const stack: Type[] = [type]\n while (stack.length > 0) {\n const next = stack.pop() as Type\n if (++count > MAX_EXPECT_NODES)\n throw new RangeError(`${what} has more than ${MAX_EXPECT_NODES} type nodes`)\n if (next.kind === 'list') stack.push(next.element)\n else if (next.kind === 'union') stack.push(...next.types)\n else if (next.kind === 'map') {\n stack.push(...Object.values(next.fields))\n if (next.rest !== undefined) stack.push(next.rest)\n }\n }\n}\n\n/** Names of the functions a bound tree calls and the context variables it reads. */\nexport function treeNames(root: Node): { functions: string[]; variables: string[] } {\n const functions = new Set<string>()\n const variables = new Set<string>()\n const stack: Node[] = [root]\n while (stack.length > 0) {\n const node = stack.pop() as Node\n if (node.type === 'Call') functions.add(node.name)\n else if (node.type === 'Variable') variables.add(node.name)\n forEachChild(node, (child) => {\n stack.push(child)\n })\n }\n return { functions: [...functions].sort(), variables: [...variables].sort() }\n}\n\n/**\n * The digest of an artifact: its fields (all but `digest`), every function the\n * bound tree calls, and the declared type of every variable it reads, since\n * the checker's overload choices and result guard depend on all of them. The\n * producer and the runtime compute it the same way, so a stale or edited\n * artifact fails to load instead of running with choices made for something\n * else.\n */\nexport function artifactDigest(\n fields: Omit<Artifact, 'digest'>,\n root: Node,\n lookup: (name: string) => FunctionDef | undefined,\n variables: Readonly<Record<string, Type>> | undefined,\n): string {\n const names = treeNames(root)\n const plans = fields.plans.map(([start, name, ...candidates]) =>\n lengthPrefixed(`${start}:${lengthPrefixed(name)}${candidates.join(',')}`),\n )\n const functions = names.functions.map((name) => {\n const def = lookup(name)\n return def === undefined ? lengthPrefixed(`?${name}`) : lengthPrefixed(encodeFunction(def))\n })\n const declared = names.variables.map((name) => {\n const type =\n variables !== undefined && Object.hasOwn(variables, name) ? variables[name] : undefined\n return lengthPrefixed(`${lengthPrefixed(name)}${type === undefined ? '?' : encodeType(type)}`)\n })\n return hash64(\n [\n `bonsai${fields.bonsai}`,\n lengthPrefixed(fields.by),\n lengthPrefixed(fields.source),\n lengthPrefixed(plans.join('')),\n fields.expect === undefined ? '-' : lengthPrefixed(encodeType(fields.expect)),\n fields.guard ? 'g' : '-',\n fields.async ? 'a' : '-',\n lengthPrefixed(functions.join('')),\n lengthPrefixed(declared.join('')),\n ].join(''),\n )\n}\n\nconst artifacts = new WeakMap<object, Artifact>()\n\n/**\n * A compiled program as plain JSON data, for `bonsai-js/runtime` to load and\n * evaluate without the checker. It records the source, the checker's overload\n * choices, the expected type, and a digest of the definitions they depend on,\n * so a runtime with different definitions refuses it instead of running it\n * differently. Throws TypeError for anything that is not a program compiled\n * by an environment, and RangeError when the expected type is too large for a\n * runtime to load (more than 100,000 type nodes).\n *\n * ```ts\n * const rule = env.compile('user.age >= 18', { expect: t.boolean() })\n * const artifact = toArtifact(rule) // store it, or send it to the browser\n * ```\n */\nexport function toArtifact(program: object): Artifact {\n const known = artifacts.get(program)\n if (known !== undefined) return known\n const facts = compiledFacts(program)\n if (facts === undefined) throw new TypeError('Not a program compiled by a Bonsai environment')\n const artifact = makeArtifact(facts)\n artifacts.set(program, artifact)\n return artifact\n}\n\nfunction makeArtifact({ source, analysis, expect, lookup, variables }: CompiledFacts): Artifact {\n if (expect !== undefined) assertExpectSize(expect, 'The expected type')\n const plans: Plan[] = []\n for (const [call, plan] of analysis.calls) {\n if (plan.candidates.length === plan.def.overloads.length) continue\n // The parser sets nameStart on every call it builds.\n plans.push([call.nameStart as number, call.name, ...plan.candidates])\n }\n plans.sort((a, b) => a[0] - b[0])\n const fields: Omit<Artifact, 'digest'> = {\n bonsai: ARTIFACT_FORMAT,\n by: VERSION,\n source,\n plans,\n // Frozen plain data, as the artifact must be; a type built by t already is.\n ...(expect === undefined ? {} : { expect: typeData(expect) }),\n guard: expect !== undefined && analysis.checkResult,\n async: analysis.async,\n }\n const digest = artifactDigest(fields, analysis.root, lookup, variables)\n return deepFreeze({ ...fields, digest })\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n Object.freeze(value)\n for (const child of Object.values(value)) deepFreeze(child)\n }\n return value\n}\n"],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import { isItemLambdaPosition, isLambdaPosition } from "../functions/define.mjs";
2
+ import { forEachChild, mapChildren } from "../syntax/ast.mjs";
3
+ //#region src/check/bind.ts
4
+ /**
5
+ * Binds the implicit parameter: an argument in a lambda position of the called
6
+ * function (per its overloads, see isLambdaPosition) that contains a free `.`
7
+ * becomes an implicit Lambda. Depends only on the tree and the signatures of
8
+ * the functions it calls, so the checker and code that compiles without it
9
+ * bind every source the same way.
10
+ */
11
+ function bindImplicit(root, lookup) {
12
+ const problems = [];
13
+ if (!containsIt(root)) return {
14
+ root,
15
+ problems
16
+ };
17
+ /** Rewrites implicit lambdas as explicit ones; `free` when a `.` is left unbound. */
18
+ function bind(node) {
19
+ if (node.type === "It") return {
20
+ node,
21
+ free: true
22
+ };
23
+ if (node.type === "Call") return bindCall(node);
24
+ let free = false;
25
+ return {
26
+ node: mapChildren(node, (child) => {
27
+ const r = bind(child);
28
+ free ||= r.free;
29
+ return r.node;
30
+ }),
31
+ free: node.type === "Lambda" ? false : free
32
+ };
33
+ }
34
+ function bindCall(node) {
35
+ const def = lookup(node.name);
36
+ let free = false;
37
+ let sawSpread = false;
38
+ const args = node.args.map((arg, index) => {
39
+ if (arg.type === "Spread") {
40
+ sawSpread = true;
41
+ const r = bind(arg.argument);
42
+ free ||= r.free;
43
+ return {
44
+ ...arg,
45
+ argument: r.node
46
+ };
47
+ }
48
+ const lambdaPosition = !sawSpread && (def === void 0 || isLambdaPosition(def, index));
49
+ if (arg.type === "Lambda") {
50
+ const body = bind(arg.body);
51
+ if (body.free) problems.push({
52
+ message: "Use the lambda parameter instead of \".\" inside an explicit lambda",
53
+ at: findIt(body.node) ?? arg
54
+ });
55
+ return {
56
+ ...arg,
57
+ body: body.node
58
+ };
59
+ }
60
+ const r = bind(arg);
61
+ if (lambdaPosition && r.free) {
62
+ if (def !== void 0 && !isItemLambdaPosition(def, index)) problems.push({
63
+ message: `"." is the current item, but the first parameter of ${def.name}'s lambda is not; write the parameters, e.g. (acc, x) => acc + x`,
64
+ at: findIt(r.node) ?? arg
65
+ });
66
+ return {
67
+ type: "Lambda",
68
+ params: [],
69
+ implicit: true,
70
+ body: r.node,
71
+ start: r.node.start,
72
+ end: r.node.end
73
+ };
74
+ }
75
+ free ||= r.free;
76
+ return r.node;
77
+ });
78
+ return {
79
+ node: {
80
+ ...node,
81
+ args
82
+ },
83
+ free
84
+ };
85
+ }
86
+ const bound = bind(root);
87
+ if (bound.free) problems.push({
88
+ message: "\".\" (the current item) must be inside an argument of a function that takes a lambda, e.g. items.filter(.active)",
89
+ at: findIt(bound.node) ?? bound.node
90
+ });
91
+ return {
92
+ root: bound.node,
93
+ problems
94
+ };
95
+ }
96
+ function containsIt(node) {
97
+ if (node.type === "It") return true;
98
+ let found = false;
99
+ forEachChild(node, (child) => {
100
+ if (!found && containsIt(child)) found = true;
101
+ });
102
+ return found;
103
+ }
104
+ function findIt(node) {
105
+ if (node.type === "It") return node;
106
+ let found;
107
+ const visit = (child) => {
108
+ found ??= findIt(child);
109
+ };
110
+ if (node.type === "Lambda") return void 0;
111
+ forEachChild(node, visit);
112
+ return found;
113
+ }
114
+ //#endregion
115
+ export { bindImplicit };
116
+
117
+ //# sourceMappingURL=bind.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind.mjs","names":[],"sources":["../../src/check/bind.ts"],"sourcesContent":["import { isItemLambdaPosition, isLambdaPosition, type FunctionDef } from '../functions/define.js'\nimport {\n forEachChild,\n mapChildren,\n type CallNode,\n type LambdaNode,\n type Node,\n type SpreadNode,\n} from '../syntax/ast.js'\n\n/** A `.` that binds to no lambda, or to the wrong parameter: an INVALID_LAMBDA diagnostic. */\nexport interface BindProblem {\n readonly message: string\n readonly at: { readonly start: number; readonly end: number }\n}\n\nexport interface Bound {\n /** The tree with implicit lambdas made explicit. */\n readonly root: Node\n /** In the order the checker reports them. */\n readonly problems: readonly BindProblem[]\n}\n\n/**\n * Binds the implicit parameter: an argument in a lambda position of the called\n * function (per its overloads, see isLambdaPosition) that contains a free `.`\n * becomes an implicit Lambda. Depends only on the tree and the signatures of\n * the functions it calls, so the checker and code that compiles without it\n * bind every source the same way.\n */\nexport function bindImplicit(root: Node, lookup: (name: string) => FunctionDef | undefined): Bound {\n const problems: BindProblem[] = []\n // Binding only rewrites the tree around `.`; most expressions have none.\n if (!containsIt(root)) return { root, problems }\n\n /** Rewrites implicit lambdas as explicit ones; `free` when a `.` is left unbound. */\n function bind(node: Node): { node: Node; free: boolean } {\n if (node.type === 'It') return { node, free: true }\n if (node.type === 'Call') return bindCall(node)\n let free = false\n const out = mapChildren(node, (child) => {\n const r = bind(child)\n free ||= r.free\n return r.node\n })\n // A lambda outside a call argument position (the parser only allows\n // arguments, so this is a lambda in a non-function parameter).\n return { node: out, free: node.type === 'Lambda' ? false : free }\n }\n\n function bindCall(node: CallNode): { node: Node; free: boolean } {\n const def = lookup(node.name)\n let free = false\n let sawSpread = false\n const args = node.args.map((arg, index): Node | SpreadNode => {\n if (arg.type === 'Spread') {\n sawSpread = true\n const r = bind(arg.argument)\n free ||= r.free\n return { ...arg, argument: r.node }\n }\n // An unknown function reports itself; treat its arguments as lambdas so a\n // misspelled filter(.x) does not also report a stray `.`.\n const lambdaPosition = !sawSpread && (def === undefined || isLambdaPosition(def, index))\n if (arg.type === 'Lambda') {\n const body = bind(arg.body)\n if (body.free) {\n problems.push({\n message: 'Use the lambda parameter instead of \".\" inside an explicit lambda',\n at: findIt(body.node) ?? arg,\n })\n }\n return { ...arg, body: body.node }\n }\n const r = bind(arg)\n if (lambdaPosition && r.free) {\n if (def !== undefined && !isItemLambdaPosition(def, index)) {\n problems.push({\n message: `\".\" is the current item, but the first parameter of ${def.name}'s lambda is not; write the parameters, e.g. (acc, x) => acc + x`,\n at: findIt(r.node) ?? arg,\n })\n }\n const lambda: LambdaNode = {\n type: 'Lambda',\n params: [],\n implicit: true,\n body: r.node,\n start: r.node.start,\n end: r.node.end,\n }\n return lambda\n }\n free ||= r.free\n return r.node\n })\n return { node: { ...node, args }, free }\n }\n\n const bound = bind(root)\n if (bound.free) {\n problems.push({\n message:\n '\".\" (the current item) must be inside an argument of a function that takes a lambda, e.g. items.filter(.active)',\n at: findIt(bound.node) ?? bound.node,\n })\n }\n return { root: bound.node, problems }\n}\n\nfunction containsIt(node: Node): boolean {\n if (node.type === 'It') return true\n let found = false\n forEachChild(node, (child) => {\n if (!found && containsIt(child)) found = true\n })\n return found\n}\n\nfunction findIt(node: Node): Node | undefined {\n if (node.type === 'It') return node\n let found: Node | undefined\n const visit = (child: Node): void => {\n found ??= findIt(child)\n }\n if (node.type === 'Lambda') return undefined\n forEachChild(node, visit)\n return found\n}\n"],"mappings":";;;;;;;;;;AA8BA,SAAgB,aAAa,MAAY,QAA0D;CACjG,MAAM,WAA0B,CAAC;CAEjC,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO;EAAE;EAAM;CAAS;;CAG/C,SAAS,KAAK,MAA2C;EACvD,IAAI,KAAK,SAAS,MAAM,OAAO;GAAE;GAAM,MAAM;EAAK;EAClD,IAAI,KAAK,SAAS,QAAQ,OAAO,SAAS,IAAI;EAC9C,IAAI,OAAO;EAQX,OAAO;GAAE,MAPG,YAAY,OAAO,UAAU;IACvC,MAAM,IAAI,KAAK,KAAK;IACpB,SAAS,EAAE;IACX,OAAO,EAAE;GACX,CAGiB;GAAG,MAAM,KAAK,SAAS,WAAW,QAAQ;EAAK;CAClE;CAEA,SAAS,SAAS,MAA+C;EAC/D,MAAM,MAAM,OAAO,KAAK,IAAI;EAC5B,IAAI,OAAO;EACX,IAAI,YAAY;EAChB,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,UAA6B;GAC5D,IAAI,IAAI,SAAS,UAAU;IACzB,YAAY;IACZ,MAAM,IAAI,KAAK,IAAI,QAAQ;IAC3B,SAAS,EAAE;IACX,OAAO;KAAE,GAAG;KAAK,UAAU,EAAE;IAAK;GACpC;GAGA,MAAM,iBAAiB,CAAC,cAAc,QAAQ,KAAA,KAAa,iBAAiB,KAAK,KAAK;GACtF,IAAI,IAAI,SAAS,UAAU;IACzB,MAAM,OAAO,KAAK,IAAI,IAAI;IAC1B,IAAI,KAAK,MACP,SAAS,KAAK;KACZ,SAAS;KACT,IAAI,OAAO,KAAK,IAAI,KAAK;IAC3B,CAAC;IAEH,OAAO;KAAE,GAAG;KAAK,MAAM,KAAK;IAAK;GACnC;GACA,MAAM,IAAI,KAAK,GAAG;GAClB,IAAI,kBAAkB,EAAE,MAAM;IAC5B,IAAI,QAAQ,KAAA,KAAa,CAAC,qBAAqB,KAAK,KAAK,GACvD,SAAS,KAAK;KACZ,SAAS,uDAAuD,IAAI,KAAK;KACzE,IAAI,OAAO,EAAE,IAAI,KAAK;IACxB,CAAC;IAUH,OAAO;KAPL,MAAM;KACN,QAAQ,CAAC;KACT,UAAU;KACV,MAAM,EAAE;KACR,OAAO,EAAE,KAAK;KACd,KAAK,EAAE,KAAK;IAEF;GACd;GACA,SAAS,EAAE;GACX,OAAO,EAAE;EACX,CAAC;EACD,OAAO;GAAE,MAAM;IAAE,GAAG;IAAM;GAAK;GAAG;EAAK;CACzC;CAEA,MAAM,QAAQ,KAAK,IAAI;CACvB,IAAI,MAAM,MACR,SAAS,KAAK;EACZ,SACE;EACF,IAAI,OAAO,MAAM,IAAI,KAAK,MAAM;CAClC,CAAC;CAEH,OAAO;EAAE,MAAM,MAAM;EAAM;CAAS;AACtC;AAEA,SAAS,WAAW,MAAqB;CACvC,IAAI,KAAK,SAAS,MAAM,OAAO;CAC/B,IAAI,QAAQ;CACZ,aAAa,OAAO,UAAU;EAC5B,IAAI,CAAC,SAAS,WAAW,KAAK,GAAG,QAAQ;CAC3C,CAAC;CACD,OAAO;AACT;AAEA,SAAS,OAAO,MAA8B;CAC5C,IAAI,KAAK,SAAS,MAAM,OAAO;CAC/B,IAAI;CACJ,MAAM,SAAS,UAAsB;EACnC,UAAU,OAAO,KAAK;CACxB;CACA,IAAI,KAAK,SAAS,UAAU,OAAO,KAAA;CACnC,aAAa,MAAM,KAAK;CACxB,OAAO;AACT"}
@@ -1,11 +1,12 @@
1
1
  import { BonsaiLimitError } from "../errors.mjs";
2
2
  import { BLOCKED_NAMES } from "../syntax/lexer.mjs";
3
- import { TypeBudgetExceeded, chargeTypeWork, exactObject, fieldOf, formatType, freshLiteral, isAssignable, isExact, isNullable, isProvenAssignable, nonNull, overlaps, sameType, t, unionMembers, unionOf, widen, widenFresh, withFields, withTypeBudget } from "../types.mjs";
4
- import { isItemLambdaPosition, isLambdaPosition } from "../functions/define.mjs";
3
+ import { TypeBudgetExceeded, chargeTypeWork, exactObject, fieldOf, freshLiteral, isAssignable, isExact, isNullable, isProvenAssignable, nonNull, overlaps, sameType, t, typeText, unionMembers, unionOf, widen, widenFresh, withFields, withTypeBudget } from "../types.mjs";
4
+ import { signatureText } from "../functions/define.mjs";
5
+ import { forEachChild } from "../syntax/ast.mjs";
6
+ import { bindImplicit } from "./bind.mjs";
5
7
  import { RESULT_REFINERS } from "../functions/builtins.mjs";
6
8
  import { isStackOverflow, tooDeep } from "../runtime/overflow.mjs";
7
9
  import { closest, didYouMean } from "../suggest.mjs";
8
- import { forEachChild, mapChildren } from "../syntax/ast.mjs";
9
10
  import { writtenWithExponent } from "../syntax/parser.mjs";
10
11
  //#region src/check/checker.ts
11
12
  function hasFact(facts, key) {
@@ -21,14 +22,6 @@ function hasFact(facts, key) {
21
22
  * limits admit, small enough that a pathological one fails fast.
22
23
  */
23
24
  const CHECK_BUDGET = 5e5;
24
- function containsIt(node) {
25
- if (node.type === "It") return true;
26
- let found = false;
27
- forEachChild(node, (child) => {
28
- if (!found && containsIt(child)) found = true;
29
- });
30
- return found;
31
- }
32
25
  /** Whether a type explicitly admits null (unlike `any`, which admits everything). */
33
26
  function mayBeNull(type) {
34
27
  return type.kind === "null" || type.kind === "union" && type.types.some(mayBeNull);
@@ -393,85 +386,19 @@ function analyzeWithin(root, env, options) {
393
386
  * this node writes the entries for its subtree.
394
387
  */
395
388
  const callMemo = /* @__PURE__ */ new WeakMap();
396
- const bound = containsIt(root) ? bind(root) : {
397
- node: root,
398
- free: false
399
- };
400
- if (bound.free) report("INVALID_LAMBDA", "\".\" (the current item) must be inside an argument of a function that takes a lambda, e.g. items.filter(.active)", findIt(bound.node) ?? bound.node);
401
- /** Rewrites implicit lambdas as explicit ones; `free` when a `.` is left unbound. */
402
- function bind(node) {
403
- if (node.type === "It") return {
404
- node,
405
- free: true
406
- };
407
- if (node.type === "Call") return bindCall(node);
408
- let free = false;
409
- return {
410
- node: mapChildren(node, (child) => {
411
- const r = bind(child);
412
- free ||= r.free;
413
- return r.node;
414
- }),
415
- free: node.type === "Lambda" ? false : free
416
- };
417
- }
418
- function bindCall(node) {
419
- const def = env.lookup(node.name);
420
- let free = false;
421
- let sawSpread = false;
422
- const args = node.args.map((arg, index) => {
423
- if (arg.type === "Spread") {
424
- sawSpread = true;
425
- const r = bind(arg.argument);
426
- free ||= r.free;
427
- return {
428
- ...arg,
429
- argument: r.node
430
- };
431
- }
432
- const lambdaPosition = !sawSpread && (def === void 0 || isLambdaPosition(def, index));
433
- if (arg.type === "Lambda") {
434
- const body = bind(arg.body);
435
- if (body.free) report("INVALID_LAMBDA", "Use the lambda parameter instead of \".\" inside an explicit lambda", findIt(body.node) ?? arg);
436
- return {
437
- ...arg,
438
- body: body.node
439
- };
440
- }
441
- const r = bind(arg);
442
- if (lambdaPosition && r.free) {
443
- if (def !== void 0 && !isItemLambdaPosition(def, index)) report("INVALID_LAMBDA", `"." is the current item, but the first parameter of ${def.name}'s lambda is not; write the parameters, e.g. (acc, x) => acc + x`, findIt(r.node) ?? arg);
444
- return {
445
- type: "Lambda",
446
- params: [],
447
- implicit: true,
448
- body: r.node,
449
- start: r.node.start,
450
- end: r.node.end
451
- };
452
- }
453
- free ||= r.free;
454
- return r.node;
455
- });
456
- return {
457
- node: {
458
- ...node,
459
- args
460
- },
461
- free
462
- };
463
- }
464
- const rootScope = {
389
+ const binding = bindImplicit(root, (name) => env.lookup(name));
390
+ for (const problem of binding.problems) report("INVALID_LAMBDA", problem.message, problem.at);
391
+ const boundRoot = binding.root;
392
+ const raw = check(boundRoot, {
465
393
  locals: /* @__PURE__ */ new Map(),
466
394
  it: void 0
467
- };
468
- const raw = check(bound.node, rootScope, options.expected);
395
+ }, options.expected);
469
396
  let checkResult = false;
470
397
  if (options.expected !== void 0) {
471
398
  const problem = expectationProblem(raw, options.expected);
472
- if (problem !== void 0) report("EXPECTED_TYPE", problem, resultSpan(bound.node));
399
+ if (problem !== void 0) report("EXPECTED_TYPE", problem, resultSpan(boundRoot));
473
400
  const extra = unexpectedFields(raw, options.expected);
474
- if (extra !== void 0) report("EXPECTED_TYPE", extra, resultSpan(bound.node), "warning");
401
+ if (extra !== void 0) report("EXPECTED_TYPE", extra, resultSpan(boundRoot), "warning");
475
402
  checkResult = containsAny(raw) || !isProvenAssignable(raw, options.expected);
476
403
  }
477
404
  const type = widen(raw);
@@ -1268,12 +1195,12 @@ function analyzeWithin(root, env, options) {
1268
1195
  report("NO_OVERLOAD", `${node.name}(${shown}) does not match ${def.overloads.length > 1 ? "any of " : ""}${signatures}`, node);
1269
1196
  }
1270
1197
  return {
1271
- root: bound.node,
1198
+ root: boundRoot,
1272
1199
  type,
1273
1200
  diagnostics,
1274
1201
  calls,
1275
1202
  types,
1276
- async: asyncNodes.has(bound.node),
1203
+ async: asyncNodes.has(boundRoot),
1277
1204
  checkResult,
1278
1205
  references: {
1279
1206
  variables: [...variables],
@@ -1504,22 +1431,6 @@ function substitute(type, b) {
1504
1431
  default: return type;
1505
1432
  }
1506
1433
  }
1507
- function signatureText(name, candidate) {
1508
- const required = candidate.required ?? candidate.params.length;
1509
- const params = candidate.params.map((param, i) => `${typeText(param)}${i >= required ? "?" : ""}`);
1510
- if (candidate.rest !== void 0) params.push(`...${typeText(candidate.rest)}`);
1511
- return `${name}(${params.join(", ")}): ${typeText(candidate.result)}`;
1512
- }
1513
- function findIt(node) {
1514
- if (node.type === "It") return node;
1515
- let found;
1516
- const visit = (child) => {
1517
- found ??= findIt(child);
1518
- };
1519
- if (node.type === "Lambda") return void 0;
1520
- forEachChild(node, visit);
1521
- return found;
1522
- }
1523
1434
  /** Names from other languages, with the Bonsai spelling. */
1524
1435
  const FUNCTION_ALIASES = {
1525
1436
  length: "length is a property: write x.length",
@@ -1566,19 +1477,6 @@ const JS_GLOBALS = {
1566
1477
  String: "use toString(x)",
1567
1478
  Array: "use a list literal [...]"
1568
1479
  };
1569
- /**
1570
- * Longest type text in a diagnostic message. A message can name a large object
1571
- * type, and a source can repeat the mistake thousands of times, so a short cap
1572
- * keeps diagnostics (and their JSON) near the size of the source.
1573
- */
1574
- const MESSAGE_TYPE_TEXT = 120;
1575
- const typeTexts = /* @__PURE__ */ new WeakMap();
1576
- /** A type's text for a diagnostic message, shortened with an ellipsis. */
1577
- function typeText(type) {
1578
- let text = typeTexts.get(type);
1579
- if (text === void 0) typeTexts.set(type, text = formatType(type, MESSAGE_TYPE_TEXT));
1580
- return text;
1581
- }
1582
1480
  /** Longest receiver path a diagnostic message names instead of its type. */
1583
1481
  const MESSAGE_PATH_TEXT = 60;
1584
1482
  /**
@@ -1632,6 +1530,6 @@ function suggest(name, candidatesOf) {
1632
1530
  return closest(name, candidatesOf());
1633
1531
  }
1634
1532
  //#endregion
1635
- export { acceptsArgument, analyze, signatureText };
1533
+ export { acceptsArgument, analyze };
1636
1534
 
1637
1535
  //# sourceMappingURL=checker.mjs.map