mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-26 18:47:35 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
241dc76ca5 | ||
|
|
beab7df087 |
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> =>
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())],
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user