Compare commits

...
11 changed files with 2625 additions and 4 deletions
+39 -1
View File
@@ -9,7 +9,8 @@ standard-library surface that programs can use today, plus concrete gaps that ma
- Intentional boundaries are not listed as compatibility work.
When behavior changes, update this file and the tests in the same change. The implementation and tests remain the
ultimate source of truth.
ultimate source of truth. Upstream test262 files run verbatim from `test/test262`; a failing file is listed in
`test/test262/skipped.txt` and its gap is an unchecked item here (see `test/test262/README.md`).
## Source and execution model
@@ -25,6 +26,10 @@ ultimate source of truth.
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
rest parameter are accepted unless the program itself begins with `"use strict"`.
- [ ] Valid JavaScript rejected by TypeScript transpilation before interpretation, such as `in` inside a destructuring
default in a `for...of` head and Unicode-escaped keywords.
## Values and literals
@@ -64,6 +69,12 @@ ultimate source of truth.
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
or binding/default failure.
- [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object
sources are rejected.
- [ ] Destructuring a key that member access resolves through the owning built-in, such as
`const { constructor } = error`, reads `undefined`.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
- [ ] `IteratorClose` during destructuring should throw a `TypeError` when `return()` yields a non-object.
## Statements and control flow
@@ -111,6 +122,12 @@ ultimate source of truth.
- [ ] User-defined constructor calls.
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
- [ ] Classes and private fields.
- [ ] `name` and `length` properties of functions, including names inferred from bindings and destructuring defaults.
- [ ] A named function expression's name is not bound inside its own body.
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
- [ ] A line terminator between `async function` and the function name.
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
rather than at the call, so their errors are not thrown synchronously.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
@@ -154,6 +171,12 @@ ultimate source of truth.
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length.
- [ ] Operators, `switch` discriminants, template interpolation, and coercion helpers such as `String` and `isNaN`
applied to functions and namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
- [ ] ToPrimitive on object operands: operators, `Error(message)`, `Date` arguments, and `parseInt` radix should call
`valueOf`/`toString` in spec order and surface their throws.
- [ ] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via `toString`) become string keys; only
strings and numbers are accepted.
## Promises and tools
@@ -226,6 +249,8 @@ ultimate source of truth.
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and null-prototype results.
- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, and
`propertyIsEnumerable`.
## Arrays
@@ -249,6 +274,13 @@ ultimate source of truth.
`1`; arbitrary array-property assignment remains unsupported.
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
like JavaScript.
- [ ] Assigning `length` to truncate or extend an array.
- [ ] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`).
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
`undefined`.
- [ ] Iterator objects from `keys`, `values`, and `entries` with a live `next()`.
## Strings
@@ -268,6 +300,8 @@ ultimate source of truth.
`repeat` still requires a finite non-negative count.
- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. Present
arguments must still be a regular expression or string pattern.
- [ ] `String.raw`.
- [ ] `match`, `search`, and `split` accept any value and coerce it (objects via `toString`), like JavaScript.
## Numbers and Math
@@ -322,6 +356,8 @@ ultimate source of truth.
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] `toDateString` and `toTimeString` in the host's local timezone.
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
- [ ] Date setters and multi-argument construction coerce object arguments through `valueOf`/`toString` and surface
their throws.
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
representation for the string primitive.
- [x] Native `RangeError` branding for invalid `toISOString()` calls.
@@ -391,3 +427,5 @@ ultimate source of truth.
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
This is deliberate: the program should handle a failure the same way regardless of where it originated.
- [ ] Failures raised by the interpreter itself carry the generic `Error` name where JavaScript throws a `TypeError`,
`RangeError`, or `ReferenceError`, so `e instanceof TypeError` and `e.constructor === TypeError` are false.
+62
View File
@@ -0,0 +1,62 @@
// Copies the manifest's test262 directories from a local checkout into test/test262, verbatim.
// Files needing unsupported flags, features, or harness includes, or whose code crosses one of the
// interpreter's intentional boundaries, are not copied, so the vendored tree is exactly what
// test/test262.test.ts runs.
//
// Usage: bun run script/sync-test262.ts /path/to/test262
import path from "node:path"
import { rm } from "node:fs/promises"
type Frontmatter = { flags?: Array<string>; features?: Array<string>; includes?: Array<string> }
const root = path.resolve(import.meta.dir, "../test/test262")
const manifest = (await Bun.file(path.join(root, "manifest.json")).json()) as {
revision: string
directories: Array<string>
harness: Array<string>
flags: Array<string>
features: Array<string>
boundaries: Record<string, string>
}
const boundaries = Object.entries(manifest.boundaries).map(([name, pattern]) => [name, new RegExp(pattern)] as const)
const checkout = process.argv[2]
if (checkout === undefined) {
console.error("usage: bun run script/sync-test262.ts /path/to/test262")
process.exit(1)
}
const head = (await Bun.$`git -C ${checkout} rev-parse HEAD`.text()).trim()
if (head !== manifest.revision) {
console.error(`checkout is at ${head}; manifest pins ${manifest.revision}`)
process.exit(1)
}
const excluded = new Map<string, number>()
let copied = 0
for (const dir of manifest.directories) {
await rm(path.join(root, dir), { recursive: true, force: true })
const from = path.join(checkout, "test", dir)
for await (const file of new Bun.Glob("**/*.js").scan({ cwd: from })) {
if (file.endsWith("_FIXTURE.js")) continue
const source = await Bun.file(path.join(from, file)).text()
const start = source.indexOf("/*---")
const end = source.indexOf("---*/", start)
const meta = start === -1 ? {} : (Bun.YAML.parse(source.slice(start + 5, end)) as Frontmatter)
const code = start === -1 ? source : source.slice(end + 5)
const reason =
meta.flags?.find((flag) => manifest.flags.includes(flag)) ??
meta.features?.find((feature) => manifest.features.includes(feature)) ??
meta.includes?.find((include) => !manifest.harness.includes(include)) ??
boundaries.find(([, pattern]) => pattern.test(code))?.[0]
if (reason !== undefined) {
excluded.set(reason, (excluded.get(reason) ?? 0) + 1)
continue
}
await Bun.write(path.join(root, dir, file), Bun.file(path.join(from, file)))
copied++
}
}
console.log(`copied ${copied} files`)
for (const [reason, count] of [...excluded].sort((a, b) => b[1] - a[1])) {
console.log(` excluded ${String(count).padStart(5)} ${reason}`)
}
@@ -0,0 +1,55 @@
// Runs every vendored test262 file, including skipped ones, and groups failures by cause. Pass
// --write to regenerate test/test262/skipped.txt from the current failures.
//
// Usage: bun run script/test262-report.ts [--write] [path-prefix]
import path from "node:path"
import { root, run, skipped } from "../test/test262/run.js"
const write = process.argv.includes("--write")
const prefix = process.argv.slice(2).find((arg) => !arg.startsWith("--")) ?? ""
const files = [...new Bun.Glob("**/*.js").scanSync({ cwd: root })].filter((file) => file.startsWith(prefix)).sort()
const failures: Array<{ file: string; reason: string }> = []
const recovered: Array<string> = []
for (const file of files) {
const outcome = await run(file)
if (outcome.status === "fail") failures.push({ file, reason: outcome.reason })
if (outcome.status === "pass" && skipped.has(file)) recovered.push(file)
}
// Collapse a reason to the part that identifies the cause rather than the test.
const bucket = (reason: string) => {
const syntax = reason.match(/Syntax '([A-Za-z]+)' is not supported/)
if (syntax) return `unsupported syntax ${syntax[1]}`
if (reason.startsWith("expected ")) return reason.replace(/ but got .*/, " but the program ran")
return reason
.replace(/^(\$DONE: |ExecutionFailure: |InvalidDataValue: |ParseError: |Uncaught: |Test262Error: |Error: )+/, "")
.replace(/ \(line \d+, col \d+\)/, "")
.replace(/^[\w$]+\.(\w+) is not a function/, ".$1 is not a function")
.replace(/^[\w$]+ cannot be constructed/, "… cannot be constructed")
.replace(/'[^']*'/g, "'…'")
.slice(0, 100)
}
const buckets = new Map<string, Array<string>>()
for (const failure of failures) {
const key = bucket(failure.reason)
buckets.set(key, [...(buckets.get(key) ?? []), failure.file])
}
console.log(`${files.length - failures.length} pass, ${failures.length} fail of ${files.length}\n`)
for (const [key, list] of [...buckets].sort((a, b) => b[1].length - a[1].length)) {
console.log(`${String(list.length).padStart(5)} ${key}`)
for (const file of list.slice(0, 3)) console.log(` ${file}`)
if (list.length > 3) console.log(`${list.length - 3} more`)
}
if (recovered.length > 0) {
console.log(`\n${recovered.length} skipped files pass now; remove them from skipped.txt:`)
for (const file of recovered) console.log(` ${file}`)
}
if (write) {
const lines = failures.map((failure) => `${failure.file} # ${bucket(failure.reason)}`)
await Bun.write(path.join(root, "skipped.txt"), `${lines.join("\n")}\n`)
console.log(`\nwrote ${lines.length} entries to skipped.txt`)
}
+10 -1
View File
@@ -7,6 +7,7 @@ import type { DataValue, Diagnostic, ResolvedExecutionLimits, Result } from "../
import { toData } from "../data.js"
import { ToolRuntime } from "../tool-runtime.js"
import { normalizeError } from "./errors.js"
import type { Host } from "./globals.js"
import { InterpreterRuntimeError } from "./model.js"
import { PromiseRuntime } from "./promises.js"
import { Runtime } from "./runtime.js"
@@ -16,6 +17,7 @@ export const executeProgram = <R>(
prepared: ToolRuntime.Prepared<R>,
limits: ResolvedExecutionLimits,
hooks: ToolRuntime.ToolCallHooks<R>,
extraGlobals?: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]>,
): Effect.Effect<Result, never, R> => {
if (code.trim().length === 0) {
return Effect.succeed({
@@ -39,7 +41,14 @@ export const executeProgram = <R>(
Effect.gen(function* () {
const program = parseProgram(code)
const promises = new PromiseRuntime<R>(scope)
const value = yield* new Runtime<R>(tools.execute, tools.search, tools.keys, promises, logs).run(program)
const value = yield* new Runtime<R>(
tools.execute,
tools.search,
tools.keys,
promises,
logs,
extraGlobals,
).run(program)
const result = toData(value, "Execution result", "result") as DataValue
returned = { value: result, promises }
const warnings = yield* promises.interrupt()
+3 -2
View File
@@ -66,7 +66,7 @@ import {
unsupportedSyntax,
} from "./model.js"
import { caughtErrorValue } from "./errors.js"
import { globals } from "./globals.js"
import { globals, type Host } from "./globals.js"
import { HostFunction, HostNamespace } from "./host.js"
import { invokeIntrinsic } from "./methods.js"
import { preserveConsumerError, type Runner } from "./runner.js"
@@ -285,6 +285,7 @@ export class Runtime<R> {
readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
readonly promises: PromiseRuntime<R>,
readonly logs: Array<string> = [],
extraGlobals: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]> = () => [],
) {
const globalScope = new Map<string, Binding>()
// Calling back into the program never reads frame state, so any frame serves; the root is always alive.
@@ -295,7 +296,7 @@ export class Runtime<R> {
settlePromise: (promise) => this.root.settlePromise(promise),
syncIterator: (value, node) => this.root.syncIterator(value, node),
}
this.builtins = new Map(globals(this))
this.builtins = new Map([...globals(this), ...extraGlobals(this)])
for (const [name, value] of this.builtins) globalScope.set(name, { mutable: false, value })
}
+20
View File
@@ -0,0 +1,20 @@
/*
* Runs the test262 files vendored under test/test262 (see test/test262/README.md) verbatim.
* Files listed in test/test262/skipped.txt fail on a known interpreter gap and are skipped;
* `bun run script/test262-report.ts` shows current gaps and which skipped files pass again.
* Licensed under test/LICENSE.test262.
*/
import { test } from "bun:test"
import { root, run, skipped } from "./test262/run.js"
for (const file of [...new Bun.Glob("**/*.js").scanSync({ cwd: root })].sort()) {
const define = skipped.has(file) ? test.skip : test
define(
file,
async () => {
const outcome = await run(file)
if (outcome.status === "fail") throw new Error(outcome.reason)
},
10_000,
)
}
@@ -0,0 +1,3 @@
# Upstream test262 files are populated locally by script/sync-test262.ts and not committed yet.
built-ins/
language/
+41
View File
@@ -0,0 +1,41 @@
# test262
Runs upstream [test262](https://github.com/tc39/test262) files verbatim through `test/test262.test.ts`. The files
themselves are not committed yet: populate them from a local checkout with
```sh
git clone https://github.com/tc39/test262 ~/development/test262
bun run script/sync-test262.ts ~/development/test262
```
Without them the runner registers no tests, so CI is unaffected. Licensed under `test/LICENSE.test262`.
## Layout
- `manifest.json` — the pinned upstream revision, which upstream directories are copied, and what is left out.
- `built-ins/`, `language/` — the copied files, mirroring upstream `test/`; gitignored.
- `skipped.txt` — vendored files that fail on a known interpreter gap, one `path # reason` per line. They are
skipped, and each gap is listed as unchecked in `interpreter-support.md`.
- `run.ts` — runs one file: prepends `"use strict"`, provides the harness (`assert`, `Test262Error`,
`compareArray`, `$DONE`, `$DONOTEVALUATE`) as host globals, and interprets the file's frontmatter (`negative`,
`flags: [async]`).
## What is not vendored
`script/sync-test262.ts` skips a file when its frontmatter declares a `flags`, `features`, or `includes` value the
manifest marks unsupported, or when its code matches one of the manifest's `boundaries` patterns. The sync checks the
checkout is at the pinned revision, so every machine runs the same files. Boundaries are
intentional limits of the interpreter, not compatibility work: classes, `this`, `arguments`, prototype objects,
property descriptors, accessors, boxed primitives, sloppy mode, `eval`, `Symbol()`, and the `$262` host API. If one
of those decisions changes, delete its entry and re-sync; the tests are upstream, not lost.
## Commands
```sh
bun run script/sync-test262.ts /path/to/test262 # re-copy at the pinned revision; edit manifest.json to change scope
bun run script/test262-report.ts [--write] [dir] # run everything, group failures by cause; --write regenerates skipped.txt
bun test test/test262.test.ts # what CI runs
```
When a fix makes skipped files pass, the report lists them so they can be removed from `skipped.txt`, and the
matching gap in `interpreter-support.md` is checked in the same change.
@@ -0,0 +1,136 @@
{
"revision": "250f204f23a9249ff204be2baec29600faae7b75",
"directories": [
"built-ins/Array",
"built-ins/Object",
"built-ins/JSON",
"built-ins/String",
"built-ins/Number",
"built-ins/Math",
"built-ins/Boolean",
"built-ins/Date",
"built-ins/Map",
"built-ins/Set",
"built-ins/Promise",
"built-ins/RegExp",
"built-ins/Error",
"built-ins/NativeErrors",
"built-ins/AggregateError",
"built-ins/parseInt",
"built-ins/parseFloat",
"built-ins/isNaN",
"built-ins/isFinite",
"built-ins/encodeURI",
"built-ins/encodeURIComponent",
"built-ins/decodeURI",
"built-ins/decodeURIComponent",
"built-ins/undefined",
"built-ins/NaN",
"built-ins/Infinity",
"language/statements",
"language/expressions"
],
"harness": ["assert.js", "sta.js", "compareArray.js", "doneprintHandle.js"],
"flags": ["module", "raw", "noStrict"],
"boundaries": {
"class": "\\bclass\\s*[A-Za-z_${]",
"this": "\\bthis\\b",
"arguments": "\\barguments\\b",
"call/apply/bind": "\\.(call|apply|bind)\\s*\\(",
"prototype objects": "\\.prototype\\b",
"accessor properties": "\\b(get|set)\\s+[\\w$\\[][^\\n(]*\\(",
"property descriptors": "Object\\.(defineProperty|defineProperties|getOwnPropertyDescriptors?|getOwnPropertyNames|create|getPrototypeOf|setPrototypeOf|freeze|seal|preventExtensions|isFrozen|isSealed|isExtensible)\\b",
"boxed primitives": "\\bnew\\s+(String|Number|Boolean)\\s*\\(|\\bObject\\s*\\(\\s*(-?\\d|[\"'`]|true\\b|false\\b|NaN\\b|Infinity\\b)",
"sloppy mode": "\\bwith\\s*\\(",
"eval": "\\b(eval|Function)\\b",
"new.target": "\\bnew\\.target\\b",
"globalThis": "\\bglobalThis\\b",
"Symbol": "\\bSymbol\\b(?!\\.(iterator|asyncIterator)\\b)",
"$262 host API": "\\$262"
},
"features": [
"BigInt",
"Proxy",
"proxy-missing-checks",
"Reflect",
"Reflect.construct",
"Reflect.set",
"Reflect.setPrototypeOf",
"Symbol",
"Symbol.hasInstance",
"Symbol.isConcatSpreadable",
"Symbol.match",
"Symbol.matchAll",
"Symbol.prototype.description",
"Symbol.replace",
"Symbol.search",
"Symbol.species",
"Symbol.split",
"Symbol.toPrimitive",
"Symbol.toStringTag",
"Symbol.unscopables",
"symbols-as-weakmap-keys",
"class",
"class-fields-private",
"class-fields-private-in",
"class-fields-public",
"class-methods-private",
"class-static-block",
"class-static-fields-private",
"class-static-fields-public",
"class-static-methods-private",
"super",
"new.target",
"decorators",
"TypedArray",
"TypedArray.prototype.at",
"ArrayBuffer",
"SharedArrayBuffer",
"DataView",
"Atomics",
"Atomics.pause",
"Atomics.waitAsync",
"resizable-arraybuffer",
"arraybuffer-transfer",
"immutable-arraybuffer",
"Float16Array",
"Float32Array",
"Float64Array",
"Int8Array",
"Int16Array",
"Int32Array",
"Uint8Array",
"Uint8ClampedArray",
"Uint16Array",
"Uint32Array",
"uint8array-base64",
"WeakMap",
"WeakSet",
"WeakRef",
"FinalizationRegistry",
"Intl-enumeration",
"Temporal",
"ShadowRealm",
"cross-realm",
"dynamic-import",
"import.meta",
"import-attributes",
"import-defer",
"export-defer",
"source-phase-imports",
"source-phase-imports-module-source",
"import-text",
"import-bytes",
"json-modules",
"top-level-await",
"explicit-resource-management",
"tail-call-optimization",
"caller",
"IsHTMLDDA",
"host-gc-required",
"legacy-regexp",
"__proto__",
"__getter__",
"__setter__"
]
}
+163
View File
@@ -0,0 +1,163 @@
// Runs one vendored test262 file verbatim. The harness (assert, Test262Error, compareArray, $DONE,
// $DONOTEVALUATE) is host-provided because programs cannot attach properties to functions, which
// test262's own assert.js relies on.
import path from "node:path"
import { Cause, Effect } from "effect"
import { caughtErrorValue } from "../../src/interpreter/errors.js"
import { executeProgram } from "../../src/interpreter/execute.js"
import type { Host } from "../../src/interpreter/globals.js"
import { HostFunction } from "../../src/interpreter/host.js"
import { CodeModeFunction, ProgramThrow } from "../../src/interpreter/model.js"
import { createErrorValue, errorBrandName } from "../../src/stdlib/value.js"
import { ToolRuntime } from "../../src/tool-runtime.js"
export const root = import.meta.dir
/** Files that fail on a known gap, mapped to the gap; see skipped.txt. */
export const skipped = new Map(
(await Bun.file(path.join(root, "skipped.txt")).text())
.split("\n")
.filter((line) => line.includes("#"))
.map((line) => [line.slice(0, line.indexOf("#")).trim(), line.slice(line.indexOf("#") + 1).trim()]),
)
export type Outcome = { readonly status: "pass" } | { readonly status: "fail"; readonly reason: string }
type Frontmatter = {
flags?: Array<string>
negative?: { phase: "parse" | "resolution" | "runtime"; type: string }
}
const limits = { timeoutMs: 5000, maxToolCalls: undefined, maxOutputBytes: undefined }
const prepared = ToolRuntime.prepare<never>({})
export const run = async (file: string): Promise<Outcome> => {
const source = await Bun.file(path.join(root, file)).text()
const start = source.indexOf("/*---")
const meta = Bun.YAML.parse(source.slice(start + 5, source.indexOf("---*/", start))) as Frontmatter
let done: { error: unknown } | undefined
// Sloppy-only tests are never vendored, so every file runs as the strict half of test262's two-mode run.
// A host drains the job queue after an async test's script ends; the program end interrupts un-awaited
// work instead, so drain explicitly.
const drain = meta.flags?.includes("async") ? "\nfor (let i = 0; i < 100; i++) await null" : ""
const result = await Effect.runPromise(
executeProgram(`"use strict";\n${source}${drain}`, prepared, limits, {}, (host) =>
harness(host, (error) => {
done ??= { error }
}),
),
)
if (meta.negative !== undefined) {
// Runtime negatives only check that execution failed: diagnostics do not carry the error name.
if (result.ok) return { status: "fail", reason: `expected ${meta.negative.type} but completed` }
if (meta.negative.phase === "runtime" || result.error.kind === "ParseError") return { status: "pass" }
return {
status: "fail",
reason: `expected ${meta.negative.type} but got ${result.error.kind}: ${result.error.message}`,
}
}
if (!result.ok) return { status: "fail", reason: `${result.error.kind}: ${result.error.message}` }
if (!meta.flags?.includes("async")) return { status: "pass" }
if (done === undefined) return { status: "fail", reason: "$DONE was never called" }
if (done.error !== undefined) return { status: "fail", reason: `$DONE: ${show(done.error)}` }
return { status: "pass" }
}
const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyArray<readonly [string, unknown]> => {
const fail = (message: string) => Effect.fail(new ProgramThrow(createErrorValue("Test262Error", message)))
const prefix = (message: unknown) => (message === undefined ? "" : `${String(message)} `)
const compare = (a: unknown, b: unknown) =>
Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => Object.is(value, b[i]))
const test262Error = new HostFunction<R>({
name: "Test262Error",
call: (args) => Effect.succeed(createErrorValue("Test262Error", args[0] === undefined ? "" : String(args[0]))),
construct: (args) => Effect.succeed(createErrorValue("Test262Error", args[0] === undefined ? "" : String(args[0]))),
instanceOf: (value) => errorBrandName(value) === "Test262Error",
members: { thrower: new HostFunction<R>({ name: "Test262Error.thrower", call: (args) => fail(String(args[0])) }) },
})
const compareArray = new HostFunction<R>({
name: "compareArray",
call: (args) => Effect.succeed(compare(args[0], args[1])),
members: {
format: new HostFunction<R>({ name: "compareArray.format", call: (args) => Effect.succeed(show(args[0])) }),
},
})
const assert = new HostFunction<R>({
name: "assert",
call: (args) =>
args[0] === true
? Effect.void
: fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])),
members: {
sameValue: new HostFunction<R>({
name: "assert.sameValue",
call: (args) =>
Object.is(args[0], args[1])
? Effect.void
: fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`),
}),
notSameValue: new HostFunction<R>({
name: "assert.notSameValue",
call: (args) =>
Object.is(args[0], args[1])
? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`)
: Effect.void,
}),
compareArray: new HostFunction<R>({
name: "assert.compareArray",
call: (args) =>
compare(args[0], args[1])
? Effect.void
: fail(
`Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`,
),
}),
throws: new HostFunction<R>({
name: "assert.throws",
call: (args, node) => {
const expected = args[0] instanceof HostFunction ? args[0].name : show(args[0])
return host.runner.invokeCallable(args[1], [], node).pipe(
Effect.matchCauseEffect({
onFailure: (cause) => {
if (cause.reasons.some(Cause.isInterruptReason)) return Effect.failCause(cause)
const thrown = caughtErrorValue(Cause.squash(cause))
if (thrown === null || typeof thrown !== "object") {
return fail(`${prefix(args[2])}Thrown value was not an object!`)
}
const actual = errorBrandName(thrown)
if (actual === expected) return Effect.void
return fail(`${prefix(args[2])}Expected a ${expected} but got a ${actual ?? "non-error object"}`)
},
onSuccess: () =>
fail(`${prefix(args[2])}Expected a ${expected} to be thrown but no exception was thrown at all`),
}),
)
},
}),
},
})
return [
["assert", assert],
["compareArray", compareArray],
["Test262Error", test262Error],
["$DONE", new HostFunction<R>({ name: "$DONE", call: (args) => Effect.sync(() => onDone(args[0])) })],
[
"$DONOTEVALUATE",
new HostFunction<R>({
name: "$DONOTEVALUATE",
call: () => Effect.fail(new ProgramThrow("Test262: This statement should not be evaluated.")),
}),
],
]
}
const show = (value: unknown): string => {
if (typeof value === "string") return JSON.stringify(value)
if (Object.is(value, -0)) return "-0"
if (Array.isArray(value)) return `[${value.map(show).join(", ")}]`
if (value instanceof HostFunction) return value.name
if (value instanceof CodeModeFunction) return "program function"
if (value === null || typeof value !== "object") return String(value)
const message = (value as { message?: unknown }).message
return typeof message === "string" ? `${errorBrandName(value) ?? "object"}: ${message}` : "object"
}
File diff suppressed because it is too large Load Diff