bonsai-js 0.2.1 → 0.4.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/dist/index.mjs CHANGED
@@ -1,477 +1,5 @@
1
- import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-t5i9AEaf.mjs";
2
- //#region src/lexer.ts
3
- const KEYWORDS = new Map([
4
- ["true", {
5
- type: "Boolean",
6
- value: "true"
7
- }],
8
- ["false", {
9
- type: "Boolean",
10
- value: "false"
11
- }],
12
- ["null", {
13
- type: "Null",
14
- value: "null"
15
- }],
16
- ["undefined", {
17
- type: "Undefined",
18
- value: "undefined"
19
- }],
20
- ["in", {
21
- type: "Operator",
22
- value: "in"
23
- }],
24
- ["not", {
25
- type: "Operator",
26
- value: "not"
27
- }]
28
- ]);
29
- function isDigit(ch) {
30
- return ch >= "0" && ch <= "9";
31
- }
32
- function isAlpha(ch) {
33
- return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
34
- }
35
- function isAlphaNumeric(ch) {
36
- return isAlpha(ch) || isDigit(ch);
37
- }
38
- function isHexDigit(ch) {
39
- return isDigit(ch) || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
40
- }
41
- function tokenize(source) {
42
- const tokens = [];
43
- let i = 0;
44
- while (i < source.length) {
45
- const ch = source[i];
46
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
47
- i++;
48
- continue;
49
- }
50
- if (isDigit(ch)) {
51
- const start = i;
52
- if (ch === "0" && i + 1 < source.length && (source[i + 1] === "x" || source[i + 1] === "X")) {
53
- i += 2;
54
- while (i < source.length && (isHexDigit(source[i]) || source[i] === "_")) i++;
55
- tokens.push({
56
- type: "Number",
57
- value: source.slice(start, i),
58
- start,
59
- end: i
60
- });
61
- continue;
62
- }
63
- if (ch === "0" && i + 1 < source.length && (source[i + 1] === "b" || source[i + 1] === "B")) {
64
- i += 2;
65
- while (i < source.length && (source[i] === "0" || source[i] === "1" || source[i] === "_")) i++;
66
- tokens.push({
67
- type: "Number",
68
- value: source.slice(start, i),
69
- start,
70
- end: i
71
- });
72
- continue;
73
- }
74
- if (ch === "0" && i + 1 < source.length && (source[i + 1] === "o" || source[i + 1] === "O")) {
75
- i += 2;
76
- while (i < source.length && (source[i] >= "0" && source[i] <= "7" || source[i] === "_")) i++;
77
- tokens.push({
78
- type: "Number",
79
- value: source.slice(start, i),
80
- start,
81
- end: i
82
- });
83
- continue;
84
- }
85
- while (i < source.length && (isDigit(source[i]) || source[i] === "_")) i++;
86
- if (i < source.length && source[i] === "." && i + 1 < source.length && isDigit(source[i + 1])) {
87
- i++;
88
- while (i < source.length && (isDigit(source[i]) || source[i] === "_")) i++;
89
- }
90
- if (i < source.length && (source[i] === "e" || source[i] === "E")) {
91
- i++;
92
- if (i < source.length && (source[i] === "+" || source[i] === "-")) i++;
93
- while (i < source.length && (isDigit(source[i]) || source[i] === "_")) i++;
94
- }
95
- tokens.push({
96
- type: "Number",
97
- value: source.slice(start, i),
98
- start,
99
- end: i
100
- });
101
- continue;
102
- }
103
- if (ch === "\"" || ch === "'") {
104
- const quote = ch;
105
- const start = i;
106
- i++;
107
- let value = "";
108
- while (i < source.length && source[i] !== quote) {
109
- if (source[i] === "\\") {
110
- i++;
111
- const escaped = source[i];
112
- switch (escaped) {
113
- case "n":
114
- value += "\n";
115
- break;
116
- case "t":
117
- value += " ";
118
- break;
119
- case "r":
120
- value += "\r";
121
- break;
122
- case "\\":
123
- value += "\\";
124
- break;
125
- case "0":
126
- value += "\0";
127
- break;
128
- case "x": {
129
- const hex = source.slice(i + 1, i + 3);
130
- if (hex.length < 2 || !isHexDigit(hex[0]) || !isHexDigit(hex[1])) throw new ExpressionError("Invalid \\x escape: expected 2 hex digits", {
131
- source,
132
- start,
133
- end: i + 3
134
- });
135
- value += String.fromCharCode(parseInt(hex, 16));
136
- i += 2;
137
- break;
138
- }
139
- case "u":
140
- if (i + 1 < source.length && source[i + 1] === "{") {
141
- const closeBrace = source.indexOf("}", i + 2);
142
- if (closeBrace === -1) throw new ExpressionError("Invalid \\u{} escape: missing closing brace", {
143
- source,
144
- start,
145
- end: source.length
146
- });
147
- const hex = source.slice(i + 2, closeBrace);
148
- if (hex.length === 0) throw new ExpressionError("Invalid \\u{} escape: empty code point", {
149
- source,
150
- start,
151
- end: closeBrace + 1
152
- });
153
- for (const c of hex) if (!isHexDigit(c)) throw new ExpressionError(`Invalid \\u{} escape: non-hex digit "${c}"`, {
154
- source,
155
- start,
156
- end: closeBrace + 1
157
- });
158
- const MAX_UNICODE_CODE_POINT = 1114111;
159
- const codePoint = parseInt(hex, 16);
160
- if (codePoint > MAX_UNICODE_CODE_POINT) throw new ExpressionError("Invalid \\u{} escape: code point out of range", {
161
- source,
162
- start,
163
- end: closeBrace + 1
164
- });
165
- value += String.fromCodePoint(codePoint);
166
- i = closeBrace;
167
- } else {
168
- const UNICODE_ESCAPE_LEN = 4;
169
- const hex = source.slice(i + 1, i + 1 + UNICODE_ESCAPE_LEN);
170
- if (hex.length < UNICODE_ESCAPE_LEN) throw new ExpressionError("Invalid \\u escape: expected 4 hex digits", {
171
- source,
172
- start,
173
- end: i + 1 + UNICODE_ESCAPE_LEN
174
- });
175
- for (const c of hex) if (!isHexDigit(c)) throw new ExpressionError("Invalid \\u escape: expected 4 hex digits", {
176
- source,
177
- start,
178
- end: i + 1 + UNICODE_ESCAPE_LEN
179
- });
180
- value += String.fromCharCode(parseInt(hex, 16));
181
- i += UNICODE_ESCAPE_LEN;
182
- }
183
- break;
184
- default:
185
- value += escaped;
186
- break;
187
- }
188
- } else value += source[i];
189
- i++;
190
- }
191
- if (i >= source.length) throw new ExpressionError("Unterminated string", {
192
- source,
193
- start,
194
- end: i
195
- });
196
- i++;
197
- tokens.push({
198
- type: "String",
199
- value,
200
- start,
201
- end: i
202
- });
203
- continue;
204
- }
205
- if (ch === "`") {
206
- const start = i;
207
- i++;
208
- let raw = "`";
209
- while (i < source.length && source[i] !== "`") if (source[i] === "$" && i + 1 < source.length && source[i + 1] === "{") {
210
- raw += "${";
211
- i += 2;
212
- let depth = 1;
213
- while (i < source.length && depth > 0) {
214
- const ic = source[i];
215
- if (ic === "\"" || ic === "'") {
216
- raw += ic;
217
- i++;
218
- while (i < source.length && source[i] !== ic) {
219
- if (source[i] === "\\" && i + 1 < source.length) {
220
- raw += source[i] + source[i + 1];
221
- i += 2;
222
- continue;
223
- }
224
- raw += source[i];
225
- i++;
226
- }
227
- if (i < source.length) {
228
- raw += source[i];
229
- i++;
230
- }
231
- continue;
232
- }
233
- if (ic === "{") depth++;
234
- else if (ic === "}") depth--;
235
- if (depth > 0) raw += ic;
236
- i++;
237
- }
238
- raw += "}";
239
- } else {
240
- raw += source[i];
241
- i++;
242
- }
243
- if (i >= source.length) throw new ExpressionError("Unterminated template literal", {
244
- source,
245
- start,
246
- end: i
247
- });
248
- raw += "`";
249
- i++;
250
- tokens.push({
251
- type: "TemplateLiteral",
252
- value: raw,
253
- start,
254
- end: i
255
- });
256
- continue;
257
- }
258
- if (isAlpha(ch)) {
259
- const start = i;
260
- while (i < source.length && isAlphaNumeric(source[i])) i++;
261
- const value = source.slice(start, i);
262
- const kw = KEYWORDS.get(value);
263
- if (kw) tokens.push({
264
- type: kw.type,
265
- value: kw.value,
266
- start,
267
- end: i
268
- });
269
- else tokens.push({
270
- type: "Identifier",
271
- value,
272
- start,
273
- end: i
274
- });
275
- continue;
276
- }
277
- const start = i;
278
- if (ch === "." && i + 2 < source.length && source[i + 1] === "." && source[i + 2] === ".") {
279
- tokens.push({
280
- type: "Spread",
281
- value: "...",
282
- start,
283
- end: i + 3
284
- });
285
- i += 3;
286
- continue;
287
- }
288
- if (ch === ".") {
289
- tokens.push({
290
- type: "Punctuation",
291
- value: ".",
292
- start,
293
- end: i + 1
294
- });
295
- i++;
296
- continue;
297
- }
298
- if (ch === "|") {
299
- if (i + 1 < source.length && source[i + 1] === ">") {
300
- tokens.push({
301
- type: "Pipe",
302
- value: "|>",
303
- start,
304
- end: i + 2
305
- });
306
- i += 2;
307
- continue;
308
- }
309
- if (i + 1 < source.length && source[i + 1] === "|") {
310
- tokens.push({
311
- type: "Operator",
312
- value: "||",
313
- start,
314
- end: i + 2
315
- });
316
- i += 2;
317
- continue;
318
- }
319
- }
320
- if (ch === "&" && i + 1 < source.length && source[i + 1] === "&") {
321
- tokens.push({
322
- type: "Operator",
323
- value: "&&",
324
- start,
325
- end: i + 2
326
- });
327
- i += 2;
328
- continue;
329
- }
330
- if (ch === "=" && i + 1 < source.length && source[i + 1] === "=") {
331
- tokens.push({
332
- type: "Operator",
333
- value: "==",
334
- start,
335
- end: i + 2
336
- });
337
- i += 2;
338
- continue;
339
- }
340
- if (ch === "!" && i + 1 < source.length && source[i + 1] === "=") {
341
- tokens.push({
342
- type: "Operator",
343
- value: "!=",
344
- start,
345
- end: i + 2
346
- });
347
- i += 2;
348
- continue;
349
- }
350
- if (ch === "!") {
351
- tokens.push({
352
- type: "Operator",
353
- value: "!",
354
- start,
355
- end: i + 1
356
- });
357
- i++;
358
- continue;
359
- }
360
- if (ch === "<") {
361
- if (i + 1 < source.length && source[i + 1] === "=") {
362
- tokens.push({
363
- type: "Operator",
364
- value: "<=",
365
- start,
366
- end: i + 2
367
- });
368
- i += 2;
369
- continue;
370
- }
371
- tokens.push({
372
- type: "Operator",
373
- value: "<",
374
- start,
375
- end: i + 1
376
- });
377
- i++;
378
- continue;
379
- }
380
- if (ch === ">") {
381
- if (i + 1 < source.length && source[i + 1] === "=") {
382
- tokens.push({
383
- type: "Operator",
384
- value: ">=",
385
- start,
386
- end: i + 2
387
- });
388
- i += 2;
389
- continue;
390
- }
391
- tokens.push({
392
- type: "Operator",
393
- value: ">",
394
- start,
395
- end: i + 1
396
- });
397
- i++;
398
- continue;
399
- }
400
- if (ch === "*" && i + 1 < source.length && source[i + 1] === "*") {
401
- tokens.push({
402
- type: "Operator",
403
- value: "**",
404
- start,
405
- end: i + 2
406
- });
407
- i += 2;
408
- continue;
409
- }
410
- if (ch === "?") {
411
- if (i + 1 < source.length && source[i + 1] === ".") {
412
- tokens.push({
413
- type: "OptionalChain",
414
- value: "?.",
415
- start,
416
- end: i + 2
417
- });
418
- i += 2;
419
- continue;
420
- }
421
- if (i + 1 < source.length && source[i + 1] === "?") {
422
- tokens.push({
423
- type: "NullishCoalescing",
424
- value: "??",
425
- start,
426
- end: i + 2
427
- });
428
- i += 2;
429
- continue;
430
- }
431
- tokens.push({
432
- type: "Punctuation",
433
- value: "?",
434
- start,
435
- end: i + 1
436
- });
437
- i++;
438
- continue;
439
- }
440
- if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%") {
441
- tokens.push({
442
- type: "Operator",
443
- value: ch,
444
- start,
445
- end: i + 1
446
- });
447
- i++;
448
- continue;
449
- }
450
- if ("()[]{},:".includes(ch)) {
451
- tokens.push({
452
- type: "Punctuation",
453
- value: ch,
454
- start,
455
- end: i + 1
456
- });
457
- i++;
458
- continue;
459
- }
460
- throw new ExpressionError(`Unexpected character "${ch}"`, {
461
- source,
462
- start,
463
- end: i + 1
464
- });
465
- }
466
- tokens.push({
467
- type: "EOF",
468
- value: "",
469
- start: i,
470
- end: i
471
- });
472
- return tokens;
473
- }
474
- //#endregion
1
+ import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-BONB6P_5.mjs";
2
+ import { i as tokenize, n as ExecutionContext, r as SecurityPolicy } from "./execution-context-B-5hQ92X.mjs";
475
3
  //#region src/parser.ts
476
4
  const PIPE_PRECEDENCE = 5;
477
5
  const PRECEDENCE = {
@@ -731,7 +259,7 @@ function parse(source, _depth = 0) {
731
259
  const tok = current();
732
260
  if (tok.type === "Number") {
733
261
  advance();
734
- const raw = tok.value.replace(/_/g, "");
262
+ const raw = tok.value.replace(/_/gu, "");
735
263
  return {
736
264
  type: "NumberLiteral",
737
265
  value: Number(raw),
@@ -1355,9 +883,16 @@ function resolveTransform(name, transforms) {
1355
883
  if (!Object.hasOwn(transforms, name)) throw new BonsaiReferenceError("transform", name, suggest(name, Object.keys(transforms)));
1356
884
  return transforms[name];
1357
885
  }
1358
- function resolveFunction(name, functions) {
1359
- if (!Object.hasOwn(functions, name)) throw new BonsaiReferenceError("function", name, suggest(name, Object.keys(functions)));
1360
- return functions[name];
886
+ /**
887
+ * Resolve a callable by name in the shared function namespace. Pure and context
888
+ * functions occupy one registry keyed by name, so a single lookup returns the
889
+ * tagged entry; the call site uses `kind` to decide how to invoke it (pure
890
+ * functions take only call args; context functions receive the evaluation
891
+ * context first, then call args).
892
+ */
893
+ function resolveCallable(name, functions) {
894
+ if (Object.hasOwn(functions, name)) return functions[name];
895
+ throw new BonsaiReferenceError("function", name, suggest(name, Object.keys(functions)));
1361
896
  }
1362
897
  function getIdentifierName(node, message = "Expected identifier") {
1363
898
  if (node.type !== "Identifier") throw new Error(message);
@@ -1408,29 +943,34 @@ function rejectPromise(value, kind, name) {
1408
943
  if (value instanceof Promise) throw new BonsaiTypeError(name, `a synchronous ${kind} result — use evaluate() instead of evaluateSync() for async`, value);
1409
944
  return value;
1410
945
  }
1411
- function evaluate(node, context, transforms, functions, guard, source) {
946
+ function evaluate(node, context, bindings, guard, source) {
1412
947
  return evalNode(node, {
1413
948
  ctx: context,
1414
- tr: transforms,
1415
- fn: functions,
949
+ tr: bindings.transforms,
950
+ fn: bindings.functions,
1416
951
  g: guard,
1417
952
  s: source
1418
953
  });
1419
954
  }
1420
955
  function evalNode(node, env) {
1421
- const { ctx, g, s } = env;
956
+ switch (node.type) {
957
+ case "NumberLiteral":
958
+ case "StringLiteral":
959
+ case "BooleanLiteral": return node.value;
960
+ case "NullLiteral": return null;
961
+ case "UndefinedLiteral": return;
962
+ case "Identifier":
963
+ env.g.checkNameAccess(node.name, "identifier");
964
+ return Object.hasOwn(env.ctx, node.name) ? env.ctx[node.name] : void 0;
965
+ }
966
+ return evalCompound(node, env);
967
+ }
968
+ function evalCompound(node, env) {
969
+ const { g, s } = env;
1422
970
  g.enterDepth();
1423
971
  g.step();
1424
972
  try {
1425
973
  switch (node.type) {
1426
- case "NumberLiteral":
1427
- case "StringLiteral":
1428
- case "BooleanLiteral": return node.value;
1429
- case "NullLiteral": return null;
1430
- case "UndefinedLiteral": return;
1431
- case "Identifier":
1432
- g.checkNameAccess(node.name, "identifier");
1433
- return Object.hasOwn(ctx, node.name) ? ctx[node.name] : void 0;
1434
974
  case "UnaryExpression": return applyUnaryOp(node.operator, evalNode(node.operand, env));
1435
975
  case "BinaryExpression": {
1436
976
  const op = node.operator;
@@ -1529,10 +1069,10 @@ function evalCallExpression(node, env) {
1529
1069
  }
1530
1070
  }
1531
1071
  if (node.callee.type === "Identifier") try {
1532
- const func = resolveFunction(node.callee.name, fn);
1072
+ const resolved = resolveCallable(node.callee.name, fn);
1533
1073
  const args = [];
1534
1074
  for (const arg of node.args) pushCallArgument(args, arg, env);
1535
- return rejectPromise(func(...args), "function", node.callee.name);
1075
+ return rejectPromise(resolved.kind === "context" ? resolved.fn(env.ctx, ...args) : resolved.fn(...args), "function", node.callee.name);
1536
1076
  } catch (e) {
1537
1077
  if (s) attachLocation(e, s, node.start, node.end);
1538
1078
  throw e;
@@ -1642,11 +1182,11 @@ function makeLambdaAccessor$1(property, guard) {
1642
1182
  }
1643
1183
  //#endregion
1644
1184
  //#region src/evaluator-async.ts
1645
- async function evaluateAsync(node, context, transforms, functions, guard, source) {
1185
+ async function evaluateAsync(node, context, bindings, guard, source) {
1646
1186
  const result = await evalNodeAsync(node, {
1647
1187
  ctx: context,
1648
- tr: transforms,
1649
- fn: functions,
1188
+ tr: bindings.transforms,
1189
+ fn: bindings.functions,
1650
1190
  g: guard,
1651
1191
  s: source
1652
1192
  });
@@ -1654,19 +1194,24 @@ async function evaluateAsync(node, context, transforms, functions, guard, source
1654
1194
  return result;
1655
1195
  }
1656
1196
  async function evalNodeAsync(node, env) {
1657
- const { ctx, g, s } = env;
1197
+ switch (node.type) {
1198
+ case "NumberLiteral":
1199
+ case "StringLiteral":
1200
+ case "BooleanLiteral": return node.value;
1201
+ case "NullLiteral": return null;
1202
+ case "UndefinedLiteral": return;
1203
+ case "Identifier":
1204
+ env.g.checkNameAccess(node.name, "identifier");
1205
+ return Object.hasOwn(env.ctx, node.name) ? env.ctx[node.name] : void 0;
1206
+ }
1207
+ return evalCompoundAsync(node, env);
1208
+ }
1209
+ async function evalCompoundAsync(node, env) {
1210
+ const { g, s } = env;
1658
1211
  g.enterDepth();
1659
1212
  g.step();
1660
1213
  try {
1661
1214
  switch (node.type) {
1662
- case "NumberLiteral":
1663
- case "StringLiteral":
1664
- case "BooleanLiteral": return node.value;
1665
- case "NullLiteral": return null;
1666
- case "UndefinedLiteral": return;
1667
- case "Identifier":
1668
- g.checkNameAccess(node.name, "identifier");
1669
- return Object.hasOwn(ctx, node.name) ? ctx[node.name] : void 0;
1670
1215
  case "UnaryExpression": return applyUnaryOp(node.operator, await evalNodeAsync(node.operand, env));
1671
1216
  case "BinaryExpression": {
1672
1217
  const op = node.operator;
@@ -1776,10 +1321,10 @@ async function evalCallExpressionAsync(node, env) {
1776
1321
  }
1777
1322
  }
1778
1323
  if (node.callee.type === "Identifier") try {
1779
- const func = resolveFunction(node.callee.name, fn);
1324
+ const resolved = resolveCallable(node.callee.name, fn);
1780
1325
  const args = [];
1781
1326
  for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
1782
- const result = await func(...args);
1327
+ const result = resolved.kind === "context" ? await resolved.fn(env.ctx, ...args) : await resolved.fn(...args);
1783
1328
  g.checkTimeout();
1784
1329
  return result;
1785
1330
  } catch (e) {
@@ -1937,99 +1482,23 @@ function makeLambdaAccessor(property, guard) {
1937
1482
  };
1938
1483
  }
1939
1484
  //#endregion
1940
- //#region src/execution-context.ts
1941
- const BLOCKED_PROPERTIES = new Set([
1942
- "__proto__",
1943
- "constructor",
1944
- "prototype"
1945
- ]);
1946
- const MAX_INDEX_DIGITS = 10;
1947
- function isCanonicalIndex(key) {
1948
- if (key.length === 0 || key.length > MAX_INDEX_DIGITS) return false;
1949
- const n = Number(key);
1950
- return Number.isInteger(n) && n >= 0 && String(n) === key;
1951
- }
1952
- const TIMEOUT_CHECK_INTERVAL = 1e3;
1953
- const DEFAULT_MAX_DEPTH = 100;
1954
- const DEFAULT_MAX_ARRAY_LENGTH = 1e5;
1955
- /** Immutable per-instance security configuration derived from BonsaiOptions. */
1956
- var SecurityPolicy = class {
1957
- maxDepth;
1958
- maxArrayLength;
1959
- timeout;
1960
- allowedProperties;
1961
- deniedProperties;
1962
- constructor(options = {}) {
1963
- this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
1964
- this.maxArrayLength = options.maxArrayLength ?? DEFAULT_MAX_ARRAY_LENGTH;
1965
- this.timeout = options.timeout ?? 0;
1966
- this.allowedProperties = options.allowedProperties ? new Set(options.allowedProperties) : void 0;
1967
- this.deniedProperties = options.deniedProperties ? new Set(options.deniedProperties) : void 0;
1968
- }
1969
- };
1970
- /** Mutable per-evaluation state: tracks depth, step count, and deadline for a single evaluation. */
1971
- var ExecutionContext = class {
1972
- stepCount = 0;
1973
- depth = 0;
1974
- deadline;
1975
- policy;
1976
- now;
1977
- constructor(policy, now = Date.now) {
1978
- this.policy = policy;
1979
- this.now = now;
1980
- this.deadline = policy.timeout ? now() + policy.timeout : 0;
1981
- }
1982
- step() {
1983
- if (this.deadline) {
1984
- this.stepCount++;
1985
- if (this.stepCount % TIMEOUT_CHECK_INTERVAL === 0) this.checkTimeout();
1986
- }
1987
- }
1988
- checkTimeout() {
1989
- if (this.deadline && this.now() >= this.deadline) throw new BonsaiSecurityError("TIMEOUT", `Expression timeout: exceeded ${this.policy.timeout}ms`);
1990
- }
1991
- enterDepth() {
1992
- if (++this.depth > this.policy.maxDepth) throw new BonsaiSecurityError("MAX_DEPTH", `Maximum expression depth (${this.policy.maxDepth}) exceeded`);
1993
- }
1994
- exitDepth() {
1995
- this.depth--;
1996
- }
1997
- withDepth(fn) {
1998
- this.enterDepth();
1999
- try {
2000
- return fn();
2001
- } finally {
2002
- this.exitDepth();
2003
- }
2004
- }
2005
- checkNameAccess(key, kind) {
2006
- if (BLOCKED_PROPERTIES.has(key)) throw new BonsaiSecurityError("BLOCKED_PROPERTY", `Blocked: access to "${key}" is not allowed`);
2007
- if (kind === "identifier" || kind === "object-key") return;
2008
- if (isCanonicalIndex(key)) return;
2009
- if (this.policy.allowedProperties) {
2010
- if (!this.policy.allowedProperties.has(key)) throw new BonsaiSecurityError("PROPERTY_NOT_ALLOWED", `Blocked: "${key}" is not in allowed properties`);
2011
- }
2012
- if (this.policy.deniedProperties) {
2013
- if (this.policy.deniedProperties.has(key)) throw new BonsaiSecurityError("PROPERTY_DENIED", `Blocked: "${key}" is in denied properties`);
2014
- }
2015
- }
2016
- checkArrayLength(length) {
2017
- if (length > this.policy.maxArrayLength) throw new BonsaiSecurityError("MAX_ARRAY_LENGTH", `Array length (${length}) exceeds maximum (${this.policy.maxArrayLength})`);
2018
- }
2019
- };
2020
- //#endregion
2021
1485
  //#region src/cache.ts
2022
1486
  var LRUCache = class {
2023
1487
  max;
2024
1488
  map = /* @__PURE__ */ new Map();
1489
+ lastKey;
1490
+ lastValue;
2025
1491
  constructor(max) {
2026
1492
  this.max = max > 0 ? max : 0;
2027
1493
  }
2028
1494
  get(key) {
1495
+ if (key === this.lastKey) return this.lastValue;
2029
1496
  if (!this.map.has(key)) return void 0;
2030
1497
  const value = this.map.get(key);
2031
1498
  this.map.delete(key);
2032
1499
  this.map.set(key, value);
1500
+ this.lastKey = key;
1501
+ this.lastValue = value;
2033
1502
  return value;
2034
1503
  }
2035
1504
  set(key, value) {
@@ -2038,14 +1507,22 @@ var LRUCache = class {
2038
1507
  else if (this.map.size >= this.max) {
2039
1508
  const oldest = this.map.keys().next().value;
2040
1509
  this.map.delete(oldest);
1510
+ if (this.lastKey === oldest) {
1511
+ this.lastKey = void 0;
1512
+ this.lastValue = void 0;
1513
+ }
2041
1514
  }
2042
1515
  this.map.set(key, value);
1516
+ this.lastKey = key;
1517
+ this.lastValue = value;
2043
1518
  }
2044
1519
  get size() {
2045
1520
  return this.map.size;
2046
1521
  }
2047
1522
  clear() {
2048
1523
  this.map.clear();
1524
+ this.lastKey = void 0;
1525
+ this.lastValue = void 0;
2049
1526
  }
2050
1527
  };
2051
1528
  //#endregion
@@ -2057,13 +1534,24 @@ function createPluginRegistry() {
2057
1534
  let functionsCache = {};
2058
1535
  let transformsDirty = false;
2059
1536
  let functionsDirty = false;
1537
+ let bindingsCache;
2060
1538
  const registry = {
2061
1539
  addTransform(name, fn) {
2062
1540
  transformMap.set(name, fn);
2063
1541
  transformsDirty = true;
2064
1542
  },
2065
1543
  addFunction(name, fn) {
2066
- functionMap.set(name, fn);
1544
+ functionMap.set(name, {
1545
+ kind: "pure",
1546
+ fn
1547
+ });
1548
+ functionsDirty = true;
1549
+ },
1550
+ addContextFunction(name, fn) {
1551
+ functionMap.set(name, {
1552
+ kind: "context",
1553
+ fn
1554
+ });
2067
1555
  functionsDirty = true;
2068
1556
  },
2069
1557
  removeTransform(name) {
@@ -2082,6 +1570,12 @@ function createPluginRegistry() {
2082
1570
  getFunction(name) {
2083
1571
  return functionMap.get(name);
2084
1572
  },
1573
+ isContextFunction(name) {
1574
+ return functionMap.get(name)?.kind === "context";
1575
+ },
1576
+ hasFunction(name) {
1577
+ return functionMap.has(name);
1578
+ },
2085
1579
  getTransformNames() {
2086
1580
  return [...transformMap.keys()];
2087
1581
  },
@@ -2104,6 +1598,15 @@ function createPluginRegistry() {
2104
1598
  functionsDirty = false;
2105
1599
  }
2106
1600
  return functionsCache;
1601
+ },
1602
+ get bindings() {
1603
+ const t = registry.transforms;
1604
+ const f = registry.functions;
1605
+ if (!bindingsCache || bindingsCache.transforms !== t || bindingsCache.functions !== f) bindingsCache = {
1606
+ transforms: t,
1607
+ functions: f
1608
+ };
1609
+ return bindingsCache;
2107
1610
  }
2108
1611
  };
2109
1612
  return registry;
@@ -2111,24 +1614,36 @@ function createPluginRegistry() {
2111
1614
  //#endregion
2112
1615
  //#region src/index.ts
2113
1616
  const DEFAULT_CACHE_SIZE = 256;
2114
- let _shared;
1617
+ let sharedInstance;
2115
1618
  /**
2116
1619
  * Evaluate a single expression with default options. Uses a shared instance internally.
2117
1620
  * For repeated evaluation or custom configuration, use `bonsai()` to create a dedicated instance.
2118
1621
  */
2119
1622
  function evaluateExpression(expression, context) {
2120
- if (!_shared) _shared = bonsai();
2121
- return _shared.evaluateSync(expression, context);
1623
+ if (!sharedInstance) sharedInstance = bonsai();
1624
+ return sharedInstance.evaluateSync(expression, context);
2122
1625
  }
2123
1626
  /**
2124
1627
  * Create a new Bonsai instance with optional safety and caching configuration.
2125
- * Register transforms and functions via `.use()`, `.addTransform()`, and `.addFunction()`.
1628
+ * Register transforms and functions via `.use()`, `.addTransform()`, `.addFunction()`,
1629
+ * and `.addContextFunction()`.
1630
+ *
1631
+ * Pass a context type argument to get end-to-end type safety on the evaluation
1632
+ * context: `bonsai<MyContext>()` will type-check `evaluate(expr, ctx)` calls,
1633
+ * require a context argument when `MyContext` has required fields, and
1634
+ * propagate `MyContext` into context-aware function signatures.
2126
1635
  *
2127
1636
  * @example
2128
1637
  * ```ts
2129
1638
  * const expr = bonsai({ timeout: 50 })
2130
1639
  * expr.use(strings)
2131
1640
  * expr.evaluateSync('name |> trim |> upper', { name: ' hello ' }) // "HELLO"
1641
+ *
1642
+ * type AppCtx = { userId: string; perms: string[] }
1643
+ * const app = bonsai<AppCtx>()
1644
+ * app.addContextFunction('hasPermission', (ctx, action) =>
1645
+ * ctx.perms.includes(String(action)))
1646
+ * app.evaluateSync('hasPermission("write")', { userId: 'u_1', perms: ['write'] })
2132
1647
  * ```
2133
1648
  */
2134
1649
  function bonsai(options = {}) {
@@ -2136,6 +1651,8 @@ function bonsai(options = {}) {
2136
1651
  const cache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
2137
1652
  const astCache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
2138
1653
  const policy = new SecurityPolicy(options);
1654
+ const syncCtx = new ExecutionContext(policy);
1655
+ let syncCtxInUse = false;
2139
1656
  function createExecutionContext() {
2140
1657
  return new ExecutionContext(policy);
2141
1658
  }
@@ -2153,11 +1670,19 @@ function bonsai(options = {}) {
2153
1670
  const compiled = {
2154
1671
  ast: optimized,
2155
1672
  source,
2156
- async evaluate(context = {}) {
2157
- return evaluateAsync(optimized, context, registry.transforms, registry.functions, createExecutionContext(), source);
1673
+ async evaluate(...args) {
1674
+ return evaluateAsync(optimized, args[0] ?? {}, registry.bindings, createExecutionContext(), source);
2158
1675
  },
2159
- evaluateSync(context = {}) {
2160
- return evaluate(optimized, context, registry.transforms, registry.functions, createExecutionContext(), source);
1676
+ evaluateSync(...args) {
1677
+ const ctx = args[0] ?? {};
1678
+ if (syncCtxInUse) return evaluate(optimized, ctx, registry.bindings, createExecutionContext(), source);
1679
+ syncCtxInUse = true;
1680
+ try {
1681
+ syncCtx.reset();
1682
+ return evaluate(optimized, ctx, registry.bindings, syncCtx, source);
1683
+ } finally {
1684
+ syncCtxInUse = false;
1685
+ }
2161
1686
  }
2162
1687
  };
2163
1688
  cache.set(source, compiled);
@@ -2176,6 +1701,10 @@ function bonsai(options = {}) {
2176
1701
  registry.addFunction(name, fn);
2177
1702
  return instance;
2178
1703
  },
1704
+ addContextFunction(name, fn) {
1705
+ registry.addContextFunction(name, fn);
1706
+ return instance;
1707
+ },
2179
1708
  removeTransform(name) {
2180
1709
  return registry.removeTransform(name);
2181
1710
  },
@@ -2186,7 +1715,10 @@ function bonsai(options = {}) {
2186
1715
  return registry.getTransform(name) !== void 0;
2187
1716
  },
2188
1717
  hasFunction(name) {
2189
- return registry.getFunction(name) !== void 0;
1718
+ return registry.hasFunction(name);
1719
+ },
1720
+ isContextFunction(name) {
1721
+ return registry.isContextFunction(name);
2190
1722
  },
2191
1723
  listTransforms() {
2192
1724
  return registry.getTransformNames();
@@ -2194,6 +1726,12 @@ function bonsai(options = {}) {
2194
1726
  listFunctions() {
2195
1727
  return registry.getFunctionNames();
2196
1728
  },
1729
+ getPolicy() {
1730
+ return {
1731
+ ...policy.allowedProperties ? { allowedProperties: [...policy.allowedProperties] } : {},
1732
+ ...policy.deniedProperties ? { deniedProperties: [...policy.deniedProperties] } : {}
1733
+ };
1734
+ },
2197
1735
  clearCache() {
2198
1736
  cache.clear();
2199
1737
  astCache.clear();
@@ -2201,11 +1739,20 @@ function bonsai(options = {}) {
2201
1739
  compile(expression) {
2202
1740
  return compileExpr(expression);
2203
1741
  },
2204
- async evaluate(expression, context = {}) {
2205
- return evaluateAsync(getAst(expression), context, registry.transforms, registry.functions, createExecutionContext(), expression);
1742
+ async evaluate(expression, ...args) {
1743
+ return evaluateAsync(getAst(expression), args[0] ?? {}, registry.bindings, createExecutionContext(), expression);
2206
1744
  },
2207
- evaluateSync(expression, context = {}) {
2208
- return evaluate(getAst(expression), context, registry.transforms, registry.functions, createExecutionContext(), expression);
1745
+ evaluateSync(expression, ...args) {
1746
+ const ast = getAst(expression);
1747
+ const ctx = args[0] ?? {};
1748
+ if (syncCtxInUse) return evaluate(ast, ctx, registry.bindings, createExecutionContext(), expression);
1749
+ syncCtxInUse = true;
1750
+ try {
1751
+ syncCtx.reset();
1752
+ return evaluate(ast, ctx, registry.bindings, syncCtx, expression);
1753
+ } finally {
1754
+ syncCtxInUse = false;
1755
+ }
2209
1756
  },
2210
1757
  validate(expression) {
2211
1758
  try {