Compare commits

...
20 changed files with 539 additions and 86 deletions
+40 -16
View File
@@ -34,10 +34,20 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [x] The timeout fires between interpreter steps, so one built-in is bounded in what it may build: strings up to
2^24 characters (`repeat`, `pad*`, `concat`, `join`, `+`, template literals, `JSON.stringify`), arrays up to
10,000,000 elements (`Array(n)`, `length =`, `Array.from`, `split`, `matchAll`, `concat`, `flat`; below the JS
maximum of 2^32 - 1), and 10,000 pending promises at once. Exceeding one throws a `RangeError`. A single regular
expression match can still run long on a pathological pattern; the host regex engine has no interrupt hook.
2^24 characters (`repeat`, `pad*`, `concat`, `join`, `+`, template literals, `JSON.stringify`, `replace` and
`replaceAll` with a string replacement (checked before the host builds the result), `encodeURI*`, `btoa`,
`Uint8Array` `toString`/`toBase64`/`toHex`, `URLSearchParams.toString`), arrays up to 10,000,000 elements
(`Array(n)`, `length =`, `Array.from`, spread, rest, `split`, `matchAll`, `concat`, `flat`; below the JS maximum
of 2^32 - 1), 250,000 arguments to one call (spread arguments, `apply`, bound arguments), and 10,000 pending
promises at once. Exceeding one throws a `RangeError`. A replacement whose quick bound is too big is measured
first: the check is exact for a global replacement without `$`, and otherwise charges each `$` token the longest
group of its match (a whole subject for `` $` `` and `$'`), so a result near the cap may still be rejected.
`encodeURI*` and `Uint8Array.toString` are checked after the host builds the string (at most nine times a capped
input). Unhandled rejections from un-awaited promises are reported individually up to 100, each message cut at
4,096 characters, with one summary warning counting the rest that were never handled.
- [ ] A single regular expression match can still run long on a pathological pattern (`/a*a*a*a*b/` on a 1 KB
subject): the host regex engine has no interrupt hook and no subject-length cap helps beyond quadratic patterns,
so this needs a step-counted regex engine of our own.
- [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`.
- [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at
parse time. Without it, `yield` is an ordinary binding.
@@ -55,7 +65,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Tagged templates: a tag applied to a template literal is called as `tag(strings, ...values)`, with the tag read
like a callee so a member tag keeps its receiver. `strings` is an array of the cooked text with a read-only `raw`
array of the source text; an invalid escape such as `\unicode` cooks to `undefined`. One template object per
site, as in JS, but it is not frozen: `strings[0] = "x"` succeeds here where JS throws.
site and both arrays are frozen, as in JS: `strings[0] = "x"` throws a `TypeError`.
- [x] Regular-expression literals.
- [x] `NaN` and `Infinity` globals.
- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries.
@@ -101,9 +111,11 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `for`, `while`, and `do...while`.
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, Headers, Uint8Arrays, built-in iterators, custom
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
- [ ] `for...in` over inherited enumerable keys (`Object.create(proto)`), and skipping keys deleted during the loop.
- [x] `for...in` over the enumerable keys of plain objects, arrays, strings, and tool references, following the
prototype chain like JS (`for (k in Object.create({ a: 1 }))` visits `a`; built-in prototype methods are
non-enumerable so `for (k in [])` visits nothing). A key deleted before its turn is skipped and keys added during
the loop are not visited. `null`, `undefined`, and other non-objects iterate nothing. An un-awaited promise
throws rather than iterating.
- [x] Unlabeled `break` and `continue`.
- [x] `try`, `catch`, optional catch bindings, and `finally`.
- [x] `throw` with arbitrary values.
@@ -122,8 +134,11 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters.
- [x] A call depth limit of 10000: deeper nesting throws a catchable `RangeError: Maximum call stack size exceeded`
at the overflowing call instead of running until the timeout. Callbacks invoked by built-ins count below the
call that invoked the built-in, and a resumed `await` starts from depth 0 as in JS, so long async chains such
as recursive pagination are unaffected.
call that invoked the built-in, and built-ins invoking one another count toward the same limit, so a cycle
through built-ins alone (`String(a)` on a 100,000-deep nested array runs `toString` → `join` → `toString`)
bottoms out too. A resumed `await` starts from depth 0 as in JS, so long async chains such as recursive pagination are
unaffected. A thenable that keeps resolving with another thenable loops in constant memory until the timeout, as
in JS.
- [x] Expression and block function bodies.
- [x] User callbacks for the supported Array, Map, Set, URLSearchParams, sort, string-replacement, and `Array.from`
mapper APIs, with one shared acceptance rule everywhere including promise reactions.
@@ -170,8 +185,11 @@ Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.
- [x] Redeclaring a function in the same scope, or alongside a `var`, is allowed: the last declaration wins.
- [x] Generator functions have their own `prototype` (inheriting the shared generator prototype), so
`g() instanceof g` holds. Plain functions have none, since they cannot construct.
- [ ] `GeneratorFunction.prototype`: every function, generator or not, inherits directly from `Function.prototype`,
and a generator whose `prototype` was replaced by a non-object still creates from the shared generator prototype.
- [x] Generator functions inherit from `GeneratorFunction.prototype` (async ones from
`AsyncGeneratorFunction.prototype`), an ordinary non-callable object under `Function.prototype` whose `prototype`
is the shared generator prototype and vice versa (`Object.getPrototypeOf(g).prototype.constructor`). Neither is
a global; async non-generator functions still inherit from `Function.prototype` directly. A generator whose
`prototype` was replaced by a non-object creates from the shared generator prototype.
- [x] Generator and async generator functions bind parameters (defaults, destructuring) at the call and defer only the
body to the first `next()`, so a bad argument throws synchronously from the call site, as in JS.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
@@ -247,9 +265,13 @@ Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.
hint (`"abc".indexOf({ toString() { return "b" } })` is `1`, `(255).toString({ valueOf() { return 16 } })` is
`"ff"`, `String.prototype.trim.call({ toString() { return " a " } })` is `"a"`). Only consumed positions
convert; a RegExp pattern is used as is, and `includes`/`startsWith`/`endsWith` reject one before converting.
- [ ] ToPrimitive elsewhere: `Error.prototype.toString` on an object `message` and numeric arguments of the Array and
Uint8Array methods (`at`, `indexOf` start, `slice`) still use the built-in form (`NaN`, `"[object Object]"`) and
ignore own methods.
- [x] `Error.prototype.toString` converts an object `name` or `message` through its own `toString` (`String(e)` with
`e.message = { toString() { return "m" } }` is `"Error: m"`); an uncaught error's report at the result boundary
still uses the built-in form.
- [x] `Array.prototype.toString` calls `this.join`, so `arr.join = () => "j"` makes `arr + ""`, `String(arr)`, and
`${arr}` all `"j"`; a non-callable `join` gives `"[object Array]"`.
- [ ] ToPrimitive elsewhere: numeric arguments of the Array and Uint8Array methods (`at`, `indexOf` start, `slice`)
still use the built-in form (`NaN`) and ignore own methods.
- [x] Property keys follow ToPropertyKey: `x[null]` and `x[true]` become string keys, and a data object key
converts through its own `toString`/`valueOf` (string hint) exactly once per access, in reads, writes,
compound assignment, `++`, `delete`, `in`, object literals, and destructuring:
@@ -259,6 +281,8 @@ Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.
## Promises and tools
- [x] Tool calls start eagerly and return supervised, run-once CodeMode promises.
- [x] Tool references have identity: `tools.x === tools.x` and `tools.ns === tools.ns`, so they work as `Map`/`Set`
members and `switch` cases like any other object.
- [x] Direct `await`, repeated awaits, and recursive thenable assimilation when a promise or thenable is returned from
a function/program.
- [x] `Promise.resolve` and `Promise.reject`.
@@ -335,7 +359,7 @@ reject }` object.
`TypeError`.
- [x] `Object.create(proto)` with an object or `null` prototype; any other prototype is a `TypeError`
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, and own-only
`Object.keys` follow the chain as in JS, but `for...in` still enumerates own keys only. A second `properties`
`Object.keys` follow the chain as in JS, and `for...in` enumerates inherited keys too. A second `properties`
argument other than `undefined` throws a `TypeError`: property descriptors are not supported (there is no
`Object.defineProperty` either).
- [x] `Object.freeze`, `Object.seal`, and `Object.preventExtensions`, with `isFrozen`, `isSealed`, and `isExtensible`.
+15 -6
View File
@@ -20,7 +20,7 @@ import {
type Value,
} from "./objects.js"
import type { Interpreter } from "./interpreter.js"
import { toPrimitiveString } from "./callback.js"
import { toPrimitiveString, withPrimitives } from "./callback.js"
import { formatValue } from "../stdlib/console.js"
export const normalizeError = (error: unknown): Diagnostic => {
@@ -48,7 +48,9 @@ export const normalizeError = (error: unknown): Diagnostic => {
if (error instanceof Throw) {
const value = error.value
if (value instanceof ErrorObj) {
return value.host ? normalizeError(value.host) : { kind: "ExecutionFailure", message: errorToString(value) }
return value.host
? normalizeError(value.host)
: { kind: "ExecutionFailure", message: errorToString(get(value, "name"), get(value, "message")) }
}
let message: string
if (containsRuntimeReference(value)) {
@@ -113,9 +115,7 @@ export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): Value => {
}
/** Error.prototype.toString: `name: message`, omitting whichever side is empty. */
const errorToString = (self: Obj): string => {
const name = get(self, "name")
const message = get(self, "message")
const errorToString = (name: Value, message: Value): string => {
const shownName = name === undefined ? "Error" : coerceToString(name)
const shownMessage = message === undefined ? "" : coerceToString(message)
if (shownMessage === "") return shownName
@@ -179,7 +179,16 @@ export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
})
if (type === "Error") {
methods(builtins, prototype, [
["toString", 0, (thisValue) => errorToString(receiver(Obj, thisValue, "Error.prototype.toString"))],
[
"toString",
0,
(thisValue) => {
const self = receiver(Obj, thisValue, "Error.prototype.toString")
return withPrimitives(ctx, "string", [get(self, "name"), get(self, "message")], ([name, message]) =>
errorToString(name, message),
)
},
],
])
methods(builtins, ctor, [["isError", 1, (_, args) => args[0] instanceof ErrorObj]])
return ctor
+10 -4
View File
@@ -21,7 +21,7 @@ import { errorGlobal } from "./errors.js"
import { errorTypes } from "./intrinsics.js"
import { constants, constructor, methods, native, receiver } from "./native.js"
import { AsyncIteratorSymbol, IteratorSymbol, typeError } from "./model.js"
import { checkArrayLength } from "./limits.js"
import { checkArgumentCount } from "./limits.js"
import { generatorGlobals } from "./generators.js"
import { promiseGlobal } from "./promises.js"
import type { Interpreter } from "./interpreter.js"
@@ -45,7 +45,10 @@ const functionGlobal = <R>(ctx: Interpreter<R>) => {
return native<R>(ctx.builtins, {
name: `bound ${coerceToString(get(fn, "name"))}`,
length: Math.max(0, fn.length - bound.length),
call: (_, rest) => ctx.call(fn, args[0], [...bound, ...rest]),
call: (_, rest) => {
checkArgumentCount(bound.length + rest.length)
return ctx.call(fn, args[0], [...bound, ...rest])
},
})
},
],
@@ -61,10 +64,13 @@ const functionGlobal = <R>(ctx: Interpreter<R>) => {
// CreateListFromArrayLike: `apply` reads `length` and the indexed properties of any object.
const listFromArrayLike = (value: Value): Array<Value> => {
if (value === undefined || value === null) return []
if (value instanceof Arr) return [...value.items]
if (value instanceof Arr) {
checkArgumentCount(value.items.length)
return [...value.items]
}
if (!(value instanceof Obj)) throw typeError("Function.prototype.apply expects an array-like argument list.")
const length = Math.max(0, coerceToInteger(get(value, "length")))
checkArrayLength(length)
checkArgumentCount(length)
return Array.from({ length }, (_, index) => get(value, String(index)))
}
@@ -63,7 +63,7 @@ import {
typeError,
unsupportedSyntax,
} from "./model.js"
import { checkStringLength } from "./limits.js"
import { checkArgumentCount, checkArrayLength, checkStringLength } from "./limits.js"
import { locate, materialize } from "./errors.js"
import { type Builtins, primitivePrototype } from "./intrinsics.js"
import { globals } from "./globals.js"
@@ -79,7 +79,8 @@ import {
has,
hidden,
hasPrototype,
keys,
ownKeys,
enumerable,
Native,
parseArrayIndex,
Arguments,
@@ -100,7 +101,7 @@ import { Pending, resolvePromise, resolvePromiseValue } from "./promises.js"
import { describeValue, isOpaque, rejectCircularInsertion, typeofValue } from "./references.js"
import { ScopeStack } from "./scope.js"
import { constructRegExp } from "../stdlib/regexp.js"
import { enumerableSource } from "../stdlib/object.js"
import { enumerableSource, restrict } from "../stdlib/object.js"
import { compoundOperators } from "../stdlib/value.js"
/** The binary operators that convert object operands through ToPrimitive before acting on primitives. */
@@ -318,7 +319,7 @@ export class Interpreter<R> {
readonly logs: Array<string>
/** Template objects by site: a tag sees the same `strings` array every time its literal is evaluated, as in JS. */
readonly templates = new WeakMap<TaggedTemplateExpression, Arr>()
private readonly root: Frame<R>
readonly root: Frame<R>
constructor(options: {
readonly tools: ToolRuntime<R>
@@ -506,7 +507,7 @@ class Frame<R> {
): Fn {
const builtins = this.ctx.builtins
const fn = new Fn(
builtins.Function,
node.generator ? (node.async ? builtins.AsyncGeneratorFunction : builtins.GeneratorFunction) : builtins.Function,
name,
node.params,
node.body,
@@ -955,10 +956,24 @@ class Frame<R> {
}
// for...in over null/undefined iterates nothing, like JS.
// EnumerateObjectProperties: own keys, then each prototype's, visiting a shadowed key once.
private enumerableKeys(value: Value, node: AstNode): Array<string> {
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
if (value === null || value === undefined) return []
return keys(enumerableSource(this.ctx, "for...in", value, node))
const seen = new Set<string>()
const result: Array<string> = []
for (
let current: Obj | null = enumerableSource(this.ctx, "for...in", value, node);
current !== null;
current = current.proto
) {
for (const key of ownKeys(current)) {
if (typeof key !== "string" || seen.has(key)) continue
seen.add(key)
if (enumerable(current, key)) result.push(key)
}
}
return result
}
private evaluateForInStatement(
@@ -982,6 +997,8 @@ class Frame<R> {
const assignment = left.type === "VariableDeclaration" ? undefined : left
for (const key of keys) {
// A key deleted before its turn is skipped, as in JS.
if (right instanceof Obj && !has(right, key)) continue
const result = yield* Effect.gen(function* () {
if (declared?.lexical) {
self.scopes.push()
@@ -1264,6 +1281,7 @@ class Frame<R> {
const next = yield* cursor.next
done = next.done
if (!done) rest.push(next.value)
checkArrayLength(rest.length)
}
yield* consume(element.argument, new Arr(self.ctx.builtins.Array, rest), element)
return
@@ -1710,13 +1728,20 @@ class Frame<R> {
})
}
// Built-ins throw without a location, synchronously or inside their Effect; the call site supplies it.
// Built-ins throw without a location, synchronously or inside their Effect; the call site supplies it. A built-in
// reached through `ctx.call` runs on the root frame, so the deeper of the frame and the enclosing site counts. There
// it was invoked by another built-in, and those hops are counted too, so a cycle through built-ins alone
// (toString → join → toString) bottoms out without changing the depth of program calls.
private native(body: () => Effect.Effect<Value, unknown, R>, node?: AstNode): Effect.Effect<Value, unknown, R> {
return Effect.provideService(
Effect.catchDefect(Effect.suspend(body), (defect) => Effect.die(locate(defect, node))),
CallSite,
{ node, depth: this.depth },
)
return Effect.flatMap(CallSite, (site) => {
const natives = this === this.ctx.root ? site.natives + 1 : 0
if (natives > MAX_CALL_DEPTH) throw rangeError("Maximum call stack size exceeded", node)
return Effect.provideService(
Effect.catchDefect(Effect.suspend(body), (defect) => Effect.die(locate(defect, node))),
CallSite,
{ node, depth: Math.max(this.depth, site.depth), natives },
)
})
}
private evaluateCallArguments(
@@ -1734,6 +1759,7 @@ class Frame<R> {
const step = yield* cursor.next
if (step.done) break
args.push(step.value)
checkArgumentCount(args.length)
}
} else {
args.push(yield* self.evaluateExpression(argNode))
@@ -2093,6 +2119,7 @@ class Frame<R> {
const step = yield* cursor.next
if (step.done) break
values.push(step.value)
checkArrayLength(values.length)
}
} else {
values.push(yield* self.evaluateExpression(element))
@@ -2155,12 +2182,16 @@ class Frame<R> {
define(
strings,
"raw",
new Arr(
array,
node.quasi.quasis.map((quasi) => quasi.value.raw),
restrict(
"freeze",
new Arr(
array,
node.quasi.quasis.map((quasi) => quasi.value.raw),
),
),
frozen,
)
restrict("freeze", strings)
this.ctx.templates.set(node, strings)
return strings
}
@@ -2208,7 +2239,7 @@ class Frame<R> {
if (typeof key !== "string") {
throw typeError("Tool paths must use string property names.", propertyNode)
}
return new ToolReference([...objectValue.path, key])
return objectValue.child(key)
}
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define, hidden, Native, Arr, ErrorObj, Obj, type Value } from "./objects.js"
import { Arr, define, ErrorObj, hidden, Native, Obj, readOnly, type Value } from "./objects.js"
export const errorTypes = [
"Error",
@@ -41,6 +41,8 @@ const builtins = [
"AsyncIterator",
"Generator",
"AsyncGenerator",
"GeneratorFunction",
"AsyncGeneratorFunction",
] as const
/**
@@ -79,6 +81,15 @@ export const createBuiltins = (): Builtins => {
}
const iterator = plain()
const asyncIterator = plain()
// %GeneratorFunction.prototype% is an ordinary object linked both ways with %GeneratorPrototype%.
const generatorFunction = (generator: Obj) => {
const proto = new Obj(fn)
define(proto, "prototype", generator, readOnly)
define(generator, "constructor", proto, readOnly)
return proto
}
const generator = new Obj(iterator)
const asyncGenerator = new Obj(asyncIterator)
return {
Object: object,
Function: fn,
@@ -102,8 +113,10 @@ export const createBuiltins = (): Builtins => {
Iterator: iterator,
IteratorHelper: new Obj(iterator),
AsyncIterator: asyncIterator,
Generator: new Obj(iterator),
AsyncGenerator: new Obj(asyncIterator),
Generator: generator,
AsyncGenerator: asyncGenerator,
GeneratorFunction: generatorFunction(generator),
AsyncGeneratorFunction: generatorFunction(asyncGenerator),
Error: error,
TypeError: derived("TypeError"),
RangeError: derived("RangeError"),
@@ -7,6 +7,8 @@ import { rangeError } from "./model.js"
export const MAX_STRING_LENGTH = 1 << 24
/** Longest array a built-in may create or grow to. */
export const MAX_ARRAY_LENGTH = 10_000_000
/** Most arguments one call may receive, including a bound function's bound arguments. */
export const MAX_ARGUMENTS = 250_000
/** Most promises that may be pending at once. */
export const MAX_PENDING_PROMISES = 10_000
/** Deepest nesting a value may have when it crosses to or from the host. */
@@ -16,6 +18,16 @@ export const checkStringLength = (length: number): void => {
if (length > MAX_STRING_LENGTH) throw rangeError("Invalid string length")
}
/** A string the host already built, after checking its length; for outputs at most nine times a capped input. */
export const boundedString = (value: string): string => {
checkStringLength(value.length)
return value
}
export const checkArgumentCount = (count: number): void => {
if (count > MAX_ARGUMENTS) throw rangeError(`Too many arguments: a call may pass at most ${MAX_ARGUMENTS}.`)
}
export const checkArrayLength = (length: number): void => {
if (length > MAX_ARRAY_LENGTH) throw rangeError("Invalid array length")
}
+9 -4
View File
@@ -7,10 +7,15 @@ import type { ErrorObj, Value } from "./objects.js"
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
export type AstNode = Node
/** The program call a built-in is running under: where to locate failures born inside it, and how deep the stack is there. */
export const CallSite = Context.Reference<{ readonly node?: AstNode; readonly depth: number }>("codemode/CallSite", {
defaultValue: () => ({ depth: 0 }),
})
/**
* The program call a built-in is running under: where to locate failures born inside it, how deep the stack is there,
* and how many built-ins have invoked one another since program code last called one.
*/
export const CallSite = Context.Reference<{
readonly node?: AstNode
readonly depth: number
readonly natives: number
}>("codemode/CallSite", { defaultValue: () => ({ depth: 0, natives: 0 }) })
export type Binding = {
mutable: boolean
+2 -1
View File
@@ -36,6 +36,7 @@ export const hidden: Attributes = { writable: true, enumerable: false, configura
export const readonly: Attributes = { writable: false, enumerable: false, configurable: true }
/** Constants such as `Math.PI` and a constructor's `prototype`. */
export const frozen: Attributes = { writable: false, enumerable: false, configurable: false }
export const readOnly: Attributes = { writable: false, enumerable: false, configurable: true }
/**
* An object owned by the program: own properties plus a prototype link. Subclasses answer, in one place, how a
@@ -645,7 +646,7 @@ export const ownKeys = (target: Obj): Array<string | symbol> => {
]
}
const enumerable = (target: Obj, key: string | symbol): boolean => own(target, key)?.enumerable === true
export const enumerable = (target: Obj, key: string | symbol): boolean => own(target, key)?.enumerable === true
/** Own enumerable keys, including the iterator symbols; what spread and `Object.assign` copy. */
export const enumerableKeys = (target: Obj): Array<string | symbol> =>
+32 -7
View File
@@ -16,12 +16,20 @@ const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: Value)
return undefined
})
// Unhandled rejections past the first hundred are only counted, and each reported message is cut, so a rejection loop
// cannot grow the report without bound.
const MAX_REJECTION_DIAGNOSTICS = 100
const MAX_DIAGNOSTIC_LENGTH = 4096
// Observation only controls rejection reporting; program completion interrupts all promise work.
export class Pending<R> {
private readonly active = new Set<PromiseObj>()
private readonly ids = new WeakMap<PromiseObj, number>()
private readonly observed = new WeakSet<PromiseObj>()
private readonly failures = new Map<number, Diagnostic>()
// An unreachable dropped promise can never be handled later, so a weak set is enough to keep the count exact.
private readonly dropped = new WeakSet<PromiseObj>()
private droppedCount = 0
private nextID = 0
constructor(
@@ -60,11 +68,17 @@ export class Pending<R> {
this.ids.delete(promise)
return
}
if (this.failures.size >= MAX_REJECTION_DIAGNOSTICS) {
this.dropped.add(promise)
this.droppedCount += 1
return
}
const failure = normalizeError(Cause.squash(exit.cause))
this.failures.set(id, {
...failure,
message: `Unhandled rejection from an un-awaited promise: ${failure.message}`,
})
const reason = failure.message.slice(0, MAX_DIAGNOSTIC_LENGTH)
// A JSC slice shares the memory of the string it cuts; slicing the short joined string copies it, so the long
// original message is freed.
const message = `Unhandled rejection from an un-awaited promise: ${reason}`.slice(0, MAX_DIAGNOSTIC_LENGTH)
this.failures.set(id, { ...failure, message })
})
return promise
})
@@ -74,6 +88,7 @@ export class Pending<R> {
// Observation must be recorded when responsibility transfers, before the consumer fiber runs.
markObserved(promise: PromiseObj): void {
this.observed.add(promise)
if (this.dropped.delete(promise)) this.droppedCount -= 1
const id = this.ids.get(promise)
this.ids.delete(promise)
if (id !== undefined) this.failures.delete(id)
@@ -88,7 +103,15 @@ export class Pending<R> {
}
diagnostics(): Array<Diagnostic> {
return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure)
const retained = [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure)
if (this.droppedCount === 0) return retained
return [
...retained,
{
kind: "ExecutionFailure",
message: `Unhandled rejections from un-awaited promises not reported individually: ${this.droppedCount}.`,
},
]
}
// Re-check because a straggler can create promises before its interruption lands.
@@ -116,6 +139,8 @@ export const resolvePromiseValue = <R>(
const then = get(value, "then")
if (typeofValue(then) !== "function") return Effect.succeed(value)
// The next thenable resolves through a tail flatMap, not a nested yield*, so a chain that never ends runs in
// constant memory, as in JS.
return Effect.gen(function* () {
// Promise resolution invokes a thenable's method in a later job.
yield* Effect.yieldNow
@@ -127,8 +152,8 @@ export const resolvePromiseValue = <R>(
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
}
return yield* resolvePromiseValue(ctx, yield* Deferred.await(deferred), own)
})
return yield* Deferred.await(deferred)
}).pipe(Effect.flatMap((next) => resolvePromiseValue(ctx, next, own)))
}
export const resolvePromise = <R>(ctx: Interpreter<R>, value: Value): Effect.Effect<PromiseObj, never, R> => {
+8 -7
View File
@@ -5,6 +5,7 @@ import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpret
import {
define,
get,
Callable,
hidden,
Arr,
GeneratorObj,
@@ -63,6 +64,7 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Va
? step.value
: yield* preserveConsumerError(cursor.close, apply([step.value, index], args[2])),
)
checkArrayLength(values.length)
index += 1
}
})
@@ -165,13 +167,12 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
[
"toString",
0,
(thisValue) =>
withPrimitives(
ctx,
"string",
Array.from(self(thisValue, "toString").items, (item) => item ?? ""),
(items) => items.map(coerceToString).join(","),
),
(thisValue) => {
// Spec: delegate to this.join, so an overridden join shows up in `arr + ""` and String(arr).
const target = self(thisValue, "toString")
const join = get(target, "join")
return join instanceof Callable ? ctx.call(join, target, []) : `[object ${target.tag}]`
},
],
[
"includes",
+11 -3
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
import { boundedString, checkArrayLength, checkStringLength } from "../interpreter/limits.js"
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
import { IteratorSymbol, rangeError, syntaxError, typeError } from "../interpreter/model.js"
import {
@@ -171,9 +171,17 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
return joined
},
],
["toString", 0, (thisValue) => self(thisValue, "toString").bytes.join(",")],
["toString", 0, (thisValue) => boundedString(self(thisValue, "toString").bytes.join(","))],
["toBase64", 0, (thisValue) => self(thisValue, "toBase64").bytes.toBase64()],
["toHex", 0, (thisValue) => self(thisValue, "toHex").bytes.toHex()],
[
"toHex",
0,
(thisValue) => {
const bytes = self(thisValue, "toHex").bytes
checkStringLength(bytes.length * 2)
return bytes.toHex()
},
],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").bytes.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").bytes.values())],
[
+1 -1
View File
@@ -104,7 +104,7 @@ const propertyKey = (value: Value): PropertyKey =>
// SetIntegrityLevel: primitives pass through. A typed array's bytes cannot carry attributes, so JS throws after
// already making it non-extensible.
const restrict = (level: "freeze" | "seal" | "preventExtensions", value: Value): Value => {
export const restrict = (level: "freeze" | "seal" | "preventExtensions", value: Value): Value => {
if (!(value instanceof Obj)) return value
value.extensible = false
if (level === "preventExtensions") return value
+45 -3
View File
@@ -1,6 +1,6 @@
import { Effect } from "effect"
import { constructor, fn, type Impl, type Method, methods } from "../interpreter/native.js"
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
import { checkArrayLength, checkStringLength, MAX_STRING_LENGTH } from "../interpreter/limits.js"
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
import {
define,
@@ -36,6 +36,45 @@ const requireDataArgument = (name: string, index: number, arg: Value): Value =>
return arg
}
// The host builds a replaced string in one synchronous step, so bound its length first: the unmatched text plus, per
// match, the replacement, a whole subject per `` $` `` or `$'`, and the match's longest group per `$` token. Groups lie
// inside their match, so they sum to at most the subject, unless a positive lookaround captures past it. Matches are
// only measured when the bound for "every position matches" is too big; the measured bound is exact without `$`.
const checkReplacementLength = (subject: string, search: RegExp | string, replacement: string, all: boolean): void => {
const tokens = (token: string) => countOccurrences(replacement, token)
const perMatch = replacement.length + (tokens("$`") + tokens("$'")) * subject.length
const bound = (matches: { count: number; matched: number; reach: number }) =>
subject.length - matches.matched + matches.count * perMatch + tokens("$") * matches.reach
const lookaround = typeof search !== "string" && /\(\?<?=/.test(search.source)
const reach = lookaround ? (subject.length + 1) * subject.length : subject.length
if (bound({ count: subject.length + 1, matched: 0, reach }) <= MAX_STRING_LENGTH) return
if (!all) return checkStringLength(bound({ count: 1, matched: 0, reach: subject.length }))
if (typeof search === "string") {
const count = countOccurrences(subject, search)
return checkStringLength(bound({ count, matched: count * search.length, reach: count * search.length }))
}
// matchAll steps past an empty match by code point under `u` and `v`; the copy starts at 0 whatever the program's
// `lastIndex` is, as a global `replace` does.
const matches = subject.matchAll(new RegExp(search)).reduce(
(total, match) => ({
count: total.count + 1,
matched: total.matched + match[0].length,
reach: total.reach + match.reduce((longest, group) => Math.max(longest, group?.length ?? 0), 0),
}),
{ count: 0, matched: 0, reach: 0 },
)
checkStringLength(bound(matches))
}
const countOccurrences = (subject: string, needle: string): number => {
if (needle === "") return subject.length + 1
let count = 0
for (let index = subject.indexOf(needle); index !== -1; index = subject.indexOf(needle, index + needle.length)) {
count += 1
}
return count
}
const replaceAllNeedsGlobal = (pattern: RegExp) => {
if (!pattern.global) {
throw typeError(
@@ -208,10 +247,13 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
const regex = pattern.regex
const text = str(name, primitives, 1)
if (name === "replaceAll") replaceAllNeedsGlobal(regex)
checkReplacementLength(value, regex, text, regex.global)
return name === "replace" ? value.replace(regex, text) : value.replaceAll(regex, text)
}
if (name === "replace") return value.replace(str(name, primitives, 0), str(name, primitives, 1))
return value.replaceAll(str(name, primitives, 0), str(name, primitives, 1))
const needle = str(name, primitives, 0)
const text = str(name, primitives, 1)
checkReplacementLength(value, needle, text, name === "replaceAll")
return name === "replace" ? value.replace(needle, text) : value.replaceAll(needle, text)
},
)
})
+15 -3
View File
@@ -1,4 +1,5 @@
import { Effect } from "effect"
import { boundedString, checkStringLength } from "../interpreter/limits.js"
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
import { IteratorSymbol, PendingThrow, typeError, uriError } from "../interpreter/model.js"
import {
@@ -46,9 +47,10 @@ export const uriGlobal = <R>(ctx: Interpreter<R>, name: UriFunction) =>
fn<R>(ctx.builtins, name, 1, (_, args) => {
const value = coerceToString(args[0])
try {
return uriFunctions[name](value)
return boundedString(uriFunctions[name](value))
} catch (error) {
throw uriError(`${name} received malformed URI data: ${error instanceof Error ? error.message : String(error)}`)
if (!(error instanceof URIError)) throw error
throw uriError(`${name} received malformed URI data: ${error.message}`)
}
})
@@ -272,7 +274,17 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").params.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").params.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
[
"toString",
0,
(thisValue) => {
const params = self(thisValue, "toString").params
// Every character serializes to at least one, plus `=` per pair and `&` between pairs, so this lower bound
// rejects before the host builds anything.
checkStringLength([...params].reduce((length, [key, value]) => length + key.length + value.length + 2, -1))
return boundedString(params.toString())
},
],
[
"forEach",
1,
+2
View File
@@ -1,4 +1,5 @@
import { fn, methods } from "../interpreter/native.js"
import { checkStringLength } from "../interpreter/limits.js"
import { typeError } from "../interpreter/model.js"
import {
define,
@@ -32,6 +33,7 @@ export const base64Global = <R>(ctx: Interpreter<R>, name: "atob" | "btoa") =>
fn<R>(ctx.builtins, name, 1, (_, args) => {
if (args.length === 0) throw typeError(`${name} requires 1 argument (a string)`)
const input = coerceToString(args[0])
if (name === "btoa") checkStringLength(Math.ceil(input.length / 3) * 4)
try {
return name === "atob" ? atob(input) : btoa(input)
} catch {
+9
View File
@@ -113,7 +113,16 @@ export const toolExpression = (path: string) =>
.join("")
export class ToolReference {
private readonly children = new Map<string, ToolReference>()
constructor(readonly path: ReadonlyArray<string>) {}
/** One reference per path, so `tools.a === tools.a` holds like any other member read. */
child(key: string): ToolReference {
const existing = this.children.get(key)
if (existing !== undefined) return existing
const created = new ToolReference([...this.path, key])
this.children.set(key, created)
return created
}
}
// Dots in tool names are namespace separators; the last tool for a canonical path wins.
+19
View File
@@ -178,6 +178,25 @@ describe("call depth", () => {
expect(Date.now() - started).toBeLessThan(2000)
})
test("recursion routed through nested built-ins keeps counting depth", async () => {
const started = Date.now()
expect(
await value(`
const a = [1]
a.join = () => a + ""
const o = { toString() { return [o].map(String)[0] } }
const e = new Error()
e.message = { toString() { return String(e) } }
const names = []
for (const run of [() => String(a), () => String(o), () => String(e)]) {
try { run() } catch (error) { names.push(error.name) }
}
return names
`),
).toEqual(["RangeError", "RangeError", "RangeError"])
expect(Date.now() - started).toBeLessThan(2000)
})
test("uncaught overflow reports the call that overflowed", async () => {
const failure = await error(`const f = (n) => f(n + 1); return f(0)`)
expect(failure.kind).toBe("ExecutionFailure")
+233 -3
View File
@@ -1382,7 +1382,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
expect((await error(`Object.getPrototypeOf(Symbol.iterator)`)).message).toContain("cannot convert a symbol")
})
test("Object.create links the prototype: inherited reads, in, and own-only keys", async () => {
test("Object.create links the prototype: inherited reads, in, own-only keys, and for...in", async () => {
expect(
await value(`
const p = { greet(name) { return "hi " + name }, a: 1 }
@@ -1392,7 +1392,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
for (const key in c) seen.push(key)
return ["greet" in c, Object.keys(c), c.hasOwnProperty("a"), c.a, c.greet(c.name), Object.getPrototypeOf(c) === p, Object.getPrototypeOf(Object.create(null)), seen]
`),
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name"]])
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name", "greet", "a"]])
})
test("Object.create rejects non-object prototypes and property descriptors", async () => {
@@ -1560,7 +1560,7 @@ describe("this, arguments, and Function.prototype.call/apply/bind", () => {
"Function.prototype.call called on incompatible receiver",
)
expect((await error(`(() => 1).apply(null, 5)`)).message).toContain("expects an array-like argument list")
expect((await error(`(() => 1).apply(null, { length: 1e9 })`)).message).toContain("Invalid array length")
expect((await error(`(() => 1).apply(null, { length: 1e9 })`)).message).toContain("Too many arguments")
expect(
await value(
`function f() { return arguments.length } return [f.apply(null, { length: -5 }), f.apply(null, { length: "2" })]`,
@@ -1984,3 +1984,233 @@ describe("WeakMap and WeakSet", () => {
expect((await error(`structuredClone(new WeakSet())`)).message).toContain("DataCloneError")
})
})
describe("small language leftovers", () => {
test("for...in walks the prototype chain and skips keys deleted before their turn", async () => {
expect(
await value(`
const o = Object.create({ a: 1, shadowed: 1 })
o.b = 2
o.shadowed = 3
const keys = []
for (const k in o) keys.push(k)
const live = { a: 1, b: 2, c: 3 }
const seen = []
for (const k in live) { seen.push(k); delete live.b; live.z = 1 }
const none = []
for (const k in []) none.push(k)
for (const k in new TypeError("x")) none.push(k)
return [keys, seen, none]
`),
).toEqual([["b", "shadowed", "a"], ["a", "c"], []])
})
test("tagged template objects are frozen", async () => {
expect(
await value(`
const tag = (s) => s
const f = () => tag\`a\${1}b\`
return [f() === f(), Object.isFrozen(f()), Object.isFrozen(f().raw)]
`),
).toEqual([true, true, true])
expect((await error("const tag = (s) => s; tag`a`[0] = 'x'")).message).toContain("read only")
})
test("Array.prototype.toString delegates to join", async () => {
expect(
await value(`
const a = [1, 2]
a.join = () => "j"
const b = [1]
b.join = 5
return [a + "", String(a), \`\${a}\`, b.toString(), Array.prototype.toString.call([3, [4]])]
`),
).toEqual(["j", "j", "j", "[object Array]", "3,4"])
})
test("Error.prototype.toString converts object name and message", async () => {
expect(
await value(`
const e = new Error("m")
e.message = { toString() { return "obj" } }
e.name = { valueOf() { return "N" }, toString() { return "T" } }
return [String(e), Error.prototype.toString.call({ name: "", message: "m" }), Error.prototype.toString.call({})]
`),
).toEqual(["T: obj", "m", "Error"])
expect(
(await error(`const e = new Error(); e.message = { toString() { throw new RangeError("r") } }; String(e)`))
.message,
).toContain("r")
})
test("generator functions inherit from GeneratorFunction.prototype", async () => {
expect(
await value(`
function* g() {}
async function* ag() {}
const GFP = Object.getPrototypeOf(g)
g.prototype = null
return [
typeof GFP, GFP === Function.prototype, Object.getPrototypeOf(GFP) === Function.prototype,
GFP.prototype.constructor === GFP, Object.getPrototypeOf(ag) === GFP, typeof g.bind,
Object.getPrototypeOf(g()) === GFP.prototype,
]
`),
).toEqual(["object", false, true, true, false, "function", true])
expect((await error(`function* g() {} Object.getPrototypeOf(g)()`)).message).toContain("not a function")
})
})
describe("confinement caps", () => {
test("string replacement results are bounded before the host builds them", async () => {
expect((await error(`"x".repeat(2 ** 16).replaceAll("x", "x".repeat(2 ** 9))`)).message).toContain(
"Invalid string length",
)
expect((await error(`"x".repeat(2 ** 20).replace(/x/g, "yyyyyyyyyyyyyyyyyyyyyyyyy")`)).message).toContain(
"Invalid string length",
)
// Every prefix token can repeat the subject once per match, and so can a capture inside a lookahead.
expect((await error('"x".repeat(2600).replace(/x/g, "$`".repeat(100))')).message).toContain("Invalid string length")
expect((await error('"x".repeat(20000).replace(/(?=(x*))/g, "$1")')).message).toContain("Invalid string length")
expect(
(await error(`const p = new URLSearchParams(); const s = "x".repeat(2 ** 24); p.append(s, s); p.toString()`))
.message,
).toContain("Invalid string length")
// Counting empty matches must step over surrogate pairs under the u flag.
expect(
await value(
`return ["😀".repeat(10).replace(/(?:)/gu, "x".repeat(2 ** 20)).length, "1234567".replace(/(\\d)(?=(\\d{3})+$)/g, "$1,")]`,
),
).toEqual([11534356, "1,234,567"])
expect(
await value(`
return [
"x".repeat(2 ** 23).replaceAll("x", "y").length,
"ab".repeat(2 ** 21).replace(/a/g, "$&").length,
"abc".replaceAll("", "-"),
"aaa".replace(/a/g, (m) => m + m),
"hello world ".repeat(2000).replace(/(\\w+)(?=\\s)/g, "[$1]").length,
"x".repeat(100).replaceAll("x", "y".repeat(167772)).length,
"a ".repeat(3e6).replace(/ +/g, " ").length,
]
`),
).toEqual([8388608, 4194304, "-a-b-c-", "aaaaaa", 32000, 16777200, 6000000])
expect((await error(`encodeURIComponent("\\u{1F600}".repeat(2 ** 22))`)).message).toContain("Invalid string length")
expect((await error(`btoa("x".repeat(2 ** 24))`)).message).toContain("Invalid string length")
expect((await error(`new Uint8Array(9e6).toHex()`)).message).toContain("Invalid string length")
expect((await error(`new Uint8Array(9e6).toString()`)).message).toContain("Invalid string length")
expect(
(await error(`const p = new URLSearchParams(); p.append("\\u20ac".repeat(2 ** 21), ""); p.toString()`)).message,
).toContain("Invalid string length")
expect((await error(`decodeURIComponent("%")`)).message).toContain("malformed URI")
})
test("recursion through built-ins alone hits the call depth limit", async () => {
expect(
await value(`
let a = []
for (let i = 0; i < 100000; i++) a = [a]
let shallow = []
for (let i = 0; i < 3000; i++) shallow = [shallow]
const names = []
try { String(a) } catch (e) { names.push(e.name) }
try { \`\${a}\` } catch (e) { names.push(e.name) }
const b = [1]
b.join = b.toString
try { String(b) } catch (e) { names.push(e.name) }
return [names, String(shallow).length, [[[1]]].map((x) => String(x))]
`),
).toEqual([["RangeError", "RangeError", "RangeError"], 0, ["1"]])
// A resumed await starts from depth 0 even when each step recurses through a bound callback of a built-in.
expect(
await value(`
let n = 0
let failure = null
async function step() {
await null
n++
if (n < 12000) [0].forEach((() => { step().catch((e) => { failure = e.name }) }).bind(null))
}
await step()
while (n < 12000 && failure === null) await null
return [n, failure]
`),
).toEqual([12000, null])
})
test("argument and spread counts are capped", async () => {
expect((await error(`Math.max(...Array(300000).fill(1))`)).message).toContain("Too many arguments")
expect((await error(`Math.max.apply(null, { length: 1e9 })`)).message).toContain("Too many arguments")
expect(
(await error(`function f() { return arguments.length } const a = Array(2e5).fill(0); f(...a, ...a)`)).message,
).toContain("Too many arguments")
expect((await error(`function f() {} f.apply(null, Array(300000).fill(0))`)).message).toContain(
"Too many arguments",
)
expect(
(
await error(
`const a = Array(2e5).fill(0); const f = ((...xs) => xs.length).bind(null, ...a).bind(null, ...a); f()`,
)
).message,
).toContain("Too many arguments")
expect((await error(`const a = Array(6e6).fill(0); [...a, ...a]`)).message).toContain("Invalid array length")
expect(await value(`return Math.max(...Array(200000).fill(7))`)).toBe(7)
})
test("a thenable chain resolves iteratively and a self-resolving promise is a chaining cycle", async () => {
expect(
await value(
`const mk = (n) => n === 0 ? "done" : { then(res) { res(mk(n - 1)) } }; return await Promise.resolve(mk(5000))`,
),
).toBe("done")
expect(
(await error(`let p; p = new Promise((r) => Promise.resolve().then(() => r(p))); await p`)).message,
).toContain("Chaining cycle")
// The cycle check still applies after a thenable step hands back the promise being resolved.
expect(
(await error(`let p; p = new Promise((r) => Promise.resolve().then(() => r({ then(r2) { r2(p) } }))); await p`))
.message,
).toContain("Chaining cycle")
})
test("a thenable that resolves with itself runs in constant memory until the timeout", async () => {
Bun.gc(true)
const before = process.memoryUsage().heapUsed
let peak = before
const sample = setInterval(() => (peak = Math.max(peak, process.memoryUsage().heapUsed)), 50)
const result = await Effect.runPromise(
CodeMode.execute({
code: `const t = { then(res) { res(t) } }; await Promise.resolve(t)`,
tools: {},
limits: { timeoutMs: 1000 },
}),
)
clearInterval(sample)
expect(result.ok ? undefined : result.error.kind).toBe("TimeoutExceeded")
expect(peak - before).toBeLessThan(50_000_000)
})
test("unhandled rejection diagnostics are capped with a summary", async () => {
const result = await run(`for (let i = 0; i < 500; i++) Promise.reject(new Error("x".repeat(10000))); return 1`)
if (!result.ok) throw new Error(result.error.message)
expect(result.warnings?.length).toBe(101)
expect(result.warnings?.[0]?.message.length).toBeLessThanOrEqual(4096)
expect(result.warnings?.at(-1)?.message).toContain("not reported individually: 400")
const handled = await run(
`const ps = []; for (let i = 0; i < 200; i++) ps.push(Promise.reject(new Error("x"))); await Promise.allSettled(ps); return 1`,
)
expect(handled.ok && handled.warnings).toBeUndefined()
})
test("cut rejection messages do not keep the full message alive", async () => {
Bun.gc(true)
const before = process.memoryUsage().heapUsed
const result = await run(
`for (let i = 0; i < 50; i++) Promise.reject(new Error(i + "x".repeat(2 ** 22))); return 1`,
)
Bun.gc(true)
expect(result.ok && result.warnings?.length).toBe(50)
expect(process.memoryUsage().heapUsed - before).toBeLessThan(50_000_000)
})
})
@@ -884,7 +884,6 @@ built-ins/AsyncGeneratorFunction/invoked-as-constructor-no-arguments.js # TypeE
built-ins/AsyncGeneratorFunction/invoked-as-function-multiple-arguments.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/AsyncGeneratorFunction/invoked-as-function-no-arguments.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/AsyncGeneratorFunction/invoked-as-function-single-argument.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/AsyncGeneratorFunction/prototype/not-callable.js # Expected SameValue(«"function"», «"object"») to be true
built-ins/AsyncGeneratorPrototype/next/this-val-not-async-generator.js # TypeError: Cannot read properties of undefined (reading '…').
built-ins/AsyncGeneratorPrototype/return/this-val-not-async-generator.js # TypeError: Cannot read properties of undefined (reading '…').
built-ins/AsyncGeneratorPrototype/throw/this-val-not-async-generator.js # TypeError: Cannot read properties of undefined (reading '…').
@@ -900,7 +899,6 @@ built-ins/Date/prototype/toJSON/invoke-result.js # TypeError: Date.prototype.to
built-ins/Date/prototype/toJSON/to-primitive-value-of.js # TypeError: Date.prototype.toJSON called on incompatible receiver a data object.
built-ins/Date/prototype/toString/format.js # Expected SameValue(«null», «null») to be false
built-ins/Date/prototype/toString/negative-year.js # Date.prototype.toString serializes year -1 to "-0001" Expected SameValue(«undefined», «"-0001"») to
built-ins/Error/prototype/toString/tostring-message-throws-toprimitive.js # ToPrimitive(msg) called by ToString(msg) throws TypeError Expected a TypeError to be thrown but no e
built-ins/Function/15.3.5-1gs.js # Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Function/15.3.5-2gs.js # Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Function/15.3.5.4_2-1gs.js # Expected a TypeError to be thrown but no exception was thrown at all
@@ -931,7 +929,6 @@ built-ins/GeneratorFunction/invoked-as-constructor-no-arguments.js # TypeError:
built-ins/GeneratorFunction/invoked-as-function-multiple-arguments.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/GeneratorFunction/invoked-as-function-no-arguments.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/GeneratorFunction/invoked-as-function-single-argument.js # TypeError: The Function constructor is not supported; write the function inline.
built-ins/GeneratorFunction/prototype/not-callable.js # Expected SameValue(«"function"», «"object"») to be true
built-ins/GeneratorPrototype/throw/from-state-completed.js # Expected a E but got a TypeError
built-ins/GeneratorPrototype/throw/from-state-suspended-start.js # Expected a E but got a TypeError
built-ins/Iterator/from/return-method-returns-iterator-result.js # Iterator next must be a function.
@@ -1468,12 +1465,10 @@ language/expressions/function/dstr/ary-init-iter-get-err-array-prototype.js # E
language/expressions/function/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/expressions/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/expressions/function/dstr/dflt-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/expressions/generators/default-proto.js # Expected SameValue(«object», «undefined») to be true
language/expressions/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/expressions/generators/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/expressions/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/expressions/generators/dstr/dflt-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/expressions/generators/prototype-relation-to-function.js # Expected SameValue(«object», «») to be true
language/expressions/greater-than-or-equal/S11.8.4_A3.2_T1.2.js # TypeError: Binary operators require data values.
language/expressions/greater-than/S11.8.2_A3.2_T1.2.js # TypeError: Binary operators require data values.
language/expressions/in/S8.12.6_A2_T2.js # TypeError: Robin cannot be constructed: user-defined constructors and classes are not supported. Cal
@@ -1559,7 +1554,6 @@ language/expressions/super/prop-expr-obj-key-err.js # unsupported syntax Super
language/expressions/super/prop-expr-obj-ref-strict.js # unsupported syntax Super
language/expressions/super/prop-expr-obj-unresolvable.js # Expected SameValue(«SyntaxError», «ReferenceError») to be true
language/expressions/tagged-template/constructor-invocation.js # The called value cannot be constructed: user-defined constructors and classes are not supported.
language/expressions/tagged-template/template-object-frozen-strict.js # Expected a TypeError to be thrown but no exception was thrown at all
language/expressions/this/11.1.1-1.js # Expected SameValue(«undefined», «undefined») to be false
language/expressions/unary-minus/S11.4.7_A3_T5.js # TypeError: Unary operators require data values.
language/expressions/unary-plus/S11.4.6_A3_T5.js # TypeError: Unary operators require data values.
@@ -1588,7 +1582,6 @@ language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-iter-val-array-pr
language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret"
language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/const/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/for-in/S12.6.4_A7_T2.js # for...in visits keys deleted during the loop
language/statements/for-in/order-enumerable-shadowed.js # Object.create property descriptors are not supported; assign the fields after creating the object.
language/statements/for-in/S12.6.4_A6.1.js # TypeError: FACTORY cannot be constructed: user-defined constructors and classes are not supported. C
language/statements/for-in/S12.6.4_A6.js # TypeError: FACTORY cannot be constructed: user-defined constructors and classes are not supported. C
@@ -1660,12 +1653,10 @@ language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Ex
language/statements/function/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/function/dstr/dflt-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/generators/default-proto.js # generator functions have no GeneratorFunction.prototype
language/statements/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/generators/prototype-relation-to-function.js # generator functions have no GeneratorFunction.prototype
language/statements/generators/restricted-properties.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected.
language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
+13
View File
@@ -368,3 +368,16 @@ describe("tool references under ==", () => {
).toEqual([false, false, false, 0])
})
})
describe("tool reference identity", () => {
test("repeated member reads yield the same reference", async () => {
const runtime = CodeMode.make({ tools: { probe: echo("Probe", "ok"), "ns.inner": echo("Inner", "in") } })
expect(
await value(
runtime,
`return [tools.probe === tools.probe, tools.ns.inner === tools.ns.inner, tools.ns === tools.ns, tools["probe"] === tools.probe,
tools.probe === tools.ns.inner, new Set([tools.probe, tools.probe]).size, await tools.ns.inner({})]`,
),
).toEqual([true, true, true, true, false, 1, "in"])
})
})