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.
- package/CHANGELOG.md +20 -0
- package/README.md +24 -4
- package/dist/artifact.d.mts +47 -0
- package/dist/artifact.mjs +139 -0
- package/dist/artifact.mjs.map +1 -0
- package/dist/builtins/index.d.mts +91 -0
- package/dist/builtins/index.mjs +93 -0
- package/dist/builtins/index.mjs.map +1 -0
- package/dist/check/bind.mjs +117 -0
- package/dist/check/bind.mjs.map +1 -0
- package/dist/check/checker.mjs +34 -120
- package/dist/check/checker.mjs.map +1 -1
- package/dist/compile/compiler.mjs +2 -3
- package/dist/compile/compiler.mjs.map +1 -1
- package/dist/declared.mjs +8 -1
- package/dist/declared.mjs.map +1 -1
- package/dist/digest.mjs +109 -0
- package/dist/digest.mjs.map +1 -0
- package/dist/environment.d.mts +14 -1
- package/dist/environment.mjs +65 -315
- package/dist/environment.mjs.map +1 -1
- package/dist/errors.d.mts +21 -2
- package/dist/errors.mjs +17 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/features.mjs +3 -3
- package/dist/features.mjs.map +1 -1
- package/dist/functions/builtins.mjs +551 -474
- package/dist/functions/builtins.mjs.map +1 -1
- package/dist/functions/define.mjs +53 -11
- package/dist/functions/define.mjs.map +1 -1
- package/dist/functions/descriptions.mjs +110 -0
- package/dist/functions/descriptions.mjs.map +1 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -2
- package/dist/lite/index.d.mts +4 -3
- package/dist/lite/index.mjs +3 -2
- package/dist/lite/index.mjs.map +1 -1
- package/dist/load.mjs +307 -0
- package/dist/load.mjs.map +1 -0
- package/dist/program.mjs +321 -0
- package/dist/program.mjs.map +1 -0
- package/dist/query/index.mjs +1 -1
- package/dist/runtime/core.d.mts +18 -0
- package/dist/runtime/core.mjs +31 -0
- package/dist/runtime/core.mjs.map +1 -0
- package/dist/runtime/create.d.mts +61 -0
- package/dist/runtime/create.mjs +76 -0
- package/dist/runtime/create.mjs.map +1 -0
- package/dist/runtime/index.d.mts +24 -0
- package/dist/runtime/index.mjs +34 -0
- package/dist/runtime/index.mjs.map +1 -0
- package/dist/service/index.mjs +12 -8
- package/dist/service/index.mjs.map +1 -1
- package/dist/syntax/parser.mjs +1 -1
- package/dist/syntax/printer.mjs +1 -1
- package/dist/types.mjs +103 -23
- package/dist/types.mjs.map +1 -1
- package/dist/version.mjs +7 -0
- package/dist/version.mjs.map +1 -0
- package/package.json +26 -2
package/dist/environment.mjs
CHANGED
|
@@ -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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import { DEFAULT_RUNTIME_LIMITS, MAX_VALUE_DEPTH_LIMIT
|
|
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
|
-
|
|
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
|
-
/**
|
|
464
|
-
|
|
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
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
486
|
-
function
|
|
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(
|
|
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
|
|
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(
|
|
331
|
+
throw new TypeError(`${what} options could not be read`, { cause: error });
|
|
496
332
|
}
|
|
497
|
-
if (expect
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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) ??
|
|
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
|
|
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
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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 {
|
|
547
|
+
export { ProgramCache, environmentWith, expectOption, fn, internalsOf, settingsFrom, withContext };
|
|
798
548
|
|
|
799
549
|
//# sourceMappingURL=environment.mjs.map
|