mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-10 10:56:22 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a47cc022 | ||
|
|
f3ef84556a | ||
|
|
08ff21179c | ||
|
|
98a36fb1a4 | ||
|
|
e22cd0a585 | ||
|
|
8475783700 | ||
|
|
1452aadc87 | ||
|
|
1417976257 |
@@ -44,18 +44,21 @@ ultimate source of truth.
|
||||
|
||||
## Bindings and destructuring
|
||||
|
||||
- [x] `const`, `let`, and accepted `var` declarations.
|
||||
- [x] `const`, `let`, and `var` declarations.
|
||||
- [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings.
|
||||
- [x] Nested patterns, defaults, elisions, and rest elements.
|
||||
- [x] Assignment to identifiers, plain-object fields, non-negative integer array indexes, and writable URL
|
||||
fields.
|
||||
- [x] Direct function declarations are hoisted in program and block statement lists.
|
||||
- [x] Parameter defaults observe a temporal dead zone for later parameters.
|
||||
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
|
||||
- [x] `var` is function-scoped and hoisted: names declared anywhere in a function or program body, including loop
|
||||
heads, blocks, `switch` cases, and `try`/`catch`, read as `undefined` before their statement runs; redeclaration
|
||||
assigns the one binding; a same-named parameter keeps its argument; closures in parameter defaults see outer
|
||||
names rather than body `var`s.
|
||||
- [x] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
|
||||
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
|
||||
temporal dead zone.
|
||||
- [ ] Hoist function declarations accepted directly in switch cases.
|
||||
- [x] Function declarations are hoisted across all cases of a `switch`, like any other statement list.
|
||||
- [x] Computed object destructuring keys such as `const { [field]: value } = record`.
|
||||
- [x] Object destructuring from arrays, such as `const { length } = values`.
|
||||
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
|
||||
@@ -250,12 +253,13 @@ ultimate source of truth.
|
||||
## Strings
|
||||
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
|
||||
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`.
|
||||
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
|
||||
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
|
||||
- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
|
||||
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
|
||||
- [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`.
|
||||
- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
|
||||
- [x] `localeCompare`; locale and options arguments are currently ignored.
|
||||
- [x] `isWellFormed` and `toWellFormed`.
|
||||
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
|
||||
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
|
||||
- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like
|
||||
@@ -316,6 +320,7 @@ ultimate source of truth.
|
||||
- [x] Local and UTC Date setters, including native argument coercion, mutation, rollover, invalid-Date recovery, and
|
||||
`TimeClip` behavior.
|
||||
- [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.
|
||||
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
|
||||
representation for the string primitive.
|
||||
@@ -359,6 +364,17 @@ ultimate source of truth.
|
||||
`entries`, `toString`, and `size`.
|
||||
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
|
||||
|
||||
## Web platform helpers
|
||||
|
||||
- [x] `atob` and `btoa` with forgiving-base64 decoding and WebIDL string conversion; invalid input throws an Error
|
||||
named `InvalidCharacterError`, since there is no `DOMException`.
|
||||
- [x] `crypto.randomUUID()`.
|
||||
- [x] `structuredClone` over the data model: objects, arrays with holes, Date, RegExp (`lastIndex` reset), Map, Set,
|
||||
URL, URLSearchParams, and Errors (name, message, and cause only); shared references stay shared within one
|
||||
clone; functions, promises, and tool references throw an Error named `DataCloneError`.
|
||||
- [ ] `crypto.getRandomValues` and `crypto.subtle`, `TextEncoder`/`TextDecoder`, and `Blob`: these need a binary
|
||||
value type, which the JSON-like data model does not have yet.
|
||||
|
||||
## Errors and diagnostics
|
||||
|
||||
- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
|
||||
@@ -375,6 +391,6 @@ ultimate source of truth.
|
||||
shift them. The diagnostic names the rejected node type and attaches a short orientation to the supported
|
||||
subset; this matrix is the full reference.
|
||||
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
|
||||
- [ ] Distinguish user-thrown failures from interpreter defects and explicit tool refusals from internal tool
|
||||
failures; preserve those categories in caught errors, promise rejection handlers, and `Promise.allSettled`
|
||||
reasons.
|
||||
- [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.
|
||||
|
||||
@@ -105,7 +105,7 @@ export type Result = typeof Result.Type
|
||||
|
||||
/** Reusable confined runtime over explicit tools. */
|
||||
export type Runtime<R = never> = {
|
||||
readonly catalog: () => ReadonlyArray<ToolDescription>
|
||||
readonly catalog: ReadonlyArray<ToolDescription>
|
||||
readonly execute: (code: string) => Effect.Effect<Result, never, R>
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export const make = <const Provided extends Record<string, unknown> = {}>(
|
||||
const prepared = ToolRuntime.prepare((options.tools ?? {}) as Tools<Services<Provided>>)
|
||||
const limits = resolveExecutionLimits(options.limits)
|
||||
return {
|
||||
catalog: () => prepared.catalog,
|
||||
catalog: prepared.catalog,
|
||||
execute: (code) => executeProgram(code, prepared, limits, options),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,6 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
|
||||
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object or
|
||||
// array never reaches the Object.prototype setter.
|
||||
const define = (target: object, key: string, value: unknown): void => {
|
||||
export const define = (target: object, key: string, value: unknown): void => {
|
||||
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { regexpGlobal } from "../stdlib/regexp.js"
|
||||
import { stringGlobal } from "../stdlib/string.js"
|
||||
import { uriGlobal, urlGlobal, urlSearchParamsGlobal } from "../stdlib/url.js"
|
||||
import { coercion, errorConstructors } from "../stdlib/value.js"
|
||||
import { atobGlobal, btoaGlobal, cryptoGlobal, structuredCloneGlobal } from "../stdlib/web.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { errorGlobal } from "./errors.js"
|
||||
import { HostFunction } from "./host.js"
|
||||
@@ -71,5 +72,9 @@ export const globals = <R>(host: Host<R>): ReadonlyArray<readonly [string, unkno
|
||||
["encodeURIComponent", uriGlobal("encodeURIComponent")],
|
||||
["decodeURI", uriGlobal("decodeURI")],
|
||||
["decodeURIComponent", uriGlobal("decodeURIComponent")],
|
||||
["atob", atobGlobal],
|
||||
["btoa", btoaGlobal],
|
||||
["crypto", cryptoGlobal],
|
||||
["structuredClone", structuredCloneGlobal],
|
||||
...[...errorConstructors].map((name) => [name, errorGlobal(name, host.runner)] as const),
|
||||
]
|
||||
|
||||
@@ -118,9 +118,11 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
|
||||
result = value.trim()
|
||||
break
|
||||
case "trimStart":
|
||||
case "trimLeft":
|
||||
result = value.trimStart()
|
||||
break
|
||||
case "trimEnd":
|
||||
case "trimRight":
|
||||
result = value.trimEnd()
|
||||
break
|
||||
// Locale/options are deliberately unsupported; comparison uses the host default locale.
|
||||
@@ -241,6 +243,15 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
|
||||
case "substring":
|
||||
result = value.substring(optNum(0) ?? 0, optNum(1))
|
||||
break
|
||||
case "substr":
|
||||
result = value.substr(optNum(0) ?? 0, optNum(1))
|
||||
break
|
||||
case "isWellFormed":
|
||||
result = value.isWellFormed()
|
||||
break
|
||||
case "toWellFormed":
|
||||
result = value.toWellFormed()
|
||||
break
|
||||
case "charCodeAt":
|
||||
result = value.charCodeAt(optNum(0) ?? 0)
|
||||
break
|
||||
|
||||
@@ -32,6 +32,17 @@ export class PromiseRuntime<R> {
|
||||
|
||||
constructor(private readonly scope: Scope.Scope) {}
|
||||
|
||||
// Resolution bodies need the promise's own identity to reject `resolve(promise)` self-resolution.
|
||||
createWithSelf(
|
||||
body: (self: { promise?: Values.Promise }) => Effect.Effect<unknown, unknown, R>,
|
||||
): Effect.Effect<Values.Promise, never, R> {
|
||||
const self: { promise?: Values.Promise } = {}
|
||||
return Effect.map(this.create(body(self)), (promise) => {
|
||||
self.promise = promise
|
||||
return promise
|
||||
})
|
||||
}
|
||||
|
||||
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<Values.Promise, never, R> {
|
||||
return Effect.suspend(() => {
|
||||
// Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
|
||||
@@ -126,11 +137,7 @@ export const resolvePromise = <R>(
|
||||
node: AstNode,
|
||||
): Effect.Effect<Values.Promise, never, R> => {
|
||||
if (value instanceof Values.Promise) return Effect.succeed(value)
|
||||
const box: { promise?: Values.Promise } = {}
|
||||
return Effect.map(promises.create(resolvePromiseValue(runner, value, node, box)), (promise) => {
|
||||
box.promise = promise
|
||||
return promise
|
||||
})
|
||||
return promises.createWithSelf((self) => resolvePromiseValue(runner, value, node, self))
|
||||
}
|
||||
|
||||
const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"] as const
|
||||
@@ -254,11 +261,9 @@ const constructPromise = <R>(
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const box: { promise?: Values.Promise } = {}
|
||||
const promise = yield* promises.create(
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, node, box)),
|
||||
const promise = yield* promises.createWithSelf((self) =>
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, node, self)),
|
||||
)
|
||||
box.promise = promise
|
||||
const resolve = capability("resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)))
|
||||
const reject = capability("reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value))))
|
||||
const executed = yield* Effect.exit(runner.invokeFunction(executor, [resolve, reject]))
|
||||
@@ -310,19 +315,16 @@ const chainReaction = <R>(
|
||||
method: string,
|
||||
node: AstNode,
|
||||
): Effect.Effect<Values.Promise, never, R> => {
|
||||
const box: { promise?: Values.Promise } = {}
|
||||
const body = Effect.gen(function* () {
|
||||
const exit = yield* reactionExit(promises, source)
|
||||
const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
|
||||
if (handler === undefined) return yield* exit
|
||||
const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause))
|
||||
const result = yield* applyCollectionCallback(runner, handler, method, node)([input])
|
||||
return yield* resolvePromiseValue(runner, result, node, box)
|
||||
})
|
||||
return Effect.map(promises.create(body), (derived) => {
|
||||
box.promise = derived
|
||||
return derived
|
||||
})
|
||||
return promises.createWithSelf((self) =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* reactionExit(promises, source)
|
||||
const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
|
||||
if (handler === undefined) return yield* exit
|
||||
const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause))
|
||||
const result = yield* applyCollectionCallback(runner, handler, method, node)([input])
|
||||
return yield* resolvePromiseValue(runner, result, node, self)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const chainFinally = <R>(
|
||||
|
||||
@@ -90,6 +90,18 @@ import { enumerableSource } from "../stdlib/object.js"
|
||||
import { coerceToNumber, coerceToString, compoundOperators, errorBrandName } from "../stdlib/value.js"
|
||||
import { Values } from "../values.js"
|
||||
|
||||
// What a loop does with its body's result: exit with a StatementResult, or undefined to keep iterating.
|
||||
// Unlabelled break ends this loop; a label the loop does not carry propagates outward.
|
||||
const loopExit = (result: StatementResult, labels: ReadonlySet<string> | undefined): StatementResult | undefined => {
|
||||
if (result.kind === "return") return result
|
||||
if (result.kind === "break") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" }
|
||||
}
|
||||
if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return undefined
|
||||
}
|
||||
|
||||
const calleeDescription = (callee: Expression | Super | undefined): string => {
|
||||
if (callee?.type === "Identifier") return callee.name
|
||||
if (callee?.type === "MemberExpression") {
|
||||
@@ -158,6 +170,51 @@ const collectPatternNames = (pattern: Pattern, out: Array<string> = []): Array<s
|
||||
return out
|
||||
}
|
||||
|
||||
// `var` names declared anywhere in a function body except inside nested functions, which own theirs.
|
||||
// Memoized per body: a function's var names never change, and hoisting runs on every call.
|
||||
const varNames = new WeakMap<ReadonlyArray<Statement | ModuleDeclaration>, ReadonlyArray<string>>()
|
||||
const collectVarNames = (
|
||||
node: Statement | ModuleDeclaration | null | undefined,
|
||||
out: Array<string> = [],
|
||||
): Array<string> => {
|
||||
if (!node) return out
|
||||
switch (node.type) {
|
||||
case "VariableDeclaration":
|
||||
if (node.kind === "var") for (const declaration of node.declarations) collectPatternNames(declaration.id, out)
|
||||
break
|
||||
case "BlockStatement":
|
||||
for (const statement of node.body) collectVarNames(statement, out)
|
||||
break
|
||||
case "IfStatement":
|
||||
collectVarNames(node.consequent, out)
|
||||
collectVarNames(node.alternate, out)
|
||||
break
|
||||
case "ForStatement":
|
||||
if (node.init?.type === "VariableDeclaration") collectVarNames(node.init, out)
|
||||
collectVarNames(node.body, out)
|
||||
break
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
if (node.left.type === "VariableDeclaration") collectVarNames(node.left, out)
|
||||
collectVarNames(node.body, out)
|
||||
break
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "LabeledStatement":
|
||||
collectVarNames(node.body, out)
|
||||
break
|
||||
case "SwitchStatement":
|
||||
for (const item of node.cases) for (const statement of item.consequent) collectVarNames(statement, out)
|
||||
break
|
||||
case "TryStatement":
|
||||
collectVarNames(node.block, out)
|
||||
collectVarNames(node.handler?.body, out)
|
||||
collectVarNames(node.finalizer, out)
|
||||
break
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...of" | "for...in") => {
|
||||
if (left.type !== "VariableDeclaration") return undefined
|
||||
const declaration = left.declarations.length === 1 ? left.declarations[0] : undefined
|
||||
@@ -264,6 +321,7 @@ class Frame<R> {
|
||||
return Effect.gen(function* () {
|
||||
self.predeclareLexical(program.body)
|
||||
self.hoistFunctions(program.body)
|
||||
self.hoistVars(program.body)
|
||||
let value: unknown = undefined
|
||||
for (const [index, statement] of program.body.entries()) {
|
||||
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
|
||||
@@ -387,6 +445,20 @@ class Frame<R> {
|
||||
}
|
||||
}
|
||||
|
||||
// Hoisted `var` bindings start undefined, or copy a same-named parameter. Function bodies hoist
|
||||
// into their own scope above the parameters so closures in parameter defaults keep seeing outer names.
|
||||
private hoistVars(statements: ReadonlyArray<Statement | ModuleDeclaration>, parameters?: Map<string, Binding>): void {
|
||||
const names =
|
||||
varNames.get(statements) ??
|
||||
statements.reduce<Array<string>>((out, statement) => collectVarNames(statement, out), [])
|
||||
varNames.set(statements, names)
|
||||
const scope = this.scopes.current()
|
||||
for (const name of names) {
|
||||
if (scope.has(name)) continue
|
||||
scope.set(name, { mutable: true, value: parameters?.get(name)?.value, initialized: true })
|
||||
}
|
||||
}
|
||||
|
||||
private predeclareLexical(statements: ReadonlyArray<Statement | ModuleDeclaration>): void {
|
||||
for (const statement of statements) {
|
||||
if (statement.type !== "VariableDeclaration") continue
|
||||
@@ -424,7 +496,9 @@ class Frame<R> {
|
||||
self.scopes.push()
|
||||
return yield* Effect.gen(function* () {
|
||||
const cases = node.cases
|
||||
self.predeclareLexical(cases.flatMap((branch) => branch.consequent))
|
||||
const statements = cases.flatMap((branch) => branch.consequent)
|
||||
self.predeclareLexical(statements)
|
||||
self.hoistFunctions(statements)
|
||||
let defaultIndex: number | undefined
|
||||
let selected: number | undefined
|
||||
for (const [index, branch] of cases.entries()) {
|
||||
@@ -466,21 +540,8 @@ class Frame<R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
while (yield* self.evaluateExpression(node.test)) {
|
||||
const result = yield* self.evaluateStatement(node.body)
|
||||
|
||||
if (result.kind === "continue") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
continue
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "return") {
|
||||
return result
|
||||
}
|
||||
const exit = loopExit(yield* self.evaluateStatement(node.body), labels)
|
||||
if (exit !== undefined) return exit
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -494,21 +555,8 @@ class Frame<R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
do {
|
||||
const result = yield* self.evaluateStatement(node.body)
|
||||
|
||||
if (result.kind === "continue") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
continue
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "return") {
|
||||
return result
|
||||
}
|
||||
const exit = loopExit(yield* self.evaluateStatement(node.body), labels)
|
||||
if (exit !== undefined) return exit
|
||||
} while (yield* self.evaluateExpression(node.test))
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -554,27 +602,13 @@ class Frame<R> {
|
||||
nextIteration()
|
||||
|
||||
while (testNode ? yield* self.evaluateExpression(testNode) : true) {
|
||||
const result = yield* self.evaluateStatement(node.body)
|
||||
|
||||
if (result.kind === "return") {
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result
|
||||
const exit = loopExit(yield* self.evaluateStatement(node.body), labels)
|
||||
if (exit !== undefined) return exit
|
||||
|
||||
nextIteration()
|
||||
if (updateNode) {
|
||||
yield* self.evaluateExpression(updateNode)
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -617,10 +651,12 @@ class Frame<R> {
|
||||
|
||||
const evaluateBody = (value: unknown) =>
|
||||
Effect.gen(function* () {
|
||||
if (declared) {
|
||||
if (declared?.lexical) {
|
||||
self.scopes.push()
|
||||
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical)
|
||||
self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, true)
|
||||
} else if (declared) {
|
||||
yield* self.assignPattern(declared.pattern, value, left)
|
||||
} else if (assignment) {
|
||||
yield* self.assignPattern(assignment, value, left)
|
||||
}
|
||||
@@ -628,7 +664,7 @@ class Frame<R> {
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared) self.scopes.pop()
|
||||
if (declared?.lexical) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -647,22 +683,10 @@ class Frame<R> {
|
||||
}
|
||||
return yield* Effect.failCause(bodyExit.cause)
|
||||
}
|
||||
const result = bodyExit.value
|
||||
|
||||
if (result.kind === "return") {
|
||||
const exit = loopExit(bodyExit.value, labels)
|
||||
if (exit !== undefined) {
|
||||
yield* close()
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
yield* close()
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) {
|
||||
yield* close()
|
||||
return result
|
||||
return exit
|
||||
}
|
||||
}
|
||||
}).pipe(
|
||||
@@ -874,10 +898,12 @@ class Frame<R> {
|
||||
|
||||
for (const key of keys) {
|
||||
const result = yield* Effect.gen(function* () {
|
||||
if (declared) {
|
||||
if (declared?.lexical) {
|
||||
self.scopes.push()
|
||||
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical)
|
||||
self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, true)
|
||||
} else if (declared) {
|
||||
yield* self.assignPattern(declared.pattern, key, left)
|
||||
} else if (assignmentName) {
|
||||
self.scopes.set(assignmentName, key, left)
|
||||
}
|
||||
@@ -885,24 +911,13 @@ class Frame<R> {
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared) self.scopes.pop()
|
||||
if (declared?.lexical) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if (result.kind === "return") {
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
if (result.label !== undefined && !labels?.has(result.label)) return result
|
||||
continue
|
||||
}
|
||||
const exit = loopExit(result, labels)
|
||||
if (exit !== undefined) return exit
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -1003,8 +1018,13 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
const init = declaration.init
|
||||
// `var x` alone is a no-op: the binding was hoisted on function entry.
|
||||
if (kind === "var") {
|
||||
if (init) yield* self.assignPattern(declaration.id, yield* self.evaluateExpression(init), declaration)
|
||||
continue
|
||||
}
|
||||
const value = init ? yield* self.evaluateExpression(init) : undefined
|
||||
yield* self.declarePattern(declaration.id, value, kind !== "const", declaration, kind !== "var")
|
||||
yield* self.declarePattern(declaration.id, value, kind !== "const", declaration, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1625,6 +1645,8 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
if (fn.body.type === "BlockStatement") {
|
||||
invocation.scopes.push()
|
||||
invocation.hoistVars(fn.body.body, paramScope)
|
||||
const result = yield* invocation.evaluateStatement(fn.body)
|
||||
return result.kind === "return" ? result.value : undefined
|
||||
}
|
||||
@@ -1633,16 +1655,8 @@ class Frame<R> {
|
||||
})
|
||||
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
|
||||
if (!fn.async) return run
|
||||
// The initial yield assigns the promise before the body can self-resolve.
|
||||
const box: { promise?: Values.Promise } = {}
|
||||
return Effect.map(
|
||||
this.createPromise(
|
||||
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, box)),
|
||||
),
|
||||
(promise) => {
|
||||
box.promise = promise
|
||||
return promise
|
||||
},
|
||||
return this.runtime.promises.createWithSelf((self) =>
|
||||
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, self)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export const dateMethods = new Set([
|
||||
"toISOString",
|
||||
"toJSON",
|
||||
"toString",
|
||||
"toDateString",
|
||||
"toTimeString",
|
||||
"toUTCString",
|
||||
"toGMTString",
|
||||
"getFullYear",
|
||||
@@ -103,6 +105,10 @@ export const invokeDateMethod = (
|
||||
return Number.isFinite(value.time) ? hosted.toISOString() : null
|
||||
case "toString":
|
||||
return coerceToString(value)
|
||||
case "toDateString":
|
||||
return hosted.toDateString()
|
||||
case "toTimeString":
|
||||
return hosted.toTimeString()
|
||||
case "toUTCString":
|
||||
case "toGMTString":
|
||||
return hosted.toUTCString()
|
||||
|
||||
@@ -134,19 +134,6 @@ const constructObject = (args: Array<unknown>, node: AstNode): unknown => {
|
||||
)
|
||||
}
|
||||
|
||||
// Tool references are not data; only Object.keys(tools) reads them, for tool names.
|
||||
const rejectTools = (name: string, args: Array<unknown>, node: AstNode): void => {
|
||||
if (!(args[0] instanceof ToolReference)) return
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`,
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
|
||||
const objectStatic = (name: string, impl: (args: Array<unknown>, node: AstNode) => unknown) =>
|
||||
sync(`Object.${name}`, impl)
|
||||
|
||||
// Object constructs identically with or without new, like JS. Only `keys` copies its result into the
|
||||
// program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values.
|
||||
export const objectGlobal = <R>(runner: Runner<R>, toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>) =>
|
||||
@@ -164,32 +151,28 @@ export const objectGlobal = <R>(runner: Runner<R>, toolKeys: (path: ReadonlyArra
|
||||
"Object.keys result",
|
||||
),
|
||||
),
|
||||
values: objectStatic("values", (args, node) =>
|
||||
values: sync("Object.values", (args, node) =>
|
||||
Object.values(enumerableSource("Object.values(...)", args[0], node)),
|
||||
),
|
||||
entries: objectStatic("entries", (args, node) =>
|
||||
entries: sync("Object.entries", (args, node) =>
|
||||
Object.entries(enumerableSource("Object.entries(...)", args[0], node)).map(([key, item]) => [key, item]),
|
||||
),
|
||||
hasOwn: objectStatic("hasOwn", (args, node) =>
|
||||
hasOwn: sync("Object.hasOwn", (args, node) =>
|
||||
Object.hasOwn(
|
||||
enumerableSource("Object.hasOwn(...)", args[0], node),
|
||||
args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]),
|
||||
),
|
||||
),
|
||||
is: objectStatic("is", (args, node) => {
|
||||
is: sync("Object.is", (args, node) => {
|
||||
if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) {
|
||||
throw new InterpreterRuntimeError("Object.is requires data values.", node, "InvalidDataValue")
|
||||
}
|
||||
return Object.is(args[0], args[1])
|
||||
}),
|
||||
assign: objectStatic("assign", objectAssign),
|
||||
assign: sync("Object.assign", objectAssign),
|
||||
fromEntries: new HostFunction<R>({
|
||||
name: "Object.fromEntries",
|
||||
call: (args, node) =>
|
||||
Effect.suspend(() => {
|
||||
rejectTools("fromEntries", args, node)
|
||||
return objectFromEntries(runner, args[0], node)
|
||||
}),
|
||||
call: (args, node) => Effect.suspend(() => objectFromEntries(runner, args[0], node)),
|
||||
}),
|
||||
groupBy: groupBy(runner, "Object"),
|
||||
},
|
||||
|
||||
@@ -8,9 +8,12 @@ export const stringMethods = new Set([
|
||||
"trim",
|
||||
"trimStart",
|
||||
"trimEnd",
|
||||
"trimLeft",
|
||||
"trimRight",
|
||||
"split",
|
||||
"slice",
|
||||
"substring",
|
||||
"substr",
|
||||
"includes",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
@@ -32,6 +35,8 @@ export const stringMethods = new Set([
|
||||
"search",
|
||||
"localeCompare",
|
||||
"normalize",
|
||||
"isWellFormed",
|
||||
"toWellFormed",
|
||||
])
|
||||
|
||||
const codeUnits = (name: string, op: (...codes: Array<number>) => string) =>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { define, type SafeObject } from "../data.js"
|
||||
import { HostNamespace, sync } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
|
||||
import { Values } from "../values.js"
|
||||
import { coerceToString, createErrorValue, errorBrandName } from "./value.js"
|
||||
|
||||
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies.
|
||||
const base64 = (name: "atob" | "btoa") =>
|
||||
sync(name, (args, node) => {
|
||||
if (args.length === 0) {
|
||||
throw new InterpreterRuntimeError(`${name} requires 1 argument, but only 0 were provided.`, node).as("TypeError")
|
||||
}
|
||||
const input = coerceToString(args[0])
|
||||
try {
|
||||
return name === "atob" ? atob(input) : btoa(input)
|
||||
} catch {
|
||||
throw new InterpreterRuntimeError("The string contains invalid characters.", node).as("InvalidCharacterError")
|
||||
}
|
||||
})
|
||||
|
||||
export const atobGlobal = base64("atob")
|
||||
export const btoaGlobal = base64("btoa")
|
||||
|
||||
export const cryptoGlobal = new HostNamespace("crypto", {
|
||||
randomUUID: sync("crypto.randomUUID", () => crypto.randomUUID()),
|
||||
})
|
||||
|
||||
// HTML structured clone over the data model: wrappers are copied, shared references stay shared within
|
||||
// one clone, Errors keep only name, message, and cause, and RegExp lastIndex resets like the spec.
|
||||
const cloneValue = (value: unknown, seen: Map<object, unknown>, node: AstNode): unknown => {
|
||||
if (value === null || typeof value !== "object") return value
|
||||
if (value instanceof Values.Promise || (isRuntimeReference(value) && !Values.isValue(value))) {
|
||||
throw new InterpreterRuntimeError(`${describeValue(value)} could not be cloned.`, node).as("DataCloneError")
|
||||
}
|
||||
const existing = seen.get(value)
|
||||
if (existing !== undefined) return existing
|
||||
const remember = <T extends object>(copied: T): T => {
|
||||
seen.set(value, copied)
|
||||
return copied
|
||||
}
|
||||
if (value instanceof Values.Date) return remember(new Values.Date(value.time))
|
||||
if (value instanceof Values.RegExp) return remember(new Values.RegExp(value.regex.source, value.regex.flags))
|
||||
if (value instanceof Values.URL) return remember(new Values.URL(new URL(value.url.href)))
|
||||
if (value instanceof Values.URLSearchParams) {
|
||||
return remember(new Values.URLSearchParams(new URLSearchParams(value.params)))
|
||||
}
|
||||
if (value instanceof Values.Map) {
|
||||
const copied = remember(new Values.Map())
|
||||
for (const [key, item] of value.map) copied.map.set(cloneValue(key, seen, node), cloneValue(item, seen, node))
|
||||
return copied
|
||||
}
|
||||
if (value instanceof Values.Set) {
|
||||
const copied = remember(new Values.Set())
|
||||
for (const item of value.set) copied.set.add(cloneValue(item, seen, node))
|
||||
return copied
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const copied = remember(new Array<unknown>(value.length))
|
||||
for (const [key, item] of Object.entries(value)) define(copied, key, cloneValue(item, seen, node))
|
||||
return copied
|
||||
}
|
||||
const brand = errorBrandName(value)
|
||||
if (brand !== undefined) {
|
||||
const error = value as { name?: unknown; message?: unknown; cause?: unknown }
|
||||
const copied = remember(createErrorValue(brand, coerceToString(error.message)))
|
||||
if (Object.hasOwn(value, "cause")) copied.cause = cloneValue(error.cause, seen, node)
|
||||
return copied
|
||||
}
|
||||
const copied = remember(Object.create(null) as SafeObject)
|
||||
for (const [key, item] of Object.entries(value)) define(copied, key, cloneValue(item, seen, node))
|
||||
return copied
|
||||
}
|
||||
|
||||
export const structuredCloneGlobal = sync("structuredClone", (args, node) => {
|
||||
if (args.length === 0) {
|
||||
throw new InterpreterRuntimeError("structuredClone requires a value to clone.", node).as("TypeError")
|
||||
}
|
||||
return cloneValue(args[0], new Map(), node)
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
# The 3-Clause BSD License
|
||||
|
||||
Copyright © web-platform-tests contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -528,7 +528,7 @@ describe("CodeMode schema flexibility", () => {
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { adapter: { call } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
expect(runtime.catalog).toStrictEqual([
|
||||
{
|
||||
path: "adapter.call",
|
||||
description: "Call an adapter-described tool",
|
||||
@@ -611,7 +611,7 @@ describe("CodeMode schema flexibility", () => {
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { users: { lookup } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
expect(runtime.catalog).toStrictEqual([
|
||||
{
|
||||
path: "users.lookup",
|
||||
description: "Look up a user",
|
||||
@@ -631,7 +631,7 @@ describe("CodeMode schema flexibility", () => {
|
||||
execute: () => Effect.succeed("pong"),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { net: { ping } } })
|
||||
expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise<void>")
|
||||
expect(runtime.catalog[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise<void>")
|
||||
|
||||
const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`))
|
||||
expect(result.ok).toBe(true)
|
||||
@@ -684,7 +684,7 @@ describe("CodeMode public contract", () => {
|
||||
|
||||
test("describes the catalog and keeps the search built-in registered", async () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
expect(runtime.catalog).toStrictEqual([
|
||||
{
|
||||
path: "orders.lookup",
|
||||
description: "Look up an order by ID",
|
||||
@@ -726,8 +726,8 @@ describe("CodeMode public contract", () => {
|
||||
const first = CodeMode.make({ tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } } })
|
||||
const second = CodeMode.make({ tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } } })
|
||||
|
||||
expect(first.catalog()).toStrictEqual(second.catalog())
|
||||
expect(first.catalog().map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"])
|
||||
expect(first.catalog).toStrictEqual(second.catalog)
|
||||
expect(first.catalog.map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"])
|
||||
})
|
||||
|
||||
test("renders bracket notation for tool names that are not JavaScript identifiers", async () => {
|
||||
@@ -739,7 +739,7 @@ describe("CodeMode public contract", () => {
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
expect(runtime.catalog).toStrictEqual([
|
||||
{
|
||||
path: "context7.resolve-library-id",
|
||||
description: "Resolve a library ID",
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
[
|
||||
["", []],
|
||||
["abcd", [105, 183, 29]],
|
||||
[" abcd", [105, 183, 29]],
|
||||
["abcd ", [105, 183, 29]],
|
||||
[" abcd===", null],
|
||||
["abcd=== ", null],
|
||||
["abcd ===", null],
|
||||
["a", null],
|
||||
["ab", [105]],
|
||||
["abc", [105, 183]],
|
||||
["abcde", null],
|
||||
["𐀀", null],
|
||||
["=", null],
|
||||
["==", null],
|
||||
["===", null],
|
||||
["====", null],
|
||||
["=====", null],
|
||||
["a=", null],
|
||||
["a==", null],
|
||||
["a===", null],
|
||||
["a====", null],
|
||||
["a=====", null],
|
||||
["ab=", null],
|
||||
["ab==", [105]],
|
||||
["ab===", null],
|
||||
["ab====", null],
|
||||
["ab=====", null],
|
||||
["abc=", [105, 183]],
|
||||
["abc==", null],
|
||||
["abc===", null],
|
||||
["abc====", null],
|
||||
["abc=====", null],
|
||||
["abcd=", null],
|
||||
["abcd==", null],
|
||||
["abcd===", null],
|
||||
["abcd====", null],
|
||||
["abcd=====", null],
|
||||
["abcde=", null],
|
||||
["abcde==", null],
|
||||
["abcde===", null],
|
||||
["abcde====", null],
|
||||
["abcde=====", null],
|
||||
["=a", null],
|
||||
["=a=", null],
|
||||
["a=b", null],
|
||||
["a=b=", null],
|
||||
["ab=c", null],
|
||||
["ab=c=", null],
|
||||
["abc=d", null],
|
||||
["abc=d=", null],
|
||||
["ab\u000Bcd", null],
|
||||
["ab\u3000cd", null],
|
||||
["ab\u3001cd", null],
|
||||
["ab\tcd", [105, 183, 29]],
|
||||
["ab\ncd", [105, 183, 29]],
|
||||
["ab\fcd", [105, 183, 29]],
|
||||
["ab\rcd", [105, 183, 29]],
|
||||
["ab cd", [105, 183, 29]],
|
||||
["ab\u00a0cd", null],
|
||||
["ab\t\n\f\r cd", [105, 183, 29]],
|
||||
[" \t\n\f\r ab\t\n\f\r cd\t\n\f\r ", [105, 183, 29]],
|
||||
["ab\t\n\f\r =\t\n\f\r =\t\n\f\r ", [105]],
|
||||
["A", null],
|
||||
["/A", [252]],
|
||||
["//A", [255, 240]],
|
||||
["///A", [255, 255, 192]],
|
||||
["////A", null],
|
||||
["/", null],
|
||||
["A/", [3]],
|
||||
["AA/", [0, 15]],
|
||||
["AAAA/", null],
|
||||
["AAA/", [0, 0, 63]],
|
||||
["\u0000nonsense", null],
|
||||
["abcd\u0000nonsense", null],
|
||||
["YQ", [97]],
|
||||
["YR", [97]],
|
||||
["~~", null],
|
||||
["..", null],
|
||||
["--", null],
|
||||
["__", null]
|
||||
]
|
||||
@@ -491,12 +491,8 @@ describe("CodeMode-specific string behavior", () => {
|
||||
expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
|
||||
})
|
||||
|
||||
test("does not expose obsolete string aliases", async () => {
|
||||
expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([
|
||||
"undefined",
|
||||
"undefined",
|
||||
"undefined",
|
||||
])
|
||||
test("exposes the Annex B string aliases every engine ships", async () => {
|
||||
expect(await value(`return [" x ".trimLeft(), " x ".trimRight(), "abc".substr(1, 1)]`)).toEqual(["x ", " x", "b"])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -740,7 +740,7 @@ describe("JSDoc signatures in catalogs and search results", () => {
|
||||
"}",
|
||||
].join("\n")
|
||||
const signature = `tools.constrained(input: ${type}): Promise<${type}>`
|
||||
expect(runtime.catalog()[0]?.signature).toBe(signature)
|
||||
expect(runtime.catalog[0]?.signature).toBe(signature)
|
||||
const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.constrained" })'))
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) throw new Error("search failed")
|
||||
@@ -796,7 +796,7 @@ describe("JSDoc signatures in catalogs and search results", () => {
|
||||
})
|
||||
|
||||
test("the catalog uses the same JSDoc signatures as search", async () => {
|
||||
const catalog = runtime.catalog()
|
||||
const catalog = runtime.catalog
|
||||
const github = (await search("list issues repository")).items.find(
|
||||
({ path }) => path === "tools.github.list_issues",
|
||||
)!
|
||||
@@ -824,7 +824,7 @@ describe("non-identifier tool paths", () => {
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
test("catalog signatures use bracket notation for dashed tool names", () => {
|
||||
expect(runtime.catalog()[0]?.signature).toBe(
|
||||
expect(runtime.catalog[0]?.signature).toBe(
|
||||
'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise<unknown>',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
|
||||
* - test/annexB/built-ins/String/prototype/substr/length-falsey.js
|
||||
* - test/annexB/built-ins/String/prototype/substr/length-negative.js
|
||||
* - test/annexB/built-ins/String/prototype/substr/length-positive.js
|
||||
* - test/annexB/built-ins/String/prototype/substr/length-undef.js
|
||||
* - test/annexB/built-ins/String/prototype/substr/start-negative.js
|
||||
* - test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js
|
||||
* - test/built-ins/String/prototype/isWellFormed/returns-boolean.js
|
||||
* - test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js
|
||||
* - test/built-ins/Date/prototype/toDateString/format.js
|
||||
* - test/built-ins/Date/prototype/toDateString/invalid-date.js
|
||||
* - test/built-ins/Date/prototype/toDateString/negative-year.js
|
||||
* - test/built-ins/Date/prototype/toTimeString/format.js
|
||||
* - test/built-ins/Date/prototype/toTimeString/invalid-date.js
|
||||
*
|
||||
* Copyright (C) 2016, 2017 the V8 project authors. All rights reserved.
|
||||
* Copyright (C) 2018 Richard Gibson. All rights reserved.
|
||||
* Copyright (C) 2022 Jordan Harband. All rights reserved.
|
||||
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
||||
*
|
||||
* The `typeof String.prototype.method` checks are replaced with `typeof "".method` because
|
||||
* CodeMode has no prototype objects.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CodeMode } from "../src/index.js"
|
||||
|
||||
const value = async (code: string) => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
||||
return result.value
|
||||
}
|
||||
|
||||
describe("String.prototype.substr Test262 parity", () => {
|
||||
test("test/annexB/built-ins/String/prototype/substr/length-falsey.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [false, NaN, "", null].flatMap((length) => [0, 1, 2, 3].map((start) => "abc".substr(start, length)))
|
||||
`),
|
||||
).toEqual(Array(16).fill(""))
|
||||
})
|
||||
|
||||
test("test/annexB/built-ins/String/prototype/substr/length-negative.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [0, 1, 2, 3].flatMap((start) => [-1, -2, -3, -4].map((length) => "abc".substr(start, length)))
|
||||
`),
|
||||
).toEqual(Array(16).fill(""))
|
||||
})
|
||||
|
||||
test("test/annexB/built-ins/String/prototype/substr/length-positive.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [0, 1, 2, 3].map((start) => [1, 2, 3, 4].map((length) => "abc".substr(start, length)))
|
||||
`),
|
||||
).toEqual([
|
||||
["a", "ab", "abc", "abc"],
|
||||
["b", "bc", "bc", "bc"],
|
||||
["c", "c", "c", "c"],
|
||||
["", "", "", ""],
|
||||
])
|
||||
})
|
||||
|
||||
test("test/annexB/built-ins/String/prototype/substr/length-undef.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
"abc".substr(0), "abc".substr(1), "abc".substr(2), "abc".substr(3),
|
||||
"abc".substr(0, undefined), "abc".substr(1, undefined), "abc".substr(2, undefined), "abc".substr(3, undefined),
|
||||
]
|
||||
`),
|
||||
).toEqual(["abc", "bc", "c", "", "abc", "bc", "c", ""])
|
||||
})
|
||||
|
||||
test("test/annexB/built-ins/String/prototype/substr/start-negative.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return ["abc".substr(-1), "abc".substr(-2), "abc".substr(-3), "abc".substr(-4), "abc".substr(-1.1)]
|
||||
`),
|
||||
).toEqual(["c", "bc", "abc", "abc", "c"])
|
||||
})
|
||||
|
||||
test("test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const pair = "\\ud834\\udf06"
|
||||
return [pair.substr(0), pair.substr(1), pair.substr(2), pair.substr(0, 0), pair.substr(0, 1), pair.substr(0, 2)]
|
||||
`),
|
||||
).toEqual(["\ud834\udf06", "\udf06", "", "", "\ud834", "\ud834\udf06"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("String well-formedness Test262 parity", () => {
|
||||
test("test/built-ins/String/prototype/isWellFormed/returns-boolean.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const leadingPoo = "\\uD83D"
|
||||
const trailingPoo = "\\uDCA9"
|
||||
const wholePoo = leadingPoo + trailingPoo
|
||||
return [
|
||||
typeof "".isWellFormed,
|
||||
("a" + leadingPoo + "c" + leadingPoo + "e").isWellFormed(),
|
||||
("a" + trailingPoo + "c" + trailingPoo + "e").isWellFormed(),
|
||||
("a" + trailingPoo + leadingPoo + "d").isWellFormed(),
|
||||
"a💩c".isWellFormed(),
|
||||
"a\\uD83D\\uDCA9c".isWellFormed(),
|
||||
("a" + leadingPoo + trailingPoo + "d").isWellFormed(),
|
||||
wholePoo.slice(0, 1).isWellFormed(),
|
||||
wholePoo.slice(1).isWellFormed(),
|
||||
"abc".isWellFormed(),
|
||||
"a\\u25A8c".isWellFormed(),
|
||||
]
|
||||
`),
|
||||
).toEqual(["function", false, false, false, true, true, true, false, false, true, true])
|
||||
})
|
||||
|
||||
test("test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const replacementChar = "\\uFFFD"
|
||||
const leadingPoo = "\\uD83D"
|
||||
const trailingPoo = "\\uDCA9"
|
||||
const wholePoo = leadingPoo + trailingPoo
|
||||
return [
|
||||
typeof "".toWellFormed,
|
||||
("a" + leadingPoo + "c" + leadingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
|
||||
("a" + trailingPoo + "c" + trailingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
|
||||
("a" + trailingPoo + leadingPoo + "d").toWellFormed() === "a" + replacementChar + replacementChar + "d",
|
||||
"a💩c".toWellFormed() === "a💩c",
|
||||
"a\\uD83D\\uDCA9c".toWellFormed() === "a\\uD83D\\uDCA9c",
|
||||
("a" + leadingPoo + trailingPoo + "d").toWellFormed() === "a" + wholePoo + "d",
|
||||
wholePoo.slice(0, 1).toWellFormed() === replacementChar,
|
||||
wholePoo.slice(1).toWellFormed() === replacementChar,
|
||||
"abc".toWellFormed() === "abc",
|
||||
"a\\u25A8c".toWellFormed() === "a\\u25A8c",
|
||||
]
|
||||
`),
|
||||
).toEqual(["function", true, true, true, true, true, true, true, true, true, true])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Date string formatting Test262 parity", () => {
|
||||
test("test/built-ins/Date/prototype/toDateString/format.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const dateRegExp = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{2} [0-9]{4}$/
|
||||
return [dateRegExp.test(new Date(0).toDateString()), dateRegExp.test(new Date("0020-01-01T00:00:00Z").toDateString())]
|
||||
`),
|
||||
).toEqual([true, true])
|
||||
})
|
||||
|
||||
test("test/built-ins/Date/prototype/toDateString/invalid-date.js", async () => {
|
||||
expect(await value(`return new Date(NaN).toDateString()`)).toBe("Invalid Date")
|
||||
})
|
||||
|
||||
test("test/built-ins/Date/prototype/toDateString/negative-year.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return ["-000001", "-000012", "-000123", "-001234", "-012345", "-123456"].map(
|
||||
(year) => new Date(year + "-07-01T00:00Z").toDateString().split(" ")[3],
|
||||
)
|
||||
`),
|
||||
).toEqual(["-0001", "-0012", "-0123", "-1234", "-12345", "-123456"])
|
||||
})
|
||||
|
||||
test("test/built-ins/Date/prototype/toTimeString/format.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const timeRegExp = /^[0-9]{2}:[0-9]{2}:[0-9]{2} GMT[+-][0-9]{4}( \\(.+\\))?$/
|
||||
return timeRegExp.test(new Date(0).toTimeString())
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("test/built-ins/Date/prototype/toTimeString/invalid-date.js", async () => {
|
||||
expect(await value(`return new Date(NaN).toTimeString()`)).toBe("Invalid Date")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Portions adapted from web-platform-tests at revision 863077959ca8c1a7ceecfbe2534b75d2527b9013:
|
||||
* - html/webappapis/structured-clone/structured-clone-battery-of-tests.js
|
||||
*
|
||||
* Copyright © web-platform-tests contributors. Governed by the 3-Clause BSD license in LICENSE.wpt.
|
||||
*
|
||||
* The battery's `check(description, input, compare)` shape and its `compare_*` helpers are kept, run
|
||||
* inside the interpreter. Ported: primitives, Array/Object of primitives, Date, RegExp, Error, sparse
|
||||
* arrays, identical (shared) property values, and the index-property-plus-length object. Not portable:
|
||||
* boxed primitives, BigInt, Blob/File/ImageData/ArrayBuffer/typed arrays (no binary values), circular
|
||||
* references (rejected at insertion here), property descriptors and prototype properties (no
|
||||
* defineProperty or prototypes), and the throwing-getter case (no getters). `assert_throws_dom` for
|
||||
* `DataCloneError` becomes an `error.name` check.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CodeMode } from "../src/index.js"
|
||||
|
||||
const value = async (code: string) => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
||||
return result.value
|
||||
}
|
||||
|
||||
// The WPT harness, minus async: assertions push a failure description instead of throwing so one run
|
||||
// reports every failing check.
|
||||
const harness = `
|
||||
const failures = []
|
||||
const assert_equals = (a, b, m) => { if (!Object.is(a, b) && !(a !== a && b !== b)) failures.push((m ?? "") + ": " + String(a) + " !== " + String(b)) }
|
||||
const assert_not_equals = (a, b, m) => { if (a === b) failures.push((m ?? "") + ": unexpectedly identical") }
|
||||
const assert_true = (a, m) => { if (a !== true) failures.push((m ?? "") + ": not true") }
|
||||
const assert_false = (a, m) => { if (a !== false) failures.push((m ?? "") + ": not false") }
|
||||
let current = ""
|
||||
function check(description, input, callback) {
|
||||
current = description
|
||||
const newInput = typeof input === "function" ? input() : input
|
||||
const copy = structuredClone(newInput)
|
||||
const before = failures.length
|
||||
callback(copy, newInput)
|
||||
for (let i = before; i < failures.length; i++) failures[i] = description + " — " + failures[i]
|
||||
}
|
||||
function compare_primitive(actual, input) { assert_equals(actual, input) }
|
||||
function compare_Array(callback) {
|
||||
return function (actual, input) {
|
||||
assert_true(Array.isArray(actual), "instanceof Array")
|
||||
assert_not_equals(actual, input)
|
||||
assert_equals(actual.length, input.length, "length")
|
||||
callback(actual, input)
|
||||
}
|
||||
}
|
||||
function compare_Object(callback) {
|
||||
return function (actual, input) {
|
||||
assert_true(actual instanceof Object, "instanceof Object")
|
||||
assert_false(Array.isArray(actual), "instanceof Array")
|
||||
assert_not_equals(actual, input)
|
||||
callback(actual, input)
|
||||
}
|
||||
}
|
||||
function enumerate_props(compare_func) {
|
||||
return function (actual, input) { for (const x in input) compare_func(actual[x], input[x]) }
|
||||
}
|
||||
`
|
||||
|
||||
describe("structuredClone WPT battery", () => {
|
||||
test("primitives, and arrays and objects of primitives", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${harness}
|
||||
check('primitive undefined', undefined, compare_primitive)
|
||||
check('primitive null', null, compare_primitive)
|
||||
check('primitive true', true, compare_primitive)
|
||||
check('primitive false', false, compare_primitive)
|
||||
check('primitive string, empty string', '', compare_primitive)
|
||||
check('primitive string, lone high surrogate', '\\uD800', compare_primitive)
|
||||
check('primitive string, lone low surrogate', '\\uDC00', compare_primitive)
|
||||
check('primitive string, NUL', '\\u0000', compare_primitive)
|
||||
check('primitive string, astral character', '\\uDBFF\\uDFFD', compare_primitive)
|
||||
check('primitive number, 0.2', 0.2, compare_primitive)
|
||||
check('primitive number, 0', 0, compare_primitive)
|
||||
check('primitive number, -0', -0, compare_primitive)
|
||||
check('primitive number, NaN', NaN, compare_primitive)
|
||||
check('primitive number, Infinity', Infinity, compare_primitive)
|
||||
check('primitive number, -Infinity', -Infinity, compare_primitive)
|
||||
check('primitive number, 9007199254740992', 9007199254740992, compare_primitive)
|
||||
check('primitive number, -9007199254740992', -9007199254740992, compare_primitive)
|
||||
check('primitive number, 9007199254740994', 9007199254740994, compare_primitive)
|
||||
check('primitive number, -9007199254740994', -9007199254740994, compare_primitive)
|
||||
check('Array primitives', [undefined, null, true, false, '', '\\uD800', '\\uDC00', '\\u0000', '\\uDBFF\\uDFFD',
|
||||
0.2, 0, -0, NaN, Infinity, -Infinity, 9007199254740992, -9007199254740992, 9007199254740994, -9007199254740994],
|
||||
compare_Array(enumerate_props(compare_primitive)))
|
||||
check('Object primitives', { 'undefined': undefined, 'null': null, 'true': true, 'false': false, 'empty': '',
|
||||
'high surrogate': '\\uD800', 'low surrogate': '\\uDC00', 'nul': '\\u0000', 'astral': '\\uDBFF\\uDFFD',
|
||||
'0.2': 0.2, '0': 0, '-0': -0, 'NaN': NaN, 'Infinity': Infinity, '-Infinity': -Infinity,
|
||||
'9007199254740992': 9007199254740992, '-9007199254740992': -9007199254740992,
|
||||
'9007199254740994': 9007199254740994, '-9007199254740994': -9007199254740994 },
|
||||
compare_Object(enumerate_props(compare_primitive)))
|
||||
return failures
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("Date", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${harness}
|
||||
function compare_Date(actual, input) {
|
||||
assert_true(actual instanceof Date, 'instanceof Date')
|
||||
assert_equals(Number(actual), Number(input), 'converted to primitive')
|
||||
assert_not_equals(actual, input)
|
||||
}
|
||||
check('Date 0', new Date(0), compare_Date)
|
||||
check('Date -0', new Date(-0), compare_Date)
|
||||
check('Date -8.64e15', new Date(-8.64e15), compare_Date)
|
||||
check('Date 8.64e15', new Date(8.64e15), compare_Date)
|
||||
check('Array Date objects', [new Date(0), new Date(-0), new Date(-8.64e15), new Date(8.64e15)],
|
||||
compare_Array(enumerate_props(compare_Date)))
|
||||
check('Object Date objects', { '0': new Date(0), '-0': new Date(-0), '-8.64e15': new Date(-8.64e15), '8.64e15': new Date(8.64e15) },
|
||||
compare_Object(enumerate_props(compare_Date)))
|
||||
return failures
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("RegExp: flags copied, lastIndex reset, source escaped", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${harness}
|
||||
function compare_RegExp(expected_source) {
|
||||
return function (actual, input) {
|
||||
assert_true(actual instanceof RegExp, 'instanceof RegExp')
|
||||
assert_equals(actual.global, input.global, 'global')
|
||||
assert_equals(actual.ignoreCase, input.ignoreCase, 'ignoreCase')
|
||||
assert_equals(actual.multiline, input.multiline, 'multiline')
|
||||
assert_equals(actual.source, expected_source, 'source')
|
||||
assert_equals(actual.sticky, input.sticky, 'sticky')
|
||||
assert_equals(actual.unicode, input.unicode, 'unicode')
|
||||
assert_equals(actual.lastIndex, 0, 'lastIndex')
|
||||
assert_not_equals(actual, input)
|
||||
}
|
||||
}
|
||||
function func_RegExp_flags_lastIndex() {
|
||||
const r = /foo/gim
|
||||
r.lastIndex = 2
|
||||
return r
|
||||
}
|
||||
function func_RegExp_sticky() { return new RegExp('foo', 'y') }
|
||||
function func_RegExp_unicode() { return new RegExp('foo', 'u') }
|
||||
check('RegExp flags and lastIndex', func_RegExp_flags_lastIndex, compare_RegExp('foo'))
|
||||
check('RegExp sticky flag', func_RegExp_sticky, compare_RegExp('foo'))
|
||||
check('RegExp unicode flag', func_RegExp_unicode, compare_RegExp('foo'))
|
||||
check('RegExp empty', new RegExp(''), compare_RegExp('(?:)'))
|
||||
check('RegExp slash', new RegExp('/'), compare_RegExp('\\\\/'))
|
||||
check('RegExp new line', new RegExp('\\n'), compare_RegExp('\\\\n'))
|
||||
check('Array RegExp object, RegExp flags and lastIndex', [func_RegExp_flags_lastIndex()], compare_Array(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Array RegExp object, RegExp sticky flag', function () { return [func_RegExp_sticky()] }, compare_Array(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Array RegExp object, RegExp unicode flag', function () { return [func_RegExp_unicode()] }, compare_Array(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Array RegExp object, RegExp empty', [new RegExp('')], compare_Array(enumerate_props(compare_RegExp('(?:)'))))
|
||||
check('Array RegExp object, RegExp slash', [new RegExp('/')], compare_Array(enumerate_props(compare_RegExp('\\\\/'))))
|
||||
check('Array RegExp object, RegExp new line', [new RegExp('\\n')], compare_Array(enumerate_props(compare_RegExp('\\\\n'))))
|
||||
check('Object RegExp object, RegExp flags and lastIndex', { 'x': func_RegExp_flags_lastIndex() }, compare_Object(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Object RegExp object, RegExp sticky flag', function () { return { 'x': func_RegExp_sticky() } }, compare_Object(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Object RegExp object, RegExp unicode flag', function () { return { 'x': func_RegExp_unicode() } }, compare_Object(enumerate_props(compare_RegExp('foo'))))
|
||||
check('Object RegExp object, RegExp empty', { 'x': new RegExp('') }, compare_Object(enumerate_props(compare_RegExp('(?:)'))))
|
||||
check('Object RegExp object, RegExp slash', { 'x': new RegExp('/') }, compare_Object(enumerate_props(compare_RegExp('\\\\/'))))
|
||||
check('Object RegExp object, RegExp new line', { 'x': new RegExp('\\n') }, compare_Object(enumerate_props(compare_RegExp('\\\\n'))))
|
||||
return failures
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("Error: name and message kept, custom properties dropped", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${harness}
|
||||
function compare_Error(actual, input) {
|
||||
assert_true(actual instanceof Error, "Checking instanceof")
|
||||
assert_equals(actual.name, input.name, "Checking name")
|
||||
assert_equals(Object.hasOwn(actual, "message"), Object.hasOwn(input, "message"), "Checking message existence")
|
||||
assert_equals(actual.message, input.message, "Checking message")
|
||||
assert_equals(actual.foo, undefined, "Checking for absence of custom property")
|
||||
}
|
||||
check('Empty Error object', new Error(), compare_Error)
|
||||
for (const constructor of [Error, RangeError, ReferenceError, SyntaxError, TypeError, URIError]) {
|
||||
check(constructor.name, () => {
|
||||
const error = new constructor("Error message here")
|
||||
error.foo = "testing"
|
||||
return error
|
||||
}, compare_Error)
|
||||
}
|
||||
return failures
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("sparse arrays, index-property objects, and identical property values", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${harness}
|
||||
check('Array sparse', new Array(10), compare_Array(enumerate_props(compare_primitive)))
|
||||
check('Object with index property and length', { '0': 'foo', 'length': 1 }, compare_Object(enumerate_props(compare_primitive)))
|
||||
function check_identical_property_values(prop1, prop2) {
|
||||
return function (actual) { assert_equals(actual[prop1], actual[prop2]) }
|
||||
}
|
||||
check('Array with identical property values', function () {
|
||||
const obj = {}
|
||||
return [obj, obj]
|
||||
}, compare_Array(check_identical_property_values('0', '1')))
|
||||
check('Object with identical property values', function () {
|
||||
const obj = {}
|
||||
return { 'x': obj, 'y': obj }
|
||||
}, compare_Object(check_identical_property_values('x', 'y')))
|
||||
return failures
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("structuredClone beyond the WPT battery", () => {
|
||||
test("Map, Set, URL, and URLSearchParams are copied, with shared references preserved across containers", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const shared = { n: 1 }
|
||||
const input = { m: new Map([[shared, shared]]), s: new Set([shared]), u: new URL("https://a.b/c?d=1") }
|
||||
const copy = structuredClone(input)
|
||||
const [[key, item]] = [...copy.m]
|
||||
copy.u.searchParams.set("d", "2")
|
||||
return [
|
||||
copy.m !== input.m, key !== shared, key === item, key === [...copy.s][0],
|
||||
copy.u !== input.u, input.u.href, copy.u.href,
|
||||
]
|
||||
`),
|
||||
).toEqual([true, true, true, true, true, "https://a.b/c?d=1", "https://a.b/c?d=2"])
|
||||
})
|
||||
|
||||
test("functions, promises, and tool references throw DataCloneError", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [() => 1, Promise.resolve(1), tools, Math, { nested: [() => 1] }].map((input) => {
|
||||
try { structuredClone(input); return "cloned" } catch (error) { return error.name }
|
||||
})
|
||||
`),
|
||||
).toEqual(Array(5).fill("DataCloneError"))
|
||||
expect(await value(`try { structuredClone() } catch (error) { return error.name }`)).toBe("TypeError")
|
||||
})
|
||||
})
|
||||
@@ -26,7 +26,7 @@ describe("dotted tool names", () => {
|
||||
const runtime = CodeMode.make({ tools: { api: { "issues.list": echo("List issues", "listed") } } })
|
||||
|
||||
test("a dotted name becomes nested namespaces in the catalog", () => {
|
||||
const catalog = runtime.catalog()
|
||||
const catalog = runtime.catalog
|
||||
expect(catalog).toHaveLength(1)
|
||||
expect(catalog[0]?.path).toBe("api.issues.list")
|
||||
expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(")
|
||||
@@ -51,7 +51,7 @@ describe("dotted tool names", () => {
|
||||
|
||||
test("a top-level dotted name nests from the root", async () => {
|
||||
const flat = CodeMode.make({ tools: { "issues.list": echo("List issues", "flat") } })
|
||||
expect(flat.catalog()[0]?.path).toBe("issues.list")
|
||||
expect(flat.catalog[0]?.path).toBe("issues.list")
|
||||
expect(await value(flat, `return await tools.issues.list({})`)).toBe("flat")
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("callable namespaces", () => {
|
||||
test("a path can hold a tool and child tools at once", async () => {
|
||||
expect(await value(runtime, `return await tools.issues({})`)).toBe("all")
|
||||
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("list")
|
||||
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues", "issues.list"])
|
||||
expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues", "issues.list"])
|
||||
})
|
||||
|
||||
test("a callable namespace enumerates its children", async () => {
|
||||
@@ -145,7 +145,7 @@ describe("tool input diagnostics", () => {
|
||||
|
||||
test("an empty-input tool advertises () and runs with zero arguments", async () => {
|
||||
const empty = CodeMode.make({ tools: { ping: echo("Ping", "pong") } })
|
||||
expect(empty.catalog()[0]?.signature).toBe("tools.ping(): Promise<string>")
|
||||
expect(empty.catalog[0]?.signature).toBe("tools.ping(): Promise<string>")
|
||||
expect(await value(empty, `return await tools.ping()`)).toBe("pong")
|
||||
})
|
||||
})
|
||||
@@ -160,7 +160,7 @@ describe("blocked member names on tool paths", () => {
|
||||
})
|
||||
|
||||
test("tools may use blocked member names because path segments never touch real properties", async () => {
|
||||
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.constructor", "nested.__proto__", "prototype"])
|
||||
expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues.constructor", "nested.__proto__", "prototype"])
|
||||
expect(await value(runtime, `return await tools.prototype({})`)).toBe("proto")
|
||||
expect(await value(runtime, `return await tools.issues.constructor({})`)).toBe("ctor")
|
||||
expect(await value(runtime, `return await tools["issues.constructor"]({})`)).toBe("ctor")
|
||||
@@ -172,7 +172,7 @@ describe("blocked member names on tool paths", () => {
|
||||
const poisoned = CodeMode.make({
|
||||
tools: { ns: { __proto__: echo("Hidden", "hidden"), real: echo("Real tool", "real") } },
|
||||
})
|
||||
expect(poisoned.catalog().map((tool) => tool.path)).toEqual(["ns.real"])
|
||||
expect(poisoned.catalog.map((tool) => tool.path)).toEqual(["ns.real"])
|
||||
expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real")
|
||||
})
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("namespace metadata", () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
|
||||
test("the wrapper does not add a segment to callable paths", async () => {
|
||||
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["api.status", "api.users.list", "plain.read"])
|
||||
expect(runtime.catalog.map((tool) => tool.path)).toEqual(["api.status", "api.users.list", "plain.read"])
|
||||
expect(await value(runtime, `return await tools.api.users.list({})`)).toBe("users")
|
||||
})
|
||||
|
||||
@@ -260,8 +260,8 @@ describe("canonical path collisions", () => {
|
||||
tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } },
|
||||
})
|
||||
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
|
||||
expect(runtime.catalog()).toHaveLength(1)
|
||||
expect(runtime.catalog()[0]?.description).toBe("Second")
|
||||
expect(runtime.catalog).toHaveLength(1)
|
||||
expect(runtime.catalog[0]?.description).toBe("Second")
|
||||
})
|
||||
|
||||
test("overriding one path keeps sibling tools from both shapes", async () => {
|
||||
@@ -272,7 +272,7 @@ describe("canonical path collisions", () => {
|
||||
"issues.close": echo("Close issue", "closed"),
|
||||
},
|
||||
})
|
||||
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.close", "issues.get", "issues.list"])
|
||||
expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues.close", "issues.get", "issues.list"])
|
||||
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
|
||||
expect(await value(runtime, `return await tools.issues.get({})`)).toBe("got")
|
||||
expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed")
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
|
||||
* - test/language/statements/variable/S12.2_A1.js
|
||||
* - test/language/statements/variable/S12.2_A3.js
|
||||
* - test/language/statements/variable/S12.2_A6_T1.js
|
||||
* - test/language/statements/variable/S12.2_A7.js
|
||||
* - test/language/statements/variable/S12.2_A10.js
|
||||
* - test/language/statements/variable/S12.2_A12.js
|
||||
* - test/language/block-scope/shadowing/hoisting-var-declarations-out-of-blocks.js
|
||||
* - test/language/block-scope/shadowing/catch-parameter-shadowing-var-variable.js
|
||||
* - test/language/statements/for/head-var-bound-names-in-stmt.js
|
||||
* - test/language/statements/function/scope-paramsbody-var-open.js
|
||||
* - test/language/statements/function/scope-paramsbody-var-close.js
|
||||
*
|
||||
* Copyright 2009 the Sputnik authors. All rights reserved.
|
||||
* Copyright (C) 2011, 2016 the V8 project authors. All rights reserved.
|
||||
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
||||
*
|
||||
* Files that observe `var` through `eval`, `this`, `delete`, or the global object (S12.2_A2, A5, A9,
|
||||
* A11, `scope-*-none.js`, `scope-param-elem-*.js`) have no analogue here and are not ported.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CodeMode } from "../src/index.js"
|
||||
|
||||
const value = async (code: string) => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
||||
return result.value
|
||||
}
|
||||
|
||||
describe("var hoisting Test262 parity", () => {
|
||||
test("test/language/statements/variable/S12.2_A1.js: use before declaration reads undefined", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
__x = __x
|
||||
__y = __x ? "good fellow" : "liar"
|
||||
__z = __z === __x ? 1 : 0
|
||||
let unknown
|
||||
try { __something__undefined = __something__undefined } catch (error) { unknown = error.name }
|
||||
const before = [__y, __z, unknown]
|
||||
var __x, __y = true, __z = __y ? "smeagol" : "golum"
|
||||
return [...before, __y, __z]
|
||||
`),
|
||||
).toEqual(["liar", 1, "ReferenceError", true, "smeagol"])
|
||||
})
|
||||
|
||||
test("test/language/statements/variable/S12.2_A3.js: nested functions redeclare or assign", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
var __var = "OUT"
|
||||
const inner = (function () {
|
||||
var __var = "IN"
|
||||
;(function () { __var = "INNER_SPACE" })()
|
||||
;(function () { var __var = "INNER_SUN" })()
|
||||
return __var
|
||||
})()
|
||||
const after = __var
|
||||
const assigned = (function () {
|
||||
__var = "IN"
|
||||
;(function () { __var = "INNERED" })()
|
||||
;(function () { var __var = "INNAGER" })()
|
||||
return __var
|
||||
})()
|
||||
return [inner, after, assigned, __var]
|
||||
`),
|
||||
).toEqual(["INNER_SPACE", "OUT", "INNERED", "INNERED"])
|
||||
})
|
||||
|
||||
test("test/language/statements/variable/S12.2_A6_T1.js: var inside try and catch is hoisted", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
intry__var = intry__var
|
||||
incatch__var = incatch__var
|
||||
try { var intry__var } catch (e) { var incatch__var }
|
||||
return [typeof intry__var, typeof incatch__var]
|
||||
`),
|
||||
).toEqual(["undefined", "undefined"])
|
||||
})
|
||||
|
||||
test("test/language/statements/variable/S12.2_A7.js: var after break inside for is hoisted", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
infor_var = infor_var
|
||||
for (;;) { break; var infor_var }
|
||||
return typeof infor_var
|
||||
`),
|
||||
).toBe("undefined")
|
||||
})
|
||||
|
||||
test("test/language/statements/variable/S12.2_A10.js: var in for head is hoisted", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
__ind = __ind
|
||||
for (var __ind; ; ) { break }
|
||||
return typeof __ind
|
||||
`),
|
||||
).toBe("undefined")
|
||||
})
|
||||
|
||||
test("test/language/statements/variable/S12.2_A12.js: var in do-while body is hoisted", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
x = x
|
||||
do var x; while (false)
|
||||
return typeof x
|
||||
`),
|
||||
).toBe("undefined")
|
||||
})
|
||||
|
||||
test("test/language/block-scope/shadowing/hoisting-var-declarations-out-of-blocks.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
function fn() {
|
||||
{ var x = 1; var y }
|
||||
return [x, typeof y]
|
||||
}
|
||||
return fn()
|
||||
`),
|
||||
).toEqual([1, "undefined"])
|
||||
})
|
||||
|
||||
test("test/language/block-scope/shadowing/catch-parameter-shadowing-var-variable.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
function fn() {
|
||||
var a = 1
|
||||
let caught
|
||||
try { throw "stuff3" } catch (a) { caught = a }
|
||||
return [caught, a]
|
||||
}
|
||||
return fn()
|
||||
`),
|
||||
).toEqual(["stuff3", 1])
|
||||
})
|
||||
|
||||
test("test/language/statements/for/head-var-bound-names-in-stmt.js: redeclaring the head var in the body", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
var iterCount = 0
|
||||
var first = true
|
||||
for (var x; first; first = false) {
|
||||
var x
|
||||
iterCount += 1
|
||||
}
|
||||
return iterCount
|
||||
`),
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
test("test/language/statements/function/scope-paramsbody-var-open.js: parameter defaults see the outer var", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
var x = "outside"
|
||||
var probeParams, probeBody
|
||||
function f(_ = probeParams = function () { return x }) {
|
||||
var x = "inside"
|
||||
probeBody = function () { return x }
|
||||
}
|
||||
f()
|
||||
return [probeParams(), probeBody()]
|
||||
`),
|
||||
).toEqual(["outside", "inside"])
|
||||
})
|
||||
|
||||
test("test/language/statements/function/scope-paramsbody-var-close.js: body var does not leak out", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
var probe
|
||||
function f(_ = null) {
|
||||
var x = "inside"
|
||||
probe = function () { return x }
|
||||
}
|
||||
f()
|
||||
var x = "outside"
|
||||
return [probe(), x]
|
||||
`),
|
||||
).toEqual(["inside", "outside"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("var semantics beyond Test262", () => {
|
||||
test("redeclaration and block-level var assign the one function-scoped binding", async () => {
|
||||
expect(await value(`var a = 1; var a = 2; { var a = 3 } return a`)).toBe(3)
|
||||
expect(await value(`var q = 1; { let q = 2 } return q`)).toBe(1)
|
||||
})
|
||||
|
||||
test("var loop counters are shared by closures, let counters are not", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const byVar = []
|
||||
for (var i = 0; i < 3; i++) byVar.push(() => i)
|
||||
const byLet = []
|
||||
for (let j = 0; j < 3; j++) byLet.push(() => j)
|
||||
return [byVar.map((f) => f()), byLet.map((f) => f())]
|
||||
`),
|
||||
).toEqual([
|
||||
[3, 3, 3],
|
||||
[0, 1, 2],
|
||||
])
|
||||
})
|
||||
|
||||
test("for...in and for...of var heads survive the loop", async () => {
|
||||
expect(await value(`for (var k in { a: 1 }) {} for (var [p, q] of [[1, 2]]) {} return [k, p, q]`)).toEqual([
|
||||
"a",
|
||||
1,
|
||||
2,
|
||||
])
|
||||
})
|
||||
|
||||
test("var and function declarations of the same name share a binding", async () => {
|
||||
expect(await value(`var fn = 1; function fn() {} return typeof fn`)).toBe("number")
|
||||
expect(await value(`function fn() {} var fn; return typeof fn`)).toBe("function")
|
||||
expect(await value(`function h() { var fn = 1; function fn() {} return typeof fn } return h()`)).toBe("number")
|
||||
})
|
||||
|
||||
test("a var named after a parameter keeps the argument until assigned", async () => {
|
||||
expect(await value(`function f(a) { var a; return a } return f(7)`)).toBe(7)
|
||||
expect(await value(`function f(a) { var a = 2; return a } return f(7)`)).toBe(2)
|
||||
})
|
||||
|
||||
test("var does not hoist across function boundaries", async () => {
|
||||
expect(await value(`return [typeof b, (() => { var b = 1; return b })()]; var b`)).toEqual(["undefined", 1])
|
||||
expect(
|
||||
await value(
|
||||
`function outer() { var o = 1; function inner() { var o = 2; return o } return [inner(), o] } return outer()`,
|
||||
),
|
||||
).toEqual([2, 1])
|
||||
})
|
||||
|
||||
test("switch cases, labels, and generators hoist var", async () => {
|
||||
expect(await value(`switch (1) { case 1: var s = 9 } label: { var lb = 1 } return [s, lb]`)).toEqual([9, 1])
|
||||
expect(await value(`function* gen() { var t = 1; yield t; var t = 2; yield t } return [...gen()]`)).toEqual([1, 2])
|
||||
})
|
||||
})
|
||||
|
||||
describe("switch case function hoisting", () => {
|
||||
test("function declarations are visible across all cases before their statement runs", async () => {
|
||||
expect(await value(`switch (1) { case 1: return foo(); function foo() { return "hoisted" } }`)).toBe("hoisted")
|
||||
expect(await value(`switch (2) { case 1: function foo() { return "a" } break; case 2: return foo() }`)).toBe("a")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Portions adapted from web-platform-tests at revision 863077959ca8c1a7ceecfbe2534b75d2527b9013:
|
||||
* - html/webappapis/atob/base64.any.js (btoa reference encoder, input list, and atob WebIDL cases)
|
||||
* - fetch/data-urls/resources/base64.json (copied to fixtures/wpt-base64.json)
|
||||
* - WebCryptoAPI/randomUUID.https.any.js
|
||||
*
|
||||
* Copyright © web-platform-tests contributors. Governed by the 3-Clause BSD license in LICENSE.wpt.
|
||||
*
|
||||
* `assert_throws_dom("InvalidCharacterError", …)` becomes a check on `error.name`: CodeMode has no
|
||||
* DOMException, so the name is carried on a plain Error.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CodeMode } from "../src/index.js"
|
||||
|
||||
const base64Cases = (await Bun.file(new URL("./fixtures/wpt-base64.json", import.meta.url)).json()) as Array<
|
||||
[string, Array<number> | null]
|
||||
>
|
||||
|
||||
const value = async (code: string) => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
||||
return result.value
|
||||
}
|
||||
|
||||
// The reference encoder from base64.any.js, run inside the interpreter so btoa is checked against
|
||||
// an independent implementation rather than against the host's btoa.
|
||||
const referenceEncoder = `
|
||||
function btoaLookup(idx) {
|
||||
if (idx < 26) return String.fromCharCode(idx + "A".charCodeAt(0))
|
||||
if (idx < 52) return String.fromCharCode(idx - 26 + "a".charCodeAt(0))
|
||||
if (idx < 62) return String.fromCharCode(idx - 52 + "0".charCodeAt(0))
|
||||
if (idx == 62) return "+"
|
||||
if (idx == 63) return "/"
|
||||
}
|
||||
function mybtoa(s) {
|
||||
s = String(s)
|
||||
for (var i = 0; i < s.length; i++) if (s.charCodeAt(i) > 255) return "INVALID_CHARACTER_ERR"
|
||||
var out = ""
|
||||
for (var i = 0; i < s.length; i += 3) {
|
||||
var groupsOfSix = [undefined, undefined, undefined, undefined]
|
||||
groupsOfSix[0] = s.charCodeAt(i) >> 2
|
||||
groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4
|
||||
if (s.length > i + 1) {
|
||||
groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4
|
||||
groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2
|
||||
}
|
||||
if (s.length > i + 2) {
|
||||
groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6
|
||||
groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f
|
||||
}
|
||||
for (var j = 0; j < groupsOfSix.length; j++) {
|
||||
out += typeof groupsOfSix[j] == "undefined" ? "=" : btoaLookup(groupsOfSix[j])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
function testBtoa(input) {
|
||||
var expected = mybtoa(input)
|
||||
if (expected === "INVALID_CHARACTER_ERR") {
|
||||
try { btoa(input) } catch (error) { return error.name === "InvalidCharacterError" ? "ok" : error.name }
|
||||
return "did not throw"
|
||||
}
|
||||
if (btoa(input) !== expected) return "btoa mismatch"
|
||||
if (atob(btoa(input)) !== String(input)) return "roundtrip mismatch"
|
||||
return "ok"
|
||||
}
|
||||
`
|
||||
|
||||
describe("btoa WPT parity (html/webappapis/atob/base64.any.js)", () => {
|
||||
test("every input encodes like the reference encoder and round-trips through atob", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
${referenceEncoder}
|
||||
var tests = ["עברית", "", "ab", "abc", "abcd", "abcde", "\\xff\\xff\\xc0", "\\0a", "a\\0b",
|
||||
undefined, null, 7, 12, 1.5, true, false, NaN, +Infinity, -Infinity, 0, -0]
|
||||
for (var i = 0; i < 258; i++) tests.push(String.fromCharCode(i))
|
||||
tests.push(String.fromCharCode(10000), String.fromCharCode(65534), String.fromCharCode(65535))
|
||||
tests.push(String.fromCharCode(0xd800, 0xdc00))
|
||||
var everything = ""
|
||||
for (var i = 0; i < 256; i++) everything += String.fromCharCode(i)
|
||||
tests.push(everything)
|
||||
return tests.map(testBtoa).filter((outcome) => outcome !== "ok")
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("atob WPT parity (fetch/data-urls/resources/base64.json)", () => {
|
||||
const idlCases: Array<[unknown, Array<number> | null]> = [
|
||||
[undefined, null],
|
||||
[null, [158, 233, 101]],
|
||||
[7, null],
|
||||
[12, [215]],
|
||||
[1.5, null],
|
||||
[true, [182, 187]],
|
||||
[false, null],
|
||||
[NaN, [53, 163]],
|
||||
[Infinity, [34, 119, 226, 158, 43, 114]],
|
||||
[-Infinity, null],
|
||||
[0, null],
|
||||
[-0, null],
|
||||
]
|
||||
|
||||
test(`${base64Cases.length} forgiving-base64 inputs decode to the expected bytes or throw InvalidCharacterError`, async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const cases = ${JSON.stringify(base64Cases)}
|
||||
return cases.flatMap(([input, output]) => {
|
||||
try {
|
||||
const result = atob(input)
|
||||
if (output === null) return [[input, "expected throw"]]
|
||||
const bytes = Array.from({ length: result.length }, (_, i) => result.charCodeAt(i))
|
||||
return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[input, bytes]]
|
||||
} catch (error) {
|
||||
return output === null && error.name === "InvalidCharacterError" ? [] : [[input, error.name]]
|
||||
}
|
||||
})
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("WebIDL argument conversion stringifies non-string inputs", async () => {
|
||||
const literal = (input: unknown) =>
|
||||
Object.is(input, -0)
|
||||
? "-0"
|
||||
: typeof input === "number" || input === undefined
|
||||
? String(input)
|
||||
: JSON.stringify(input)
|
||||
expect(
|
||||
await value(`
|
||||
const cases = [${idlCases.map(([input, output]) => `[${literal(input)}, ${JSON.stringify(output)}]`).join(",")}]
|
||||
return cases.flatMap(([input, output]) => {
|
||||
try {
|
||||
const result = atob(input)
|
||||
if (output === null) return [[String(input), "expected throw"]]
|
||||
// The source loop checks only the listed prefix of the decoded bytes.
|
||||
const bytes = output.map((_, i) => result.charCodeAt(i))
|
||||
return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[String(input), bytes]]
|
||||
} catch (error) {
|
||||
return output === null && error.name === "InvalidCharacterError" ? [] : [[String(input), error.name]]
|
||||
}
|
||||
})
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.randomUUID WPT parity (WebCryptoAPI/randomUUID.https.any.js)", () => {
|
||||
test("namespace format, version, and variant bits over 256 iterations without collision", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const uuids = new Set()
|
||||
const randomUUID = () => {
|
||||
const uuid = crypto.randomUUID()
|
||||
if (uuids.has(uuid)) throw new Error("uuid collision " + uuid)
|
||||
uuids.add(uuid)
|
||||
return uuid
|
||||
}
|
||||
const UUIDRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
|
||||
let format = true, version = true, variant = true
|
||||
for (let i = 0; i < 256; i++) format = format && UUIDRegex.test(randomUUID())
|
||||
for (let i = 0; i < 256; i++) version = version && (parseInt(randomUUID().split("-")[2].slice(0, 2), 16) & 0b11110000) === 0b01000000
|
||||
for (let i = 0; i < 256; i++) variant = variant && (parseInt(randomUUID().split("-")[3].slice(0, 2), 16) & 0b11000000) === 0b10000000
|
||||
return [format, version, variant, uuids.size]
|
||||
`),
|
||||
).toEqual([true, true, true, 768])
|
||||
})
|
||||
})
|
||||
@@ -166,7 +166,7 @@ export const catalog = (inventory: Inventory) => {
|
||||
)
|
||||
const root: CatalogNode = { children: new Map() }
|
||||
for (const namespace of inventory.namespaces?.values() ?? []) getNode(root, namespace.name).namespace = namespace
|
||||
for (const tool of runtime(inventory, () => Effect.fail(toolError("Execute context is unavailable"))).catalog())
|
||||
for (const tool of runtime(inventory, () => Effect.fail(toolError("Execute context is unavailable"))).catalog)
|
||||
getNode(root, tool.path).tool = {
|
||||
type: "tool",
|
||||
name: tool.path.split(".").at(-1) ?? tool.path,
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigInstructionPlugin from "./instruction.js"
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Global } from "@opencode/util/global"
|
||||
import { dirname, join, relative } from "path"
|
||||
import { dirname, join } from "path"
|
||||
import { Effect, PubSub, Semaphore, Stream } from "effect"
|
||||
import { Watcher } from "../../filesystem/watcher.js"
|
||||
import { InstructionDiscovery } from "../../instruction-discovery.js"
|
||||
@@ -34,8 +34,6 @@ export const Plugin = define({
|
||||
const home = yield* fs.resolve(global.home)
|
||||
const project = discovery.project && FSUtil.contains(root, start)
|
||||
const stop = FSUtil.contains(home, start) ? home : root
|
||||
const ancestors = project ? ancestorDirectories(start, stop) : []
|
||||
const boundary = ancestors.at(-1) ?? stop
|
||||
const globalFile = yield* fs.resolve(join(global.config, "AGENTS.md"))
|
||||
const loaded: { current: Loaded } = { current: { type: "available", files: [] } }
|
||||
|
||||
@@ -45,7 +43,7 @@ export const Plugin = define({
|
||||
const candidates = [
|
||||
...(discovery.global ? [globalFile] : []),
|
||||
...(project
|
||||
? ancestors
|
||||
? ancestorDirectories(start, stop)
|
||||
.map((directory) => join(directory, "AGENTS.md"))
|
||||
.filter((file) => discovery.global || file !== globalFile)
|
||||
: []),
|
||||
@@ -69,10 +67,7 @@ export const Plugin = define({
|
||||
|
||||
const projectSource = Effect.fn("ConfigInstructionPlugin.projectSource")(function* () {
|
||||
if (!project) return []
|
||||
const walked = yield* Effect.forEach(
|
||||
yield* fs.up({ targets: ["AGENTS.md"], start, stop: boundary }),
|
||||
fs.resolve,
|
||||
)
|
||||
const walked = yield* Effect.forEach(yield* fs.up({ targets: ["AGENTS.md"], start, stop }), fs.resolve)
|
||||
const discovered = new Set(walked.filter((file) => discovery.global || file !== globalFile))
|
||||
const files = yield* Effect.forEach(discovered, read, { concurrency: "unbounded" })
|
||||
if (files.some((file) => file === undefined)) return Instructions.unavailable
|
||||
@@ -136,9 +131,6 @@ export const Plugin = define({
|
||||
})
|
||||
|
||||
function ancestorDirectories(start: string, stop: string): string[] {
|
||||
const result = [start]
|
||||
if (relative(start, stop) === "") return result
|
||||
const parent = dirname(start)
|
||||
if (parent === start) throw new Error(`Instruction boundary ${stop} is not an ancestor of ${start}`)
|
||||
return [...result, ...ancestorDirectories(parent, stop)]
|
||||
if (start === stop) return [start]
|
||||
return [start, ...ancestorDirectories(dirname(start), stop)]
|
||||
}
|
||||
|
||||
@@ -446,7 +446,6 @@ export const layer = Layer.effect(
|
||||
const compactionRequest = (
|
||||
input: ExecuteInput,
|
||||
messages: readonly SessionMessage.Info[],
|
||||
prompt: Message[],
|
||||
webSocket?: "session",
|
||||
) => {
|
||||
const context = input.context
|
||||
@@ -466,7 +465,6 @@ export const layer = Layer.effect(
|
||||
messages: [
|
||||
...transcript.messages,
|
||||
...(input.instructionUpdate ? [Message.system(input.instructionUpdate)] : []),
|
||||
...prompt,
|
||||
],
|
||||
webSocket,
|
||||
})
|
||||
@@ -486,7 +484,7 @@ export const layer = Layer.effect(
|
||||
inputID: input.inputID,
|
||||
error: { type: "provider.unsupported-operation", message },
|
||||
})
|
||||
const prepared = yield* compactionRequest(input, context.messages, [], "session")
|
||||
const prepared = yield* compactionRequest(input, context.messages, "session")
|
||||
if (prepared.event.result) {
|
||||
yield* started(input, "")
|
||||
return yield* supplied(input, prepared.event.result, "")
|
||||
@@ -603,10 +601,12 @@ export const layer = Layer.effect(
|
||||
)
|
||||
// Checkpoints from the previous template ran far longer than this one asks for; its catch-all heading identifies them.
|
||||
const legacy = previous?.summary.includes(LEGACY_HEADING) ?? false
|
||||
const prepared = yield* compactionRequest(input, history.messages, [
|
||||
Message.user(buildPrompt(previous !== undefined, legacy)),
|
||||
])
|
||||
const prepared = yield* compactionRequest(input, history.messages)
|
||||
if (prepared.event.result) return yield* supplied(input, prepared.event.result, history.recent)
|
||||
// Hooks see the transcript alone; the summary prompt is appended after they run.
|
||||
const first = LLMRequest.update(prepared.request, {
|
||||
messages: [...prepared.request.messages, Message.user(buildPrompt(previous !== undefined, legacy))],
|
||||
})
|
||||
// Both requests share the retry allowance; rejected output never enters the reminder request.
|
||||
const transient = SessionRunnerRetry.transient(yield* SessionRunnerRetry.policy(context.session.id), {
|
||||
agent: context.agent.id,
|
||||
@@ -614,10 +614,10 @@ export const layer = Layer.effect(
|
||||
hook: prepared.retry,
|
||||
})
|
||||
for (const request of [
|
||||
prepared.request,
|
||||
LLMRequest.update(prepared.request, {
|
||||
first,
|
||||
LLMRequest.update(first, {
|
||||
messages: [
|
||||
...prepared.request.messages,
|
||||
...first.messages,
|
||||
Message.user(
|
||||
"The previous response did not fill in the required summary template. Do not call tools. Return the summary as text using the exact section headings from the template.",
|
||||
),
|
||||
|
||||
@@ -358,6 +358,14 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
}
|
||||
const session = yield* insertSession(sessionID, { parent_id: parentID })
|
||||
const modelRequests = yield* SessionModelRequest.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
let hooked = 0
|
||||
yield* hooks.register("session", "compaction", (event) =>
|
||||
Effect.sync(() => {
|
||||
hooked = event.messages.length
|
||||
expect(JSON.stringify(event.messages)).not.toContain("Summarize only what")
|
||||
}),
|
||||
)
|
||||
const messages = [
|
||||
userMessage,
|
||||
SessionMessage.Shell.make({
|
||||
@@ -410,6 +418,8 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Use Effect services and generators.")
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("User shell pwd completed: /project")
|
||||
expect(requests[0]?.messages).toHaveLength(hooked + 1)
|
||||
expect(JSON.stringify(requests[0]?.messages.at(-1))).toContain("Summarize only what")
|
||||
expect(JSON.stringify(requests[0]?.messages)).not.toContain("display-only-output")
|
||||
// The compaction message carries its own request usage so clients can show what compacting cost.
|
||||
expect(yield* store.context(sessionID)).toMatchObject([
|
||||
|
||||
@@ -169,7 +169,12 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||
: members.some((id) => (data.session.form.list(id)?.length ?? 0) > 0)
|
||||
? ("question" as const)
|
||||
: (false as const),
|
||||
busy: members.some((id) => data.session.status(id) === "running" || data.session.pending.list(id).length > 0),
|
||||
// Parked synthetic context (user shells, plan reminders) stays pending without execution; only work counts as busy.
|
||||
busy: members.some(
|
||||
(id) =>
|
||||
data.session.status(id) === "running" ||
|
||||
data.session.pending.list(id).some((item) => item.type !== "synthetic"),
|
||||
),
|
||||
renaming: data.session.title.pending(session),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1204,8 +1204,9 @@ kind of request a session issues has its own hook, so a plugin can treat the age
|
||||
differently:
|
||||
|
||||
- `context` runs for the agent loop, including tool-driven continuations.
|
||||
- `compaction` runs for checkpoint summaries. Set `result` to record the compaction yourself and skip the model
|
||||
call; it takes the same fields as a completed compaction message.
|
||||
- `compaction` runs for checkpoint summaries. `messages` is the transcript being summarized; OpenCode appends its
|
||||
summary prompt after hooks run. Set `result` to record the compaction yourself and skip the model call; it takes the
|
||||
same fields as a completed compaction message.
|
||||
- `generate` runs for transient `ctx.session.generate` calls.
|
||||
- `title` runs for title generation. It has no `agent` or `tools`. Set `result` to supply the title yourself.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user