mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-12 11:56:23 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda840e181 | ||
|
|
4c34ee5eab |
@@ -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.
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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/
|
||||
@@ -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__"
|
||||
]
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user