bonsai-js 1.0.0 → 1.2.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 (60) hide show
  1. package/CHANGELOG.md +20 -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/builtins/index.d.mts +91 -0
  7. package/dist/builtins/index.mjs +93 -0
  8. package/dist/builtins/index.mjs.map +1 -0
  9. package/dist/check/bind.mjs +117 -0
  10. package/dist/check/bind.mjs.map +1 -0
  11. package/dist/check/checker.mjs +34 -120
  12. package/dist/check/checker.mjs.map +1 -1
  13. package/dist/compile/compiler.mjs +2 -3
  14. package/dist/compile/compiler.mjs.map +1 -1
  15. package/dist/declared.mjs +8 -1
  16. package/dist/declared.mjs.map +1 -1
  17. package/dist/digest.mjs +109 -0
  18. package/dist/digest.mjs.map +1 -0
  19. package/dist/environment.d.mts +14 -1
  20. package/dist/environment.mjs +65 -315
  21. package/dist/environment.mjs.map +1 -1
  22. package/dist/errors.d.mts +21 -2
  23. package/dist/errors.mjs +17 -1
  24. package/dist/errors.mjs.map +1 -1
  25. package/dist/features.mjs +3 -3
  26. package/dist/features.mjs.map +1 -1
  27. package/dist/functions/builtins.mjs +551 -474
  28. package/dist/functions/builtins.mjs.map +1 -1
  29. package/dist/functions/define.mjs +53 -11
  30. package/dist/functions/define.mjs.map +1 -1
  31. package/dist/functions/descriptions.mjs +110 -0
  32. package/dist/functions/descriptions.mjs.map +1 -0
  33. package/dist/index.d.mts +4 -3
  34. package/dist/index.mjs +3 -2
  35. package/dist/lite/index.d.mts +4 -3
  36. package/dist/lite/index.mjs +3 -2
  37. package/dist/lite/index.mjs.map +1 -1
  38. package/dist/load.mjs +307 -0
  39. package/dist/load.mjs.map +1 -0
  40. package/dist/program.mjs +321 -0
  41. package/dist/program.mjs.map +1 -0
  42. package/dist/query/index.mjs +1 -1
  43. package/dist/runtime/core.d.mts +18 -0
  44. package/dist/runtime/core.mjs +31 -0
  45. package/dist/runtime/core.mjs.map +1 -0
  46. package/dist/runtime/create.d.mts +61 -0
  47. package/dist/runtime/create.mjs +76 -0
  48. package/dist/runtime/create.mjs.map +1 -0
  49. package/dist/runtime/index.d.mts +24 -0
  50. package/dist/runtime/index.mjs +34 -0
  51. package/dist/runtime/index.mjs.map +1 -0
  52. package/dist/service/index.mjs +12 -8
  53. package/dist/service/index.mjs.map +1 -1
  54. package/dist/syntax/parser.mjs +1 -1
  55. package/dist/syntax/printer.mjs +1 -1
  56. package/dist/types.mjs +103 -23
  57. package/dist/types.mjs.map +1 -1
  58. package/dist/version.mjs +7 -0
  59. package/dist/version.mjs.map +1 -0
  60. package/package.json +26 -2
@@ -1,17 +1,15 @@
1
1
  import { BonsaiCheckError, BonsaiError, BonsaiLimitError, BonsaiRuntimeError, locate } from "./errors.mjs";
2
2
  import { isName } from "./syntax/lexer.mjs";
3
- import { errorText, isMap } from "./runtime/values.mjs";
4
- import { warmZone } from "./runtime/time.mjs";
5
- import { formatType, isNullable } from "./types.mjs";
6
- import { assertHostSpec, assertType, conforms, describeMismatch, isRecord, overload } from "./functions/define.mjs";
7
- import { BUILTINS, zoneParameter } from "./functions/builtins.mjs";
8
- import { isStackOverflow, tooDeep } from "./runtime/overflow.mjs";
3
+ import { isExact, isNullable, typeData } from "./types.mjs";
4
+ import { assertHostSpec, assertType, builtinsOption, isRecord, overload } from "./functions/define.mjs";
5
+ import { BUILTINS } from "./functions/builtins.mjs";
9
6
  import { DEFAULT_PARSE_LIMITS, parse } from "./syntax/parser.mjs";
10
7
  import { analyze } from "./check/checker.mjs";
11
- import { escaping, isOwn, raised } from "./runtime/attribution.mjs";
12
- import { compileProgram } from "./compile/compiler.mjs";
13
- import { DEFAULT_RUNTIME_LIMITS, MAX_VALUE_DEPTH_LIMIT, State } from "./runtime/state.mjs";
14
- import { recordDeclared } from "./declared.mjs";
8
+ import { raised } from "./runtime/attribution.mjs";
9
+ import { descriptionOf } from "./functions/descriptions.mjs";
10
+ import { DEFAULT_RUNTIME_LIMITS, MAX_VALUE_DEPTH_LIMIT } from "./runtime/state.mjs";
11
+ import { recordCompiled, recordDeclared } from "./declared.mjs";
12
+ import { assertKeys, numberOption, programRunner, timeoutOption, warmZones } from "./program.mjs";
15
13
  //#region src/environment.ts
16
14
  /**
17
15
  * Declares a host function.
@@ -107,24 +105,6 @@ function mergeVariables(base, options) {
107
105
  for (const [k, v] of Object.entries(options.variables ?? {})) add(k, v, "the variables option");
108
106
  return out === void 0 ? void 0 : Object.freeze(out);
109
107
  }
110
- /** A number option: TypeError when it is not a number, RangeError when out of range. */
111
- function numberOption(name, value, fallback, minimum) {
112
- if (value === void 0) return fallback;
113
- if (typeof value !== "number") throw new TypeError(`${name} must be a number`);
114
- if (!Number.isInteger(value) || value < minimum) throw new RangeError(minimum === 0 ? `${name} must be a non-negative integer (0 disables it)` : `${name} must be a positive integer`);
115
- return value;
116
- }
117
- /**
118
- * A timeout in milliseconds: 0 turns it off; any other non-negative number,
119
- * fractions included, is a limit, so a budget computed down to its last
120
- * fraction of a millisecond stays a limit rather than becoming 0.
121
- */
122
- function timeoutOption(name, value, fallback) {
123
- if (value === void 0) return fallback;
124
- if (typeof value !== "number") throw new TypeError(`${name} must be a number`);
125
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`${name} must be a non-negative number of milliseconds (0 disables it)`);
126
- return value;
127
- }
128
108
  const OPTION_KEYS = /* @__PURE__ */ new Set([
129
109
  "variables",
130
110
  "strict",
@@ -133,7 +113,8 @@ const OPTION_KEYS = /* @__PURE__ */ new Set([
133
113
  "limits",
134
114
  "cacheSize",
135
115
  "clock",
136
- "validateContext"
116
+ "validateContext",
117
+ "builtins"
137
118
  ]);
138
119
  const LIMIT_KEYS = /* @__PURE__ */ new Set([
139
120
  ...Object.keys(DEFAULT_PARSE_LIMITS),
@@ -145,10 +126,6 @@ const LIBRARY_KEYS = /* @__PURE__ */ new Set([
145
126
  "functions",
146
127
  "variables"
147
128
  ]);
148
- function assertKeys(value, allowed, what) {
149
- if (!isRecord(value)) throw new TypeError(`${what} must be an object`);
150
- for (const key of Object.keys(value)) if (!allowed.has(key)) throw new TypeError(`Unknown ${what.toLowerCase()} key "${key}" (expected one of: ${[...allowed].join(", ")})`);
151
- }
152
129
  /** Environment options come from configuration: mistakes there are programming errors. */
153
130
  function assertOptions(options) {
154
131
  assertKeys(options, OPTION_KEYS, "Options");
@@ -190,7 +167,11 @@ const DEPTH_CAPS = {
190
167
  maxDepth: 256,
191
168
  maxValueDepth: MAX_VALUE_DEPTH_LIMIT
192
169
  };
193
- function settingsFrom(base, options) {
170
+ /**
171
+ * Settings from options, over a base (extend) or, for a new environment or
172
+ * runtime, over the entry point's built-ins.
173
+ */
174
+ function settingsFrom(base, options, builtins = /* @__PURE__ */ new Map()) {
194
175
  assertOptions(options);
195
176
  const limits = options.limits ?? {};
196
177
  const variables = mergeVariables(base?.variables, options);
@@ -200,6 +181,7 @@ function settingsFrom(base, options) {
200
181
  strictOption,
201
182
  strict: strictOption ?? variables !== void 0,
202
183
  host: mergeFunctions(base?.host ?? /* @__PURE__ */ new Map(), options),
184
+ builtins: builtinsOption(options.builtins, base?.builtins ?? (options.builtins === void 0 ? builtins : /* @__PURE__ */ new Map())),
203
185
  parseLimits: pickLimits(DEFAULT_PARSE_LIMITS, base?.parseLimits ?? DEFAULT_PARSE_LIMITS, limits),
204
186
  runtimeLimits: pickLimits(DEFAULT_RUNTIME_LIMITS, base?.runtimeLimits ?? DEFAULT_RUNTIME_LIMITS, limits),
205
187
  timeout: timeoutOption("Limit \"timeout\"", limits.timeout, base?.timeout ?? 0),
@@ -253,7 +235,6 @@ var ProgramCache = class {
253
235
  }
254
236
  }
255
237
  };
256
- const EMPTY_CONTEXT = Object.freeze({});
257
238
  const NO_DIAGNOSTICS = Object.freeze([]);
258
239
  /**
259
240
  * What check() found. `ok`, `type`, and `diagnostics` are the JSON form; the
@@ -292,66 +273,6 @@ var CheckSuccess = class extends CheckFailure {
292
273
  return this.#program();
293
274
  }
294
275
  };
295
- const EVALUATE_OPTION_KEYS = /* @__PURE__ */ new Set([
296
- "timeout",
297
- "maxSteps",
298
- "signal",
299
- "now",
300
- "validateContext"
301
- ]);
302
- /** Whether a value is a valid Date (an object that only inherits from Date.prototype is not). */
303
- function isValidDate(value) {
304
- try {
305
- return value instanceof Date && !Number.isNaN(Date.prototype.getTime.call(value));
306
- } catch {
307
- return false;
308
- }
309
- }
310
- /** Whether a value has an AbortSignal's members; a signal whose getters throw does not. */
311
- function looksLikeSignal(signal) {
312
- try {
313
- const s = signal;
314
- return typeof s.aborted === "boolean" && typeof s.addEventListener === "function" && typeof s.removeEventListener === "function";
315
- } catch {
316
- return false;
317
- }
318
- }
319
- /** Validates per-evaluation options the way `limits` are validated; nothing fails open. */
320
- function evaluationLimits(options, settings) {
321
- if (options === void 0) return {
322
- maxSteps: settings.runtimeLimits.maxSteps,
323
- timeout: settings.timeout,
324
- signal: void 0,
325
- now: void 0,
326
- validateContext: void 0
327
- };
328
- let o;
329
- try {
330
- assertKeys(options, EVALUATE_OPTION_KEYS, "Evaluate option");
331
- const read = options;
332
- o = {
333
- maxSteps: read.maxSteps,
334
- timeout: read.timeout,
335
- signal: read.signal,
336
- now: read.now,
337
- validateContext: read.validateContext
338
- };
339
- } catch (error) {
340
- if (error instanceof TypeError || error instanceof RangeError) throw error;
341
- throw new TypeError(`Evaluate options could not be read: ${errorText(error)}`, { cause: error });
342
- }
343
- const signal = o.signal;
344
- if (signal !== void 0 && (typeof signal !== "object" || signal === null || !looksLikeSignal(signal))) throw new TypeError("signal must be an AbortSignal");
345
- if (o.now !== void 0 && !isValidDate(o.now)) throw new TypeError("now must be a valid Date");
346
- if (o.validateContext !== void 0 && typeof o.validateContext !== "boolean") throw new TypeError("validateContext must be a boolean");
347
- return {
348
- maxSteps: numberOption("maxSteps", o.maxSteps, settings.runtimeLimits.maxSteps, 0),
349
- timeout: timeoutOption("timeout", o.timeout, settings.timeout),
350
- signal,
351
- now: o.now,
352
- validateContext: o.validateContext
353
- };
354
- }
355
276
  /** Freezes a syntax tree (or any tree of plain objects and arrays) in place, without recursion. */
356
277
  function deepFreeze(root) {
357
278
  const seen = /* @__PURE__ */ new Set();
@@ -365,87 +286,6 @@ function deepFreeze(root) {
365
286
  }
366
287
  return root;
367
288
  }
368
- function validationBudget(source, limits) {
369
- return {
370
- maxDepth: limits.maxDepth,
371
- remaining: limits.maxSteps > 0 ? limits.maxSteps : Number.POSITIVE_INFINITY,
372
- deadline: limits.timeout > 0 ? performance.now() + limits.timeout : 0,
373
- onExhausted: (reason) => {
374
- if (reason === "depth") throw raised(new BonsaiLimitError("VALUE_DEPTH_LIMIT", `Context nests deeper than ${limits.maxDepth}`, { source }));
375
- if (reason === "time") throw raised(new BonsaiLimitError("TIMEOUT", "Context validation timed out", { source }));
376
- throw raised(new BonsaiLimitError("STEP_LIMIT", "Context validation exceeded the step limit", { source }));
377
- }
378
- };
379
- }
380
- function invalidContext(problem, source) {
381
- if (problem !== void 0) throw raised(new BonsaiRuntimeError("INVALID_CONTEXT", `Invalid context: ${problem}`, { source }));
382
- }
383
- function validateContext(ctx, variables, source, limits) {
384
- const budget = validationBudget(source, limits);
385
- for (const [name, type] of Object.entries(variables)) {
386
- const value = Object.hasOwn(ctx, name) ? ctx[name] : void 0;
387
- invalidContext(describeMismatch(value, type, name, budget), source);
388
- }
389
- }
390
- /**
391
- * Why the value read along `segments` (from index `at`) does not conform, or
392
- * undefined. Each step is judged by its own declared type: an absent or null
393
- * value is fine only where that type is nullable; a declared field is followed
394
- * (a missing required one fails); an undeclared key of an object (objects are
395
- * open) or an absent record entry ends the check; past a type that is not a
396
- * map, and at the end of the path, the value is checked whole, as a context is.
397
- * Fields the path does not name are not checked.
398
- */
399
- function describeAlong(value, type, segments, at, path, budget) {
400
- if (at === segments.length) return describeMismatch(value, type, path, budget);
401
- if (--budget.remaining < 0) budget.onExhausted("steps");
402
- if (type.kind === "union") {
403
- let first;
404
- for (const member of type.types) {
405
- const problem = describeAlong(value, member, segments, at, path, budget);
406
- if (problem === void 0) return void 0;
407
- first ??= problem;
408
- }
409
- return first;
410
- }
411
- const actual = value === void 0 ? null : value;
412
- if (type.kind !== "map" || !isMap(actual)) return describeMismatch(value, type, path, budget);
413
- const key = segments[at];
414
- const field = Object.hasOwn(actual, key) ? actual[key] : void 0;
415
- if (Object.hasOwn(type.fields, key)) return describeAlong(field, type.fields[key], segments, at + 1, `${path}.${key}`, budget);
416
- if (type.rest === void 0 || field === void 0) return void 0;
417
- return describeAlong(field, type.rest, segments, at + 1, `${path}.${key}`, budget);
418
- }
419
- /**
420
- * Validates the values a residual reads: each path in `dependsOn`, step by
421
- * step against the types declared along it (see describeAlong), so a context
422
- * holding exactly those paths passes and invalid data on them fails as it
423
- * would for the program.
424
- */
425
- function validatePaths(ctx, variables, paths, source, limits) {
426
- const budget = validationBudget(source, limits);
427
- for (const path of paths) {
428
- const segments = path.split(".");
429
- const name = segments[0];
430
- if (!Object.hasOwn(variables, name)) continue;
431
- invalidContext(describeAlong(Object.hasOwn(ctx, name) ? ctx[name] : void 0, variables[name], segments, 1, name, budget), source);
432
- }
433
- }
434
- /**
435
- * Reading host data (a getter, a Proxy trap, a `then` hook) can throw. Such a
436
- * failure is a HOST_ERROR, never a raw JavaScript error. Only the errors this
437
- * evaluation raised pass through: any other Bonsai error (from another
438
- * evaluation, from parsing, checking, or translating, or built by the host)
439
- * that host data threw is the host data's failure too.
440
- */
441
- function hostDataFailure(error, source) {
442
- if (isOwn(error)) return error;
443
- if (isStackOverflow(error)) return raised(tooDeep(source, error));
444
- return raised(new BonsaiRuntimeError("HOST_ERROR", `Reading host data failed: ${errorText(error)}`, {
445
- source,
446
- cause: error
447
- }));
448
- }
449
289
  const systemClock = () => /* @__PURE__ */ new Date();
450
290
  /** A host clock whose results are checked: now() must be a valid timestamp. */
451
291
  function checkedClock(clock) {
@@ -460,62 +300,50 @@ function checkedClock(clock) {
460
300
  return value;
461
301
  };
462
302
  }
463
- /** The most zones one program warms when it is compiled. */
464
- const WARMED_ZONES = 16;
465
- /**
466
- * Loads the time zone data a program's calls need, so its first evaluation
467
- * does not pay for it inside its deadline. Only built-in functions that take a
468
- * zone count: a zone written as text is loaded itself, and any other zone
469
- * argument loads the data every zone shares. Each zone is warmed once, and at
470
- * most WARMED_ZONES of them, so a source full of zone names compiles quickly.
471
- */
472
- function warmZones(analysis) {
303
+ /** Whether a type has an exact map type anywhere in it. */
304
+ function holdsExact(type) {
305
+ const stack = [type];
473
306
  const seen = /* @__PURE__ */ new Set();
474
- for (const [call, plan] of analysis.calls) {
475
- const index = zoneParameter(plan.def);
476
- const arg = index === void 0 ? void 0 : call.args[index];
477
- if (arg === void 0) continue;
478
- const zone = arg.type === "Literal" ? arg.value : void 0;
479
- if (zone !== void 0 && typeof zone !== "string" || seen.has(zone)) continue;
480
- seen.add(zone);
481
- warmZone(zone);
482
- if (seen.size >= WARMED_ZONES) return;
307
+ while (stack.length > 0) {
308
+ const next = stack.pop();
309
+ if (seen.has(next)) continue;
310
+ seen.add(next);
311
+ if (isExact(next)) return true;
312
+ if (next.kind === "list") stack.push(next.element);
313
+ else if (next.kind === "union") stack.push(...next.types);
314
+ else if (next.kind === "map") {
315
+ stack.push(...Object.values(next.fields));
316
+ if (next.rest !== void 0) stack.push(next.rest);
317
+ }
483
318
  }
319
+ return false;
484
320
  }
485
- /** Reads compile() and check() options: only `expect`, which must be a type built with t. */
486
- function compileExpect(options) {
321
+ /** Reads compile(), check(), and the runtime's load() options: only `expect`, which must be a type built with t. Returns a frozen copy of it. */
322
+ function expectOption(options, what) {
487
323
  if (options === void 0) return void 0;
488
- if (typeof options !== "object" || options === null || Array.isArray(options)) throw new TypeError("Compile options must be an object");
324
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw new TypeError(`${what} options must be an object`);
489
325
  let expect;
490
326
  try {
491
- for (const key of Object.keys(options)) if (key !== "expect") throw new TypeError(`Unknown compile option key "${key}" (expected one of: expect)`);
327
+ for (const key of Object.keys(options)) if (key !== "expect") throw new TypeError(`Unknown ${what.toLowerCase()} option key "${key}" (expected one of: expect)`);
492
328
  expect = options.expect;
493
329
  } catch (error) {
494
330
  if (error instanceof TypeError) throw error;
495
- throw new TypeError("Compile options could not be read", { cause: error });
331
+ throw new TypeError(`${what} options could not be read`, { cause: error });
496
332
  }
497
- if (expect !== void 0) assertType(expect, "Compile option \"expect\"");
498
- return expect;
499
- }
500
- function contextOf(value) {
501
- if (value === void 0 || value === null) return EMPTY_CONTEXT;
502
- let map;
503
- try {
504
- map = typeof value === "object" && isMap(value);
505
- } catch (error) {
506
- throw new BonsaiRuntimeError("HOST_ERROR", "Reading the evaluation context failed", { cause: error });
507
- }
508
- if (!map) throw new TypeError("The evaluation context must be a plain object or class instance");
509
- return value;
333
+ if (expect === void 0) return void 0;
334
+ assertType(expect, `${what} option "expect"`);
335
+ const own = typeData(expect);
336
+ if (own !== expect) assertType(own, `${what} option "expect"`);
337
+ return own;
510
338
  }
511
339
  function createEnvironment(settings) {
512
340
  const checkEnv = {
513
341
  variables: settings.variables,
514
342
  strict: settings.strict,
515
- lookup: (name) => settings.host.get(name) ?? BUILTINS.get(name),
343
+ lookup: (name) => settings.host.get(name) ?? settings.builtins.get(name),
516
344
  *functionNames() {
517
345
  yield* settings.host.keys();
518
- for (const name of BUILTINS.keys()) if (!settings.host.has(name)) yield name;
346
+ for (const name of settings.builtins.keys()) if (!settings.host.has(name)) yield name;
519
347
  }
520
348
  };
521
349
  const cache = new ProgramCache(settings.cacheSize);
@@ -539,101 +367,14 @@ function createEnvironment(settings) {
539
367
  function makeProgram(source, analysis, expect, constants, residualOf) {
540
368
  if (residualOf === void 0) warmZones(analysis);
541
369
  const guard = expect !== void 0 && analysis.checkResult ? expect : void 0;
542
- const checked = (result, state) => {
543
- if (guard !== void 0 && !conforms(result, guard, state)) throw raised(new BonsaiRuntimeError("TYPE_ERROR", `The result does not match the expected type ${formatType(guard)}`, { source }));
544
- };
545
- const defaultLimits = evaluationLimits(void 0, settings);
546
- const limitsOf = (options) => options === void 0 ? defaultLimits : evaluationLimits(options, settings);
547
- let syncCode;
548
- let asyncCode;
549
- let pooled;
550
- let pooledInUse = false;
551
- const compileOptions = constants === void 0 ? {} : {
370
+ const { prepare, checked, settle, runSync, runAsync, compileOptions } = programRunner({
371
+ source,
372
+ analysis,
373
+ guard,
374
+ settings,
552
375
  constants,
553
- shapes: residualOf?.shapes
554
- };
555
- const syncProgram = () => syncCode ??= compileProgram(analysis, "sync", compileOptions);
556
- const asyncProgram = () => asyncCode ??= analysis.async ? compileProgram(analysis, "async", compileOptions) : syncProgram();
557
- function prepare(state, ctx, limits, locals) {
558
- state.reset(ctx, source, locals, limits.maxSteps, limits.timeout, limits.signal);
559
- if (limits.now !== void 0) state.nowValue = limits.now;
560
- const residual = residualOf;
561
- if (residual !== void 0 && residual.readsContext) {
562
- const roots = residual.knownRoots;
563
- state.charge(roots.length);
564
- const declared = settings.strict ? settings.variables : void 0;
565
- const missing = roots.find((name) => {
566
- if (declared !== void 0 && !Object.hasOwn(declared, name)) return false;
567
- const descriptor = Object.getOwnPropertyDescriptor(ctx, name);
568
- return descriptor === void 0 || "value" in descriptor && descriptor.value === void 0;
569
- });
570
- if (missing !== void 0) throw raised(new BonsaiRuntimeError("INVALID_CONTEXT", `Invalid context: ${missing} is missing; this residual calls a call: true host function, so pass the full context, as for the program`, { source }));
571
- }
572
- const validation = limits.validateContext ?? settings.validateContext ? settings.variables : void 0;
573
- if (validation !== void 0) {
574
- const validationLimits = {
575
- maxDepth: settings.runtimeLimits.maxValueDepth,
576
- maxSteps: limits.maxSteps,
577
- timeout: limits.timeout
578
- };
579
- if (residual === void 0 || residual.readsContext) validateContext(ctx, validation, source, validationLimits);
580
- else validatePaths(ctx, validation, residual.dependsOn, source, validationLimits);
581
- }
582
- }
583
- function runSync(context, options) {
584
- if (analysis.async) throw escaping(new BonsaiRuntimeError("ASYNC_IN_SYNC", "This expression calls an async function; use evaluate() instead of evaluateSync()", { source }));
585
- const code = syncProgram();
586
- const limits = limitsOf(options);
587
- const ctx = contextOf(context);
588
- let state;
589
- const reuse = !pooledInUse;
590
- if (reuse) {
591
- pooled ??= new State(settings.runtimeLimits, settings.clock);
592
- state = pooled;
593
- pooledInUse = true;
594
- } else state = new State(settings.runtimeLimits, settings.clock);
595
- try {
596
- prepare(state, ctx, limits, code.localCount);
597
- const result = code.run(state);
598
- checked(result, state);
599
- state.checkTime();
600
- return result;
601
- } catch (error) {
602
- throw escaping(hostDataFailure(error, source));
603
- } finally {
604
- state.release();
605
- if (reuse) pooledInUse = false;
606
- }
607
- }
608
- async function runAsync(context, options) {
609
- if (!analysis.async) {
610
- const result = runSync(context, options);
611
- try {
612
- return settle(result);
613
- } catch (error) {
614
- throw escaping(hostDataFailure(error, source));
615
- }
616
- }
617
- const code = asyncProgram();
618
- const limits = limitsOf(options);
619
- const ctx = contextOf(context);
620
- const state = new State(settings.runtimeLimits, settings.clock);
621
- try {
622
- prepare(state, ctx, limits, code.localCount);
623
- const result = (await code.run(state)).v;
624
- checked(result, state);
625
- state.checkTime();
626
- return settle(result);
627
- } catch (error) {
628
- throw escaping(hostDataFailure(error, source));
629
- } finally {
630
- state.release();
631
- }
632
- }
633
- function settle(result) {
634
- if (result !== null && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") throw raised(new BonsaiRuntimeError("TYPE_ERROR", "evaluate() cannot return a promise-like value; use evaluateSync()", { source }));
635
- return result;
636
- }
376
+ residualOf
377
+ });
637
378
  const features = settings.features?.program({
638
379
  source,
639
380
  analysis,
@@ -674,6 +415,13 @@ function createEnvironment(settings) {
674
415
  evaluateSync: (...args) => runSync(args[0], args[1])
675
416
  });
676
417
  recordDeclared(program, settings.variables);
418
+ recordCompiled(program, {
419
+ source,
420
+ analysis,
421
+ expect,
422
+ lookup: checkEnv.lookup,
423
+ variables: settings.variables
424
+ });
677
425
  return program;
678
426
  }
679
427
  function compile(source, expect) {
@@ -684,10 +432,12 @@ function createEnvironment(settings) {
684
432
  }
685
433
  /**
686
434
  * compile(), through the program cache: programs are immutable, so the same
687
- * source with an equal expected type (types are plain JSON data) shares one.
435
+ * source with an equal expected type shares one. `expect` is expectOption's
436
+ * plain copy, so its JSON is the type the program is checked against.
688
437
  */
689
438
  function cachedCompile(source, expect) {
690
439
  if (typeof source !== "string") throw new TypeError("An expression must be a string");
440
+ if (expect !== void 0 && holdsExact(expect)) return compile(source, expect);
691
441
  const expectKey = expect === void 0 ? void 0 : JSON.stringify(expect);
692
442
  const key = expectKey === void 0 ? source : `${source}\u0000${expectKey}`;
693
443
  const entry = cache.get(key);
@@ -706,7 +456,7 @@ function createEnvironment(settings) {
706
456
  function info(def) {
707
457
  return {
708
458
  name: def.name,
709
- description: def.description,
459
+ description: descriptionOf(def),
710
460
  host: def.host === true,
711
461
  async: def.async === true,
712
462
  call: def.call === true,
@@ -724,7 +474,7 @@ function createEnvironment(settings) {
724
474
  strict: settings.strict,
725
475
  parse: parseSource,
726
476
  check(source, options) {
727
- const expect = compileExpect(options);
477
+ const expect = expectOption(options, "Compile");
728
478
  let analysis;
729
479
  try {
730
480
  analysis = analyzeSource(source, expect);
@@ -745,7 +495,7 @@ function createEnvironment(settings) {
745
495
  let program;
746
496
  return Object.freeze(new CheckSuccess(type, diagnostics, analysis, () => program ??= makeProgram(source, analysis, expect)));
747
497
  },
748
- compile: (source, options) => cachedCompile(source, compileExpect(options)),
498
+ compile: (source, options) => cachedCompile(source, expectOption(options, "Compile")),
749
499
  ...settings.features === void 0 ? {} : { partial: (source, known, options) => cached(source).partial(known, options) },
750
500
  evaluate(source, ...args) {
751
501
  try {
@@ -789,11 +539,11 @@ function createEnvironment(settings) {
789
539
  */
790
540
  function environmentWith(features, options) {
791
541
  return createEnvironment({
792
- ...settingsFrom(void 0, options),
542
+ ...settingsFrom(void 0, options, BUILTINS),
793
543
  features
794
544
  });
795
545
  }
796
546
  //#endregion
797
- export { EVALUATE_OPTION_KEYS, contextOf, environmentWith, evaluationLimits, fn, hostDataFailure, internalsOf, invalidContext, isValidDate, validationBudget, withContext };
547
+ export { ProgramCache, environmentWith, expectOption, fn, internalsOf, settingsFrom, withContext };
798
548
 
799
549
  //# sourceMappingURL=environment.mjs.map