Compare commits

...
Author SHA1 Message Date
Brendonovich 3178cf10dd fix(app): paste plain text into session titles 2026-09-22 08:12:08 +00:00
opencode-agent[bot]andBrendonovich dcfe1ec7bd feat(app): add /btw side question panel (#49750)
Co-authored-by: Brendonovich <Brendonovich@users.noreply.github.com>
2026-09-22 15:20:09 +08:00
opencode-agent[bot] ceace24a3e chore: update nix node_modules hashes 2026-09-22 05:45:49 +00:00
Aiden Cline 19e1357a06 fix(core): stop forcing a consent prompt on MCP OAuth login (#50519) 2026-09-22 00:25:06 -05:00
opencode-agent[bot] 4b381ac6a1 fix(tui): clear the active session tab (#50524) 2026-09-22 00:53:42 -04:00
Aiden Cline 07d48e1ffb feat(ai): add experimental evaluation API (#50506) 2026-09-21 23:30:38 -05:00
Aiden Cline 9fdcb8da41 fix(codemode): coerce built-in arguments as in JS instead of requiring numbers and strings (#50492) 2026-09-21 23:04:04 -05:00
Aiden Cline ba61ac6730 fix(ai): preserve Vertex function call ids (#50504) 2026-09-21 23:03:24 -05:00
Aiden Cline 94b9133910 fix(core): share child prompt cache affinity (#50495) 2026-09-21 22:44:43 -05:00
Aiden Cline 6f8c5ae0aa fix(codemode): bind generator parameters at the call, not the first next() (#50489) 2026-09-21 22:42:10 -05:00
Aiden Cline 60673aaef3 fix(core): run session HTTP hooks on the AI SDK route (#50487) 2026-09-21 22:41:06 -05:00
Aiden Cline 651529d64e fix(codemode): coerce any value to a property key (#50479) 2026-09-21 21:35:20 -05:00
opencode-agent[bot] 532f25d0d4 feat(tui): restore sidebar onboarding (#50475) 2026-09-21 22:17:22 -04:00
opencode-agent[bot]andjlongster 643c4c3500 fix: remove first-month Go pricing (#50473)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
2026-09-21 21:55:12 -04:00
opencode-agent[bot]andBrendonovich 9d531435b4 fix(desktop): stop signing macOS DMGs (#50469)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-22 01:35:21 +00:00
opencode-agent[bot] 5b9dc35eec feat(tui): add automatic tabs mode (#50456) 2026-09-22 01:00:26 +00:00
Aiden Cline 1814dd9799 test: stabilize Windows CI without longer timeouts (#50454) 2026-09-21 18:30:30 -05:00
Aiden Cline 1e1cd042ea feat(codemode): name the closest tool in unknown-tool errors (#50455) 2026-09-21 18:14:31 -05:00
Aiden Cline 02566f6219 fix(codemode): live Map/Set forEach, generator prototypes, repeated function declarations, delete on non-references (#50450) 2026-09-21 18:09:45 -05:00
opencode-agent[bot]andrekram1-node f488aa3f79 fix(tui): persist MCP sidebar state (#50447)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-21 17:28:23 -05:00
Aiden Cline 97457ec7a3 feat(codemode): accept tools.-prefixed namespaces in search (#50438) 2026-09-21 16:59:31 -05:00
James Long d62049aab4 fix(tui): set renderer listener budget to 15 (#50442) 2026-09-21 17:55:27 -04:00
Aiden Cline 096ac95773 fix(codemode): drop the parameter name from rendered signatures (#50435) 2026-09-21 16:44:46 -05:00
Aiden Cline 4cc9b90f27 fix(codemode): add Promise.try and surface a failing iterator close on generator return (#50419) 2026-09-21 15:35:11 -05:00
James Long 990463aa9f refactor(tui): favor dark theme base (#50412) 2026-09-21 16:23:38 -04:00
opencode-agent[bot]andrekram1-node cd39063622 test(core): stabilize browser idle attachment (#50418)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-21 14:45:33 -05:00
Aiden Cline 4c944a86d7 fix(core): tell the model search is synchronous (#50417) 2026-09-21 14:35:46 -05:00
Aiden Cline 97e833a297 feat(codemode): add Iterator helpers and Iterator.from (#50410) 2026-09-21 14:14:24 -05:00
xin zhuangandSimon Klee b8aa08f260 fix(tui): complete Mini exit aliases (#50388)
Co-authored-by: Simon Klee <hello@simonklee.dk>
2026-09-21 17:04:58 +00:00
Aiden Cline 3f0118022b feat(codemode): add Promise.withResolvers, print errors in console, name values in iterable errors (#50389) 2026-09-21 11:56:15 -05:00
Simon Klee 6f76c31ca7 tui: resize /btw dialog to terminal (#50392) 2026-09-21 16:44:18 +00:00
Aiden Cline f90beeb9b8 fix(core): say plainly that catalog tools and search only work inside execute (#50384) 2026-09-21 11:13:33 -05:00
Adam 4b9a3d80fc feat(core): enforce Console-managed policies (#49729) 2026-09-21 11:10:57 -05:00
Aiden Cline 1316576720 refactor(codemode): type program values and let built-ins answer for themselves (#50191) 2026-09-21 11:03:16 -05:00
258 changed files with 5389 additions and 1922 deletions
+3 -2
View File
@@ -112,11 +112,12 @@ jobs:
- name: Run unit tests
timeout-minutes: 20
run: |
# The runners have four vCPUs, and each Bun test process performs its own concurrent work.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
GITHUB_ACTIONS=false bun turbo test
GITHUB_ACTIONS=false bun turbo test --concurrency=3
exit 0
fi
GITHUB_ACTIONS=false bun turbo test --affected
GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-/jah4P2a0aGJNJ0aMdlFEbGHAXx33lqxHbc7UmpLFDg=",
"aarch64-linux": "sha256-L3SoZ24qNXicsE2FK6LATQjOmjPxY679RjugrUyO/1Y=",
"aarch64-darwin": "sha256-pI9NT8KWUPi+JCk6DYMqIAYmBqTbi13uL4VdNV3WS6Y=",
"x86_64-darwin": "sha256-rMAGhTTz46KA5Ya7E5J0af7Bn1QzTDhTaNfNJm8qfsw="
"x86_64-linux": "sha256-8hc0Typ9cA1NDpToM0Pq7q3AutSp+I80Sakthq10F4c=",
"aarch64-linux": "sha256-7bzI4zWOdxuoMdMHMuqIAOgnuzWiHmpdCMYCYPbs+3c=",
"aarch64-darwin": "sha256-EWDHUSVH2AjsNvoKvzC392H6GjCWVh07cOg2x0mAsng=",
"x86_64-darwin": "sha256-7b2BRdRRVG+PMCSd/cm4E+slziwWIKLVttYkotdSrK4="
}
}
+41
View File
@@ -29,6 +29,47 @@ await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
Run `LLMClient.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses, Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
## Experimental evaluation
Evaluation models compare shared state with typed choice, score, and boolean questions. The API is
isolated under an experimental entrypoint and provider namespace while the contract evolves:
```ts
import { Effect } from "effect"
import { Evaluation, EvaluationClient } from "@opencode/ai/experimental"
import { TypeSafeAI } from "@opencode/ai/providers"
const model = TypeSafeAI.configure().experimental.evaluation("jev-latest")
const program = Evaluation.evaluate({
model,
state: "I was charged twice. Please refund the duplicate payment.",
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs prompt attention", "Blocking revenue"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
},
})
const response = await Effect.runPromise(program.pipe(Effect.provide(EvaluationClient.fetchLayer)))
console.log(response.answers.department.choice)
console.log(response.answers.refund.probability)
```
`TypeSafeAI` reads `TYPESAFE_API_KEY`. `OpenCodeZen` exposes the same selector and reads
`OPENCODE_API_KEY`. The common API uses `boolean`; System One routes lower it to native `noul`.
Choice and score confidence plus score legends remain available in provider metadata, and the
provider's rounded probabilities are returned unchanged.
## Alibaba Cloud Model Studio
`Alibaba` provides standard Model Studio inference. Configure a region explicitly, then select
+29
View File
@@ -0,0 +1,29 @@
export { EvaluationClient } from "./experimental/evaluation-client.js"
export {
BooleanAnswer,
BooleanQuestion,
ChoiceAnswer,
ChoiceQuestion,
Evaluation,
EvaluationAnswer,
EvaluationInput,
EvaluationModel,
EvaluationModelSchema,
EvaluationQuestion,
EvaluationRequest,
EvaluationResponse,
EvaluationRounding,
ScoreAnswer,
ScoreQuestion,
} from "./experimental/evaluation.js"
export type {
AnswerFor,
AnswersFor,
EvaluationModelOptions,
EvaluationOptions,
EvaluationQuestions,
EvaluationRequestFor,
EvaluationRequestInput,
EvaluationResponseFor,
EvaluationRoute,
} from "./experimental/evaluation.js"
@@ -0,0 +1,54 @@
import { Context, Effect, Layer } from "effect"
import { RequestExecutor } from "../route/executor.js"
import { mergeHttpOptions, type AIError } from "../schema/index.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import {
type EvaluationOptions,
type EvaluationQuestions,
type EvaluationRequestFor,
type EvaluationResponseFor,
} from "./evaluation.js"
export type Execute = RequestExecutor.Interface["execute"]
export interface Interface {
readonly evaluate: <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
) => Effect.Effect<EvaluationResponseFor<Questions>, AIError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/Experimental/EvaluationClient") {}
export const evaluate = <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service> =>
Effect.flatMap(Service, (client) => client.evaluate(request))
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
return Service.of({
evaluate: (request) =>
request.model.route.evaluate(
{
...sanitizeSurrogates({
...request,
model: undefined,
http: mergeHttpOptions(request.model.http, request.http),
}),
model: request.model,
},
executor.execute,
),
})
}),
)
export const fetchLayer = layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
export const EvaluationClient = {
Service,
layer,
fetchLayer,
evaluate,
} as const
+245
View File
@@ -0,0 +1,245 @@
import { Effect, Schema } from "effect"
import {
AIError,
HttpOptions,
InvalidRequestError,
ModelID,
ProviderID,
ProviderMetadata,
Usage,
} from "../schema/index.js"
import { EvaluationClient, Service, type Execute } from "./evaluation-client.js"
export const EvaluationInput = Schema.Union([Schema.String, Schema.JsonObject, Schema.Array(Schema.Json)])
export type EvaluationInput = Schema.Schema.Type<typeof EvaluationInput>
const EvaluationCriterion = Schema.NullOr(EvaluationInput)
const ChoiceCriteria = Schema.Record(Schema.String, EvaluationCriterion).pipe(
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
message: "Choice criteria must be a nonempty option map",
}),
)
export const ChoiceQuestion = Schema.Struct({
type: Schema.Literal("choice"),
instructions: EvaluationInput,
criteria: ChoiceCriteria,
})
export type ChoiceQuestion = Schema.Schema.Type<typeof ChoiceQuestion>
export const ScoreQuestion = Schema.Struct({
type: Schema.Literal("score"),
instructions: EvaluationInput,
criteria: Schema.Array(EvaluationCriterion).check(Schema.isMinLength(2)),
})
export type ScoreQuestion = Schema.Schema.Type<typeof ScoreQuestion>
export const BooleanQuestion = Schema.Struct({
type: Schema.Literal("boolean"),
instructions: EvaluationInput,
criteria: Schema.optional(
Schema.Struct({
true: Schema.optional(EvaluationCriterion),
false: Schema.optional(EvaluationCriterion),
}),
),
})
export type BooleanQuestion = Schema.Schema.Type<typeof BooleanQuestion>
export const EvaluationQuestion = Schema.Union([ChoiceQuestion, ScoreQuestion, BooleanQuestion]).pipe(
Schema.toTaggedUnion("type"),
)
export type EvaluationQuestion = Schema.Schema.Type<typeof EvaluationQuestion>
export type EvaluationQuestions = Readonly<Record<string, EvaluationQuestion>>
const EvaluationQuestions = Schema.Record(Schema.String, EvaluationQuestion).pipe(
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
message: "Evaluation questions must be a nonempty map",
}),
)
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
export const ChoiceAnswer = Schema.Struct({
type: Schema.Literal("choice"),
choice: Schema.String,
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
})
export type ChoiceAnswer = Schema.Schema.Type<typeof ChoiceAnswer>
export const ScoreAnswer = Schema.Struct({
type: Schema.Literal("score"),
score: Schema.Number,
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
})
export type ScoreAnswer = Schema.Schema.Type<typeof ScoreAnswer>
export const BooleanAnswer = Schema.Struct({
type: Schema.Literal("boolean"),
probability: Probability,
})
export type BooleanAnswer = Schema.Schema.Type<typeof BooleanAnswer>
export const EvaluationAnswer = Schema.Union([ChoiceAnswer, ScoreAnswer, BooleanAnswer]).pipe(
Schema.toTaggedUnion("type"),
)
export type EvaluationAnswer = Schema.Schema.Type<typeof EvaluationAnswer>
export type AnswerFor<Question extends EvaluationQuestion> = Question extends {
readonly type: "choice"
readonly criteria: infer Criteria
}
? {
readonly type: "choice"
readonly choice: Extract<keyof Criteria, string>
readonly probabilities?: Readonly<Record<Extract<keyof Criteria, string>, number>>
}
: Question extends { readonly type: "score" }
? ScoreAnswer
: BooleanAnswer
export type AnswersFor<Questions extends EvaluationQuestions> = {
readonly [ID in keyof Questions]: AnswerFor<Questions[ID]>
}
export type EvaluationOptions = Record<string, unknown>
export interface EvaluationRoute<Options extends EvaluationOptions = EvaluationOptions> {
readonly id: string
readonly evaluate: <const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
execute: Execute,
) => Effect.Effect<EvaluationResponseFor<Questions>, AIError>
}
export class EvaluationModel<Options extends EvaluationOptions = EvaluationOptions> {
declare protected readonly _Options: (options: Options) => Options
readonly id: ModelID
readonly provider: ProviderID
readonly route: EvaluationRoute<Options>
readonly http?: HttpOptions
constructor(input: EvaluationModel.Input<Options>) {
this.id = input.id
this.provider = input.provider
this.route = input.route
this.http = input.http
}
static make<Options extends EvaluationOptions = EvaluationOptions>(input: EvaluationModel.MakeInput<Options>) {
return new EvaluationModel<Options>({
id: ModelID.make(input.id),
provider: ProviderID.make(input.provider),
route: input.route,
http: input.http,
})
}
}
export namespace EvaluationModel {
export interface Input<Options extends EvaluationOptions = EvaluationOptions> {
readonly id: ModelID
readonly provider: ProviderID
readonly route: EvaluationRoute<Options>
readonly http?: HttpOptions
}
export interface MakeInput<Options extends EvaluationOptions = EvaluationOptions>
extends Omit<Input<Options>, "id" | "provider"> {
readonly id: string | ModelID
readonly provider: string | ProviderID
}
}
export const EvaluationModelSchema = Schema.declare(
(value): value is EvaluationModel => value instanceof EvaluationModel,
{
expected: "Evaluation.Model",
},
)
export class EvaluationRequest extends Schema.Class<EvaluationRequest>("Evaluation.Request")({
model: EvaluationModelSchema,
state: EvaluationInput,
questions: EvaluationQuestions,
options: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
http: Schema.optional(HttpOptions),
}) {
declare protected readonly _EvaluationRequest: void
}
export type EvaluationModelOptions<Model> = Model extends EvaluationModel<infer Options> ? Options : never
export type EvaluationRequestFor<
Options extends EvaluationOptions = EvaluationOptions,
Questions extends EvaluationQuestions = EvaluationQuestions,
> = Omit<EvaluationRequest, "model" | "questions" | "options"> & {
readonly model: EvaluationModel<Options>
readonly questions: Questions
readonly options?: Options
}
export type EvaluationRequestInput<
Model extends object = EvaluationModel,
Questions extends EvaluationQuestions = EvaluationQuestions,
> = Omit<ConstructorParameters<typeof EvaluationRequest>[0], "model" | "questions" | "options" | "http"> & {
readonly model: Model
readonly questions: Questions
readonly options?: NoInfer<EvaluationModelOptions<Model>>
readonly http?: HttpOptions.Input
} & (Model extends EvaluationModel<EvaluationModelOptions<Model>> ? unknown : never)
export class EvaluationRounding extends Schema.Class<EvaluationRounding>("Evaluation.Rounding")({
probabilityDecimals: Schema.optional(Schema.Int),
scoreDecimals: Schema.optional(Schema.Int),
}) {}
export class EvaluationResponse extends Schema.Class<EvaluationResponse>("Evaluation.Response")({
model: ModelID,
answers: Schema.Record(Schema.String, EvaluationAnswer),
usage: Schema.optional(Usage),
rounding: Schema.optional(EvaluationRounding),
providerMetadata: Schema.optional(ProviderMetadata),
}) {}
export type EvaluationResponseFor<Questions extends EvaluationQuestions> = Omit<EvaluationResponse, "answers"> & {
readonly answers: AnswersFor<Questions>
}
export function request<const Model extends object, const Questions extends EvaluationQuestions>(
input: EvaluationRequestInput<Model, Questions>,
): EvaluationRequestFor<EvaluationModelOptions<Model>, Questions>
export function request(input: EvaluationRequest): EvaluationRequest
export function request(input: EvaluationRequest | EvaluationRequestInput) {
if (input instanceof EvaluationRequest) return input
return new EvaluationRequest({
...input,
model: input.model as unknown as EvaluationModel,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
}
export function evaluate<const Model extends object, const Questions extends EvaluationQuestions>(
input: EvaluationRequestInput<Model, Questions>,
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>
export function evaluate(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
return Effect.try({
try: () => (input instanceof EvaluationRequest ? input : request(input)),
catch: (cause) =>
new AIError({
reason: new InvalidRequestError({
message: cause instanceof Error ? cause.message : String(cause),
cause,
}),
}),
}).pipe(
Effect.flatMap((request) =>
EvaluationClient.evaluate(request as EvaluationRequestFor<EvaluationOptions, EvaluationQuestions>),
),
)
}
export const Evaluation = {
request,
evaluate,
} as const
+222
View File
@@ -0,0 +1,222 @@
import { Effect, Schema } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http"
import {
BooleanAnswer,
ChoiceAnswer,
ChoiceQuestion,
EvaluationInput,
EvaluationModel,
EvaluationResponse,
EvaluationRounding,
ScoreAnswer,
ScoreQuestion,
type EvaluationResponseFor,
type EvaluationRoute,
} from "./evaluation.js"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js"
import {
AIError,
HttpContext,
HttpOptions,
InvalidProviderOutputError,
InvalidRequestError,
ModelID,
Usage,
mergeJsonRecords,
} from "../schema/index.js"
const Noul = Schema.Struct({
type: Schema.Literal("noul"),
instructions: EvaluationInput,
criteria: Schema.optional(
Schema.Struct({
true: Schema.optional(Schema.NullOr(EvaluationInput)),
false: Schema.optional(Schema.NullOr(EvaluationInput)),
}),
),
})
const Question = Schema.Union([
ChoiceQuestion.pipe(
Schema.refine((x): x is typeof x => Object.keys(x.criteria).length <= 255, {
message: "System One Choice questions support at most 255 options",
}),
),
ScoreQuestion.pipe(
Schema.refine((x): x is typeof x => x.criteria.length <= 10, {
message: "System One Score questions support at most 10 levels",
}),
),
Noul,
])
const Request = Schema.StructWithRest(
Schema.Struct({
model: Schema.String,
state: EvaluationInput,
questions: Schema.Record(Schema.String, Question),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability })
const Choice = Schema.Struct({
type: Schema.Literal("choice"),
choice: Schema.String,
probabilities: Schema.Record(Schema.String, Probability),
confidence: Schema.optional(Probability),
})
const Score = Schema.Struct({
type: Schema.Literal("score"),
score: Schema.Number,
probabilities: Schema.Record(Schema.String, Probability),
legend: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
confidence: Schema.optional(Probability),
})
const NativeUsage = Schema.Struct({
input_tokens: Schema.optional(Schema.Number),
output_tokens: Schema.optional(Schema.Number),
})
const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Request))
const exact = (x: Readonly<Record<string, unknown>>, keys: ReadonlyArray<string>) =>
Object.keys(x).length === keys.length && keys.every((key) => Object.hasOwn(x, key))
export interface ModelInput {
readonly id: string | ModelID
readonly provider: string
readonly providerMetadataKey: string
readonly auth: AuthDefinition
readonly baseURL: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions
}
export const model = (cfg: ModelInput) => {
const route: EvaluationRoute = {
id: "system-one",
evaluate: (req, send) =>
Effect.gen(function* () {
const url = new URL(`${cfg.baseURL.replace(/\/$/, "")}/systemone`)
Object.entries(req.http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value))
const body = yield* encode({
...mergeJsonRecords(req.options, req.http?.body),
model: req.model.id,
state: req.state,
questions: Object.fromEntries(
Object.entries(req.questions).map(([id, x]) => [id, x.type === "boolean" ? { ...x, type: "noul" } : x]),
),
}).pipe(
Effect.mapError(
(cause) => new AIError({ reason: new InvalidRequestError({ message: cause.message, cause }) }),
),
)
const headers = yield* Auth.toEffect(cfg.auth)({
request: req,
method: "POST",
url: url.toString(),
body,
headers: Headers.fromInput({ ...cfg.headers, ...req.http?.headers }),
})
const res = yield* send(
HttpClientRequest.post(url).pipe(
HttpClientRequest.setHeaders(headers),
HttpClientRequest.bodyText(body, "application/json"),
),
)
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
const fail = (message: string, cause: unknown, body?: string) =>
new AIError({ reason: new InvalidProviderOutputError({ route: route.id, message, body, http, cause }) })
const text = yield* res.text.pipe(
Effect.mapError((cause) => fail("Failed to read the System One response", cause)),
)
const entries = Object.entries(req.questions)
const output = Schema.Struct({
model: Schema.String,
answers: Schema.Struct(
Object.fromEntries(
entries.map(([id, question]) => {
if (question.type === "boolean") return [id, NoulAnswer]
if (question.type === "choice") {
const keys = Object.keys(question.criteria)
return [
id,
Choice.pipe(
Schema.refine(
(x): x is typeof x =>
Object.hasOwn(question.criteria, x.choice) && exact(x.probabilities, keys),
{ message: `Question "${id}" returned an invalid choice answer` },
),
),
]
}
const keys = question.criteria.map((_, index) => String(index))
return [
id,
Score.pipe(
Schema.refine(
(x): x is typeof x =>
x.score >= 0 && x.score <= question.criteria.length - 1 && exact(x.probabilities, keys),
{ message: `Question "${id}" returned an invalid score answer` },
),
),
]
}),
) as Record<string, Schema.Codec<unknown>>,
),
usage: Schema.optional(NativeUsage),
})
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(output))(text).pipe(
Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)),
)
const confidence: Record<string, number> = {}
const legend: Record<string, Record<string, Schema.Json>> = {}
const answers = Object.fromEntries(
entries.map(([id, question]) => {
const answer = data.answers[id]
if (question.type === "boolean")
return [
id,
{ type: "boolean", probability: (answer as typeof NoulAnswer.Type).noul } satisfies BooleanAnswer,
]
if (question.type === "choice") {
const value = answer as typeof Choice.Type
if (value.confidence !== undefined) confidence[id] = value.confidence
return [
id,
{ type: "choice", choice: value.choice, probabilities: value.probabilities } satisfies ChoiceAnswer,
]
}
const value = answer as typeof Score.Type
if (value.confidence !== undefined) confidence[id] = value.confidence
if (value.legend !== undefined) legend[id] = value.legend
return [id, { type: "score", score: value.score, probabilities: value.probabilities } satisfies ScoreAnswer]
}),
)
const meta = {
...(Object.keys(confidence).length === 0 ? {} : { confidence }),
...(Object.keys(legend).length === 0 ? {} : { legend }),
}
return new EvaluationResponse({
model: ModelID.make(data.model),
answers,
usage: data.usage
? new Usage({
inputTokens: data.usage.input_tokens,
outputTokens: data.usage.output_tokens,
totalTokens:
data.usage.input_tokens === undefined && data.usage.output_tokens === undefined
? undefined
: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
providerMetadata: { [cfg.providerMetadataKey]: data.usage },
})
: undefined,
rounding: new EvaluationRounding({ probabilityDecimals: 2, scoreDecimals: 2 }),
providerMetadata: Object.keys(meta).length === 0 ? undefined : { [cfg.providerMetadataKey]: meta },
}) as EvaluationResponseFor<typeof req.questions>
}),
}
return EvaluationModel.make({ id: cfg.id, provider: cfg.provider, route, http: cfg.http })
}
export const SystemOne = { model } as const
+1 -11
View File
@@ -38,23 +38,13 @@ export type Settings = ProviderPackage.Settings &
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
const { serviceTier: _, ...body } = yield* Gemini.protocol.body.from(request)
// Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400,
// unlike AI Studio, so history minted there cannot be lowered verbatim.
const contents = body.contents.map((content) => ({
...content,
parts: (content.parts ?? []).map((part) => {
if ("functionCall" in part) return { ...part, functionCall: { ...part.functionCall, id: undefined } }
if ("functionResponse" in part) return { ...part, functionResponse: { ...part.functionResponse, id: undefined } }
return part
}),
}))
const value = request.providerOptions?.labels
const labels = ProviderShared.isRecord(value)
? Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
: undefined
return { ...body, contents, labels }
return { ...body, labels }
})
const protocol = {
+2
View File
@@ -24,8 +24,10 @@ export * as Moonshot from "./moonshot.js"
export * as OpenAI from "./openai.js"
export * as OpenAICompatible from "./openai-compatible.js"
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js"
export * as OpenCodeZen from "./opencode-zen.js"
export * as OpenRouter from "./openrouter.js"
export * as TogetherAI from "./togetherai.js"
export * as TypeSafeAI from "./typesafe-ai.js"
export * as XAI from "./xai.js"
export * as ZAI from "./zai.js"
export * as ZAICodingPlan from "./zai-coding-plan.js"
+31
View File
@@ -0,0 +1,31 @@
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import { SystemOne } from "../experimental/system-one.js"
export const id = ProviderID.make("opencode")
const baseURL = "https://opencode.ai/zen/v1"
export type Options = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
}
export const configure = (input: Options = {}) => {
const evaluation = (modelID: string | ModelID) =>
SystemOne.model({
id: modelID,
provider: id,
providerMetadataKey: "opencode",
auth: AuthOptions.bearer(input, "OPENCODE_API_KEY"),
baseURL: input.baseURL ?? baseURL,
headers: input.headers,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
return { id, experimental: { evaluation }, configure }
}
export const provider = configure()
export const experimental = provider.experimental
export * as OpenCodeZen from "./opencode-zen.js"
+31
View File
@@ -0,0 +1,31 @@
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import { SystemOne } from "../experimental/system-one.js"
export const id = ProviderID.make("typesafe-ai")
const baseURL = "https://api.typesafe.ai/v1"
export type Options = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
}
export const configure = (input: Options = {}) => {
const evaluation = (modelID: string | ModelID) =>
SystemOne.model({
id: modelID,
provider: id,
providerMetadataKey: "typesafe",
auth: AuthOptions.bearer(input, "TYPESAFE_API_KEY"),
baseURL: input.baseURL ?? baseURL,
headers: input.headers,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
return { id, experimental: { evaluation }, configure }
}
export const provider = configure()
export const experimental = provider.experimental
export * as TypeSafeAI from "./typesafe-ai.js"
+173
View File
@@ -0,0 +1,173 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { Evaluation, EvaluationClient } from "../src/experimental.js"
import { OpenCodeZen, TypeSafeAI } from "../src/providers.js"
import { it } from "./lib/effect.js"
import { dynamicResponse } from "./lib/http.js"
describe("experimental Evaluation", () => {
it.effect("evaluates typed questions through System One", () =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
model: TypeSafeAI.configure({
apiKey: "test",
baseURL: "https://typesafe.test/v1/",
headers: { "x-default": "yes" },
http: { body: { deployment: "test" }, query: { api: "v1" } },
}).experimental.evaluation("jev-latest"),
state: { ticket: "Please refund the duplicate charge." },
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs attention", "Blocking"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
},
options: { trace: { enabled: true } },
http: { body: { request_metadata: "value" }, headers: { "x-request": "yes" }, query: { trace: "1" } },
})
expect(response.model).toBe("jev-1.13.0")
expect(response.answers.department).toEqual({
type: "choice",
choice: "billing",
probabilities: { billing: 0.9, technical: 0.1 },
})
expect(response.answers.urgency).toEqual({
type: "score",
score: 1.2,
probabilities: { "0": 0, "1": 0.8, "2": 0.2 },
})
expect(response.answers.refund).toEqual({ type: "boolean", probability: 0.97 })
expect(response.usage?.totalTokens).toBe(36)
expect(response.providerMetadata).toEqual({
typesafe: {
confidence: { department: 0.8, urgency: 0.6 },
legend: { urgency: { "0": "Can wait", "1": "Needs attention", "2": "Blocking" } },
},
})
}).pipe(
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe("https://typesafe.test/v1/systemone?api=v1&trace=1")
expect(request.headers.get("authorization")).toBe("Bearer test")
expect(request.headers.get("x-default")).toBe("yes")
expect(request.headers.get("x-request")).toBe("yes")
expect(JSON.parse(input.text)).toEqual({
deployment: "test",
request_metadata: "value",
trace: { enabled: true },
model: "jev-latest",
state: { ticket: "Please refund the duplicate charge." },
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs attention", "Blocking"],
},
refund: { type: "noul", instructions: "Is the customer asking for a refund?" },
},
})
return input.respond(
JSON.stringify({
model: "jev-1.13.0",
answers: {
department: {
type: "choice",
choice: "billing",
probabilities: { billing: 0.9, technical: 0.1 },
confidence: 0.8,
},
urgency: {
type: "score",
score: 1.2,
probabilities: { "0": 0, "1": 0.8, "2": 0.2 },
legend: { "0": "Can wait", "1": "Needs attention", "2": "Blocking" },
confidence: 0.6,
},
refund: { type: "noul", noul: 0.97 },
},
usage: { input_tokens: 30, output_tokens: 6 },
}),
{ headers: { "content-type": "application/json" } },
)
}),
),
),
),
),
),
)
it.effect("configures the OpenCode Zen System One endpoint", () =>
Evaluation.evaluate({
model: OpenCodeZen.configure({ apiKey: "zen-key", baseURL: "https://zen.test/v1" }).experimental.evaluation(
"jev-1.13",
),
state: "hello",
questions: { greeting: { type: "boolean", instructions: "Is this a greeting?" } },
}).pipe(
Effect.tap((response) =>
Effect.sync(() => {
expect(response.answers.greeting.probability).toBe(0.99)
expect(response.usage?.providerMetadata).toEqual({
opencode: { input_tokens: 10, output_tokens: 2 },
})
}),
),
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(
dynamicResponse((input) => {
expect(input.request.url).toBe("https://zen.test/v1/systemone")
expect(input.request.headers.authorization).toBe("Bearer zen-key")
return Effect.succeed(
input.respond(
JSON.stringify({
model: "jev-1.13.0",
answers: { greeting: { type: "noul", noul: 0.99 } },
usage: { input_tokens: 10, output_tokens: 2 },
}),
{ headers: { "content-type": "application/json" } },
),
)
}),
),
),
),
),
)
it.effect("rejects malformed questions before network I/O", () =>
Effect.gen(function* () {
const error = yield* Evaluation.evaluate({
model: TypeSafeAI.experimental.evaluation("jev-latest"),
state: "hello",
questions: { score: { type: "score", instructions: "How much?", criteria: ["only"] } },
}).pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
}).pipe(
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(dynamicResponse(() => Effect.die("invalid evaluation reached the network"))),
),
),
),
)
})
+60
View File
@@ -0,0 +1,60 @@
import { Effect } from "effect"
import { Evaluation, EvaluationClient, EvaluationModel, type EvaluationRoute } from "../src/experimental.js"
import type { Service } from "../src/experimental/evaluation-client.js"
import { OpenCodeZen, TypeSafeAI } from "../src/providers.js"
type Requirements<T> = T extends Effect.Effect<infer _A, infer _E, infer R> ? R : never
type Success<T> = T extends Effect.Effect<infer A, infer _E, infer _R> ? A : never
type Equal<A, B> = [A, B] extends [B, A] ? true : false
type Assert<T extends true> = T
const model = TypeSafeAI.configure({ apiKey: "test" }).experimental.evaluation("jev-latest")
const request = Evaluation.request({
model,
state: { ticket: "refund" },
questions: {
topic: {
type: "choice",
instructions: "Which team?",
criteria: { billing: null, support: { includes: ["help"] } },
},
severity: { type: "score", instructions: "How severe?", criteria: ["Low", "High"] },
refund: { type: "boolean", instructions: "Refund?" },
},
})
const result = EvaluationClient.evaluate(request)
type Result = Success<typeof result>
type Choice = Assert<Equal<Result["answers"]["topic"]["choice"], "billing" | "support">>
type ClientRequirements = Assert<Equal<Requirements<typeof result>, Service>>
void (true satisfies Choice)
void (true satisfies ClientRequirements)
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
model: OpenCodeZen.experimental.evaluation("jev-1.13"),
state: ["hello"],
questions: { greeting: { type: "boolean", instructions: "Greeting?" } },
})
response.answers.greeting.probability satisfies number
// @ts-expect-error Boolean answers do not contain a selected choice.
response.answers.greeting.choice
// @ts-expect-error Unknown question IDs are not exposed.
response.answers.missing
})
declare const route: EvaluationRoute<{ readonly temperature?: number }>
const custom = EvaluationModel.make({ id: "custom", provider: "custom", route })
Evaluation.evaluate({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
options: { temperature: 0.5 },
})
// @ts-expect-error Selected evaluation models retain their request option types.
Evaluation.evaluate({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
options: { temperature: "high" },
})
+8
View File
@@ -8,9 +8,11 @@ import {
CloudflareWorkersAI,
DeepSeek,
Fireworks,
OpenCodeZen,
OpenAI,
OpenAICompatible,
OpenRouter,
TypeSafeAI,
XAI,
} from "@opencode/ai/providers"
import {
@@ -23,6 +25,7 @@ import {
} from "@opencode/ai/protocols"
import * as AnthropicMessages from "@opencode/ai/protocols/anthropic-messages"
import { TestLLM } from "@opencode/ai/testing"
import { Evaluation, EvaluationClient } from "@opencode/ai/experimental"
describe("public exports", () => {
test("root exposes app-facing runtime APIs", () => {
@@ -37,6 +40,9 @@ describe("public exports", () => {
expect(TestLLM.layer).toBeFunction()
expect(TestLLM.testLayer).toBeFunction()
expect(TestLLM.Test.of).toBeFunction()
expect(Evaluation.evaluate).toBeFunction()
expect(EvaluationClient.layer).toBeDefined()
expect(EvaluationClient.fetchLayer).toBeDefined()
})
test("route barrel exposes route-authoring APIs", () => {
@@ -66,6 +72,8 @@ describe("public exports", () => {
expect(CloudflareWorkersAI.configure).toBeFunction()
expect(CloudflareWorkersAI.configure({ accountId: "fixture", apiKey: "fixture" }).model).toBeFunction()
expect(OpenRouter.model).toBeFunction()
expect(TypeSafeAI.experimental.evaluation).toBeFunction()
expect(OpenCodeZen.experimental.evaluation).toBeFunction()
expect(XAI.model).toBeFunction()
expect(XAI.provider.responses).toBe(XAI.responses)
expect(XAI.provider.chat).toBe(XAI.chat)
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/calls-a-tool",
"recordedAt": "2026-08-23T17:21:51.036Z"
"recordedAt": "2026-09-22T03:46:29.725Z"
},
"interactions": [
{
@@ -21,7 +25,7 @@
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_425130\"},\"thoughtSignature\": \"AY89a1+1fXnLgYhHMuN3Ak6LBhT6PcrYOW7iPav4LfsacvG/Z6l1yJ+AsU7vWhFj/JyPIbsJJQ+GjohM9sCIZ6nqUOIg3reo/7osmrCvFrVHedTHQcwiPzoz2Kp3gb+uWjFAXxk1EX4IRAKcu0ox1W/Z9PpuZvHkTerGO2a82e02N6MAF1YhhtbXFvSdqLRih2Os68rdOk5/Bcld7ol8qUgeyIZ3CtI3OJ5jwRcD8LjvK33A7ZFzH5Bxp/peUmXvqnu5iNhnGBxZaJy/vupCtxRZxjaS+ojG0/UhyrnRiKIpbzQ0FBkxePPn8GCX/LOe2y3GUc98co8lN8OOuCd9ZmEdx5AjHmQkPO9fAV9SxG6Bda6SDWVL8o/Uz3WSQYoUEfAdoajEWIBvcisoeCJjb7zgmRRZ9VQSPl3RXj5LFRvX8jn0YKV1CahYbc24jA==\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 102,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 47},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_28936\"},\"thoughtSignature\": \"AY89a1/MVeBxtwu9l/96clFobOrrd7Q5MTf5o8/A22fc++1EsFYVeCFx9WCGmJ+D3yW3FWlRXKtJECYfprSoxixCSOYBNiV9g8/IkejbAxF26k1vxTsHkoqts0O2s8GqqBSRAavJfKQ/taTRbPuIq+b+RKZ8SDJFoWDnFjXpdS5S164uJTjmgs4ALd8oB1oQRycMsTbAY51TmkcNUnhARpjKQfqiizz4KfSRFjx7xDnza0v4jj73m2GV86eLDhVT7trIT+Dl5DsJt6RBLOE2Amse54mcpIbsYaANlY1YDU6FKuUBqnuwpl0OfhMXwWuYMogv1tH/0rTXsqNwmHyt/b9ZZXqYCdFXpMGt8udmoZ0d\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 93,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\n"
}
}
]
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/continues-after-a-tool-result",
"recordedAt": "2026-08-23T17:21:51.853Z"
"recordedAt": "2026-09-22T03:46:30.691Z"
},
"interactions": [
{
@@ -14,14 +18,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a197c+fpHJftPtcufnqMAyoRQKVEQK+KeG+RVHVx2wKil3L4jP4YWvfVbcuOFr2jio4Kre/hCrDANAoMFSvaZrdaPeo1b5bXQSmJKMH03yM5M6q6ME6JiBvXym143U4exIde4UbOh2tMeyXMvB3aWxcavIHd78g5G5QPLreo6A3LO5871cYYVeRwteY+/zbEdqfaAq1hlk6WYpWkNljYpjMyKwr15YC8rFLh3HYayS9tTN++GGrk/reZn6C3OEPlzPou/pXRATzcEAGVl/TW\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 98,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"thoughtsTokenCount\": 24},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a18aVI8Uxr2VaOI822u0DyuQdy4B00uhLnjqYb5Qb6Mkscccm018knLtYThB5UX8dRv1VFsORSQ0Qo6Gx9RCng2AK9EPce7p\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 74,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}]},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\n"
}
}
]
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/streams-text",
"recordedAt": "2026-08-23T17:21:50.112Z"
"recordedAt": "2026-09-22T03:46:28.840Z"
},
"interactions": [
{
@@ -21,7 +25,7 @@
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Hello\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a1+BGsRqlGpfT0psLB4jeTkT5rDV2HFOlrRuF7aVxDOjqNVUku6t4azeSnxpd+msHWuwXj4RS+7gmVlzVs+JNi8uj+iZWTBCi71vSh9kdK9ed/sHv9J7uL9ZWSOcgbhX/hxdXaUp5yVbQzHFXPjR9A/IkEkHV8VKarDZVFE1T1uASia74lkmyBeZZz+DQmRsLwbUHzFUKlF3qnk/SliLo21ZgASd7itlALQ0PBLJZwgeI3g7tDscDSE18hnB11Fky8q7MLd3HY16zbDvHBEMb18pmmPelPI01KdrCIwMSou/01/u5jiSUCc3pFksZawUj3tAHocHSC3ZKAQQQuUXGe5tm61C2E40/NANBeePc1S4HYE6Yo/vtX6tE02LDky5IQWX09H6+DZ7fpopP5nCUfcKPHa3hVjYquWYYMtZgXO4ZpxfVd3lt1VUDuJNN3BMMCZapjBoJZFPXPJ5t/yg9Rnd791+msGH77b4wztz1vtsPrT9oV9g6SDo9ZUH6BaOcbK7fw8FaXcGw+55malEwQy6zpRLGecooBu70p6RwhaAUyKIMX49y+F2hkNxQxDeBUNckJnu6n4w+KLyjP+bR0gqPJbGjVfteHm+QujqjJdBBT/m1u9kPo1nIbzdEs/PIADBdbuV7TkD/HoRFKpLnNmM2no8ioTtFEjKBDz4ippGi15r8pGgA6wIb/1HAvOGh+PVERdGcbelVTgfONwBqjQ7B1wmEizCfyYuMIskfwjxDGayfKlpDxrnNeogtEct9u5/DjEKlURlg9MtmW1B9P8BXYJ+7SCiRJWwW6bzB+5C+MLCnETl/mljDizoJMHK8DKIhI4oxBsrWXEuoHFwEwGIeOZq0BofH2Jz/l6+KIboV/zd581Kk0zPg/rlI6acfjUEtXtbF+t0+jzoJN7006x4i2tqXeJZ+4e5yisSArEsfJ0YzNWoJtBHG9V9/euDcEP3+jsr98efaQaQbLMPvT/Hb7CYQ7ChhGfcGxQ=\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 7,\"candidatesTokenCount\": 1,\"totalTokenCount\": 150,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 7}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 1}],\"thoughtsTokenCount\": 142},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Hello\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:26.748531Z\",\"responseId\": \"EvqxavPXLbqerb8P1eSViQs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a18msz0aRAZc4U/Zl7rEXjdTJsE0EWee1+hD2dT7h711q6uIRem7KWCLlji7JVSu2hDV1L8uFh8SGAO5jU4MBSZ+mq661EVXf9DZBF1CV+fetSpCUggurbYHWGGGvLRxjI96W0wR93EMoV/1pLHa4fgGVcFupoFsz/3n9J3wCkT4pRvHjn+hC7V11aSMI07t/OUeo1ssAziob9apuB+HUm2/EO17ph9TZvg3rydE5J0nmJ+DnYGLl5FfQJHgnnsjR5qaKs5wVNIKNA811lv/K5Iee/O6gMNXI3GAcGUvPKHMD5oQSs5B343blFUiqw47H1WZ9KWBoLXhVcnss6hLPmpDchInVhr3ObAHQhZIaBDBnhQR6nuc5BtgoFnwWw3w5G1yNNFt3qT3c75Rd/8NamdtveyskDbrHxQ5GkUo7ICuE/3SQG0yqWhVDjakYgVqjLG3BTKRg6I/dT5+VCeQbdxTZ+ScWkJfXYDEnlZlTjMLz2FS+B+9Zwez/1Dy/pm9H51jvuXI942kv31DfDVcTTwvKmb3SqIB9D4aomb16gDAywC7I4L/+ZwsqmcQoUvVtHGEe5NL3r6fgWtWzKht+60ZgBalmzdBfhZHd/bDXvTAX7SVpHraXjfpcdGrtv1LhU6Kr3gol94TR5Xh9dWKNhwR0s6at7MNCpj3eG9ogrWkmYSiqNeVT1ovscWvH70tDCtCNwY5sIUJVUSWdHc2OdzFxNIPkzh9OuHIn0o9Dbmup6obFoQOgi587HA/Sqvre9pDVqVMzn7w57naDmg+lsR9wS67BxbV4tVrvvfeVQAQQRft5SmSu7WCBnaWuM2/o890nE5ynFG+M2FqApJLgdfXozOguo/hROrD3WxLUyT8+FDsoCY8ky6YYTaF3OlOHpuD5Zmw4UGSdDbcEzhc6cRqBAYfMpxzN58VwNRrjG/MGTY3jrvOLJDk3sF7URa7Tn5YuRLp/2jL5YKWF2QftWiW9jl1YeORYQwNdLBuRK9L4/WOxNRWhYZToAJfwT4byh5dKzY8OnjeFcJy8RVvuRpoYeQY1P63ie/K1D3TgJEwKQXylLdPwtZRGM/P5sm2dcf5F+lRfZGufmv+Q++unrqWr6uv3IlgVGKPa8zTh+ipSaik7taKd+jertOSYkgjrrUfjC0/oyg5giuNqXh+ebIxQC7TMX8bManc5N3aomFuYXmIJ4iT/euQctq8N3gPPWmUNmrnXztdEZgpQdV2ahj+yRr1yxCriOfRo3Az3oPMPEaXxrqDc8RoWt/lOgzzp7KfBGoOVzYdvaDGr8MhsZ8Pd7HD0vZW8TNSl8R7cEK3G5EotNA+s6lF/n/RH5ewmg3cQK8bnnZ4oNjqYo3pGtw+H2rbefMMzAk4mybGe9uDqSdhWJgBzRGsMXRW+0atn9tuJTu6vPNn9asNvwXS0lcvpQskbe5sK3yhzyFzqwrJxs7Ji6yiu6kOtsQ5Girzk1835A2Zun6ZegGtQH8DbgIPdSBMkh6kypndBu4ns2gg97g9yB8XFGbv3c60sxaXPRuycl9IjBL81Yy6n6PCOBx7Pqm9xrzPn7QzgtetKxCmNUMYJvF+myiQFrMdL1w+1DD7FoZX9VNBDbNWgcfhgnL8tlHSmTb36KnScJOe8D+3n3+aDfkWpiHjrLrGJsHlaek/Ji2Cgwa0dIC6EhQitNttskIKVMa+yD6D1T6Dvz/eolnutjYwbGb3y9mtpQzmVwf2Zp2dzxYRWZk=\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 7,\"candidatesTokenCount\": 1,\"totalTokenCount\": 285,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 7}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 1}],\"thoughtsTokenCount\": 277},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:26.748531Z\",\"responseId\": \"EvqxavPXLbqerb8P1eSViQs\"}\r\n\r\n"
}
}
]
@@ -0,0 +1,33 @@
{
"version": 1,
"metadata": {
"model": "jev-1.13-free",
"tags": [
"prefix:opencode-zen-evaluation",
"provider:opencode",
"protocol:system-one"
],
"name": "opencode-zen-evaluation/evaluates-choice-score-and-boolean-questions",
"recordedAt": "2026-09-22T03:44:57.511Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://opencode.ai/zen/v1/systemone",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13-free\",\"state\":\"I was charged twice for the same invoice. Please refund the duplicate payment today.\",\"questions\":{\"department\":{\"type\":\"choice\",\"instructions\":\"Which team should handle this support request?\",\"criteria\":{\"billing\":\"Payments, invoices, refunds, or failed charges\",\"technical\":\"Bugs, outages, or integrations\",\"sales\":\"Pricing, upgrades, or new accounts\"}},\"urgency\":{\"type\":\"score\",\"instructions\":\"How urgent is this support request?\",\"criteria\":[\"Can wait for normal support\",\"Needs prompt attention\",\"Actively blocking revenue\"]},\"refund\":{\"type\":\"noul\",\"instructions\":\"Is the customer asking for a refund?\"}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13-free\",\"answers\":{\"department\":{\"type\":\"choice\",\"choice\":\"billing\",\"confidence\":1,\"probabilities\":{\"technical\":0,\"sales\":0,\"billing\":1}},\"urgency\":{\"type\":\"score\",\"score\":1.02,\"confidence\":0.95,\"legend\":{\"0\":\"Can wait for normal support\",\"1\":\"Needs prompt attention\",\"2\":\"Actively blocking revenue\"},\"probabilities\":{\"0\":0,\"1\":0.97,\"2\":0.03}},\"refund\":{\"type\":\"noul\",\"noul\":0.99}},\"usage\":{\"input_tokens\":422,\"output_tokens\":69}}"
}
}
]
}
@@ -0,0 +1,33 @@
{
"version": 1,
"metadata": {
"model": "jev-latest",
"tags": [
"prefix:typesafe-evaluation",
"provider:typesafe-ai",
"protocol:system-one"
],
"name": "typesafe-evaluation/evaluates-choice-score-and-boolean-questions",
"recordedAt": "2026-09-22T03:44:28.626Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.typesafe.ai/v1/systemone",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-latest\",\"state\":\"I was charged twice for the same invoice. Please refund the duplicate payment today.\",\"questions\":{\"department\":{\"type\":\"choice\",\"instructions\":\"Which team should handle this support request?\",\"criteria\":{\"billing\":\"Payments, invoices, refunds, or failed charges\",\"technical\":\"Bugs, outages, or integrations\",\"sales\":\"Pricing, upgrades, or new accounts\"}},\"urgency\":{\"type\":\"score\",\"instructions\":\"How urgent is this support request?\",\"criteria\":[\"Can wait for normal support\",\"Needs prompt attention\",\"Actively blocking revenue\"]},\"refund\":{\"type\":\"noul\",\"instructions\":\"Is the customer asking for a refund?\"}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13.0\",\"answers\":{\"department\":{\"type\":\"choice\",\"choice\":\"billing\",\"confidence\":1.0,\"probabilities\":{\"billing\":1.0,\"sales\":0.0,\"technical\":0.0}},\"urgency\":{\"type\":\"score\",\"score\":1.01,\"confidence\":0.97,\"legend\":{\"0\":\"Can wait for normal support\",\"1\":\"Needs prompt attention\",\"2\":\"Actively blocking revenue\"},\"probabilities\":{\"0\":0.0,\"1\":0.98,\"2\":0.02}},\"refund\":{\"type\":\"noul\",\"noul\":0.99}},\"usage\":{\"input_tokens\":422,\"output_tokens\":69}}"
}
}
]
}
@@ -0,0 +1,79 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Evaluation } from "../../src/experimental.js"
import { OpenCodeZen, TypeSafeAI } from "../../src/providers.js"
import { recordedTests } from "../recorded-test.js"
const questions = {
department: {
type: "choice",
instructions: "Which team should handle this support request?",
criteria: {
billing: "Payments, invoices, refunds, or failed charges",
technical: "Bugs, outages, or integrations",
sales: "Pricing, upgrades, or new accounts",
},
},
urgency: {
type: "score",
instructions: "How urgent is this support request?",
criteria: ["Can wait for normal support", "Needs prompt attention", "Actively blocking revenue"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
} as const
const state = "I was charged twice for the same invoice. Please refund the duplicate payment today."
const typesafe = recordedTests({
prefix: "typesafe-evaluation",
provider: "typesafe-ai",
protocol: "system-one",
requires: ["TYPESAFE_API_KEY"],
metadata: { model: "jev-latest" },
})
const zen = recordedTests({
prefix: "opencode-zen-evaluation",
provider: "opencode",
protocol: "system-one",
requires: ["OPENCODE_API_KEY"],
metadata: { model: "jev-1.13-free" },
})
describe("experimental Evaluation recorded", () => {
typesafe.effect("evaluates choice score and boolean questions", () =>
assertEvaluation(
TypeSafeAI.configure({ apiKey: process.env.TYPESAFE_API_KEY ?? "fixture" }).experimental.evaluation("jev-latest"),
"typesafe",
),
)
zen.effect("evaluates choice score and boolean questions", () =>
assertEvaluation(
OpenCodeZen.configure({ apiKey: process.env.OPENCODE_API_KEY ?? "fixture" }).experimental.evaluation(
"jev-1.13-free",
),
"opencode",
),
)
})
const assertEvaluation = (
model: ReturnType<typeof TypeSafeAI.experimental.evaluation>,
metadataKey: "typesafe" | "opencode",
) =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({ model, state, questions })
expect(response.model).toStartWith("jev-")
expect(response.answers.department.type).toBe("choice")
expect(response.answers.department.choice).toBe("billing")
expect(response.answers.department.probabilities?.billing).toBeGreaterThan(0.5)
expect(response.answers.urgency.type).toBe("score")
expect(response.answers.urgency.score).toBeGreaterThanOrEqual(0)
expect(response.answers.urgency.score).toBeLessThanOrEqual(2)
expect(response.answers.refund.type).toBe("boolean")
expect(response.answers.refund.probability).toBeGreaterThan(0.5)
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
expect(response.providerMetadata?.[metadataKey]?.confidence).toBeDefined()
})
@@ -114,7 +114,7 @@ describe("Google Vertex providers", () => {
}),
)
it.effect("strips function call ids Vertex does not accept from lowered bodies", () =>
it.effect("preserves function call ids in lowered Vertex bodies", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
@@ -142,15 +142,14 @@ describe("Google Vertex providers", () => {
}),
)
expect(JSON.stringify(prepared.body.contents)).not.toContain('"id"')
expect(prepared.body.contents).toMatchObject([
{ role: "model", parts: [{ functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }] },
{ role: "model", parts: [{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } }] },
{
role: "user",
parts: [
{
functionResponse: {
id: undefined,
id: "call_1",
name: "lookup",
response: { name: "lookup", content: "sunny" },
},
@@ -235,12 +234,23 @@ describe("Google Vertex providers", () => {
parts: [
{ text: "Thinking.", thought: true, thoughtSignature: "reasoning_sig" },
{ text: "Checking.", thoughtSignature: "text_sig" },
{ functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" },
{
functionCall: { id: "provider_call_1", name: "lookup", args: { query: "weather" } },
thoughtSignature: "tool_sig",
},
],
},
{
role: "user",
parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: "sunny" } } }],
parts: [
{
functionResponse: {
id: "provider_call_1",
name: "lookup",
response: { name: "lookup", content: "sunny" },
},
},
],
},
])
}),
+9 -1
View File
@@ -6,6 +6,8 @@ import * as path from "node:path"
import { fileURLToPath } from "node:url"
import { LLMClient, RequestExecutor } from "../src/route.js"
import { ImageClient } from "../src/image-client.js"
import { EvaluationClient } from "../src/experimental/evaluation-client.js"
import type { Service as EvaluationClientService } from "../src/experimental/evaluation-client.js"
import type { Service as ImageClientService } from "../src/image-client.js"
import type { Service as LLMClientService } from "../src/route/client.js"
import type { Service as RequestExecutorService } from "../src/route/executor.js"
@@ -18,7 +20,12 @@ import {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
type RecordedEnv = RequestExecutorService | LLMClientService | ImageClientService | Socket.WebSocketConstructor
type RecordedEnv =
| RequestExecutorService
| LLMClientService
| ImageClientService
| EvaluationClientService
| Socket.WebSocketConstructor
type RecordedTestsOptions = RecordedGroupOptions & {
readonly options?: HttpRecorder.RecorderOptions
@@ -92,6 +99,7 @@ export const recordedTests = (options: RecordedTestsOptions) =>
requestExecutor,
LLMClient.layer.pipe(Layer.provide(requestExecutor)),
ImageClient.layer.pipe(Layer.provide(requestExecutor)),
EvaluationClient.layer.pipe(Layer.provide(requestExecutor)),
webSocket,
)
},
@@ -0,0 +1,145 @@
import { base64Encode } from "@opencode/util/encode"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/BtwSidebar"
const projectID = "proj_btw_sidebar"
const sessionID = "ses_btw_sidebar"
const otherSessionID = "ses_btw_sidebar_other"
const title = "Side question session"
const otherTitle = "Other side question session"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const sessionHref = (id: string) => `/server/${base64Encode(server)}/session/${id}`
test.use({ viewport: { width: 1440, height: 900 } })
test("answers /btw in the side panel without admitting a prompt", async ({ page }) => {
const generations: { sessionID: string; prompt: string }[] = []
const prompts: unknown[] = []
const generated = Promise.withResolvers<void>()
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "btw-sidebar",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: sessionID,
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
{
id: otherSessionID,
slug: otherSessionID,
projectID,
directory,
title: otherTitle,
version: "dev",
time: { created: 1700000001000, updated: 1700000001000 },
},
],
pageMessages: () => ({ items: [] }),
vcsDiff: [],
onPrompt: (input) => prompts.push(input),
generate: async (input) => {
generations.push(input)
if (input.sessionID === otherSessionID) return { text: "This answer belongs to the **other session**." }
await generated.promise
return {
text: "The retry loop uses **exponential backoff** and stops after three attempts.\n\n```ts\nconst delay = 2 ** attempt\n```",
}
},
})
await page.addInitScript(
({ directory, server, sessionID, otherSessionID }) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([
{ type: "session", server, sessionId: sessionID },
{ type: "session", server, sessionId: otherSessionID },
]),
)
},
{ directory, server, sessionID, otherSessionID },
)
await page.goto(sessionHref(sessionID))
await expectSessionTitle(page, title)
const editor = page.locator('[data-component="composer-editor"]')
await expect(editor).toBeEditable()
await editor.fill("/btw")
const suggestion = page.locator('[data-suggestion-id="session.btw"]')
await expect(suggestion).toBeVisible()
await suggestion.click()
await expect(editor).toHaveText("/btw ")
await editor.press("Enter")
const panel = page.locator('[data-slot="session-btw-panel"]')
await expect(panel).toBeHidden()
await expect(page.getByText("Add a question after /btw", { exact: true })).toBeVisible()
expect(generations).toEqual([])
expect(prompts).toEqual([])
await editor.fill("/btw how does the retry loop work?")
await editor.press("Enter")
const tab = page.getByRole("tab", { name: "/btw" })
await expect(panel).toBeVisible()
await expect(panel.getByRole("textbox")).toHaveCount(0)
await expect(panel.getByRole("status")).toContainText("Working")
await expect(tab).toHaveAttribute("data-selected", "")
generated.resolve()
await expect(panel.getByText("how does the retry loop work?", { exact: true })).toBeVisible()
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
await expect(panel.getByText("const delay = 2 ** attempt", { exact: true })).toBeVisible()
expect(generations).toHaveLength(1)
expect(generations[0]?.sessionID).toBe(sessionID)
expect(generations[0]?.prompt).toContain("how does the retry loop work?")
expect(prompts).toEqual([])
await expect(editor).toHaveText("")
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(otherSessionID)}"]`).click()
await expectSessionTitle(page, otherTitle)
await editor.fill("/btw what belongs here?")
await editor.press("Enter")
await expect(panel.getByText("other session", { exact: false })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(sessionID)}"]`).click()
await expectSessionTitle(page, title)
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
await expect(panel.getByText("other session", { exact: false })).toHaveCount(0)
await page.reload()
await expectSessionTitle(page, title)
await expect(page.getByRole("tab", { name: "/btw" })).toHaveCount(0)
await expect(page.locator('[data-slot="session-btw-panel"]')).toHaveCount(0)
})
@@ -2,6 +2,8 @@ import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test.use({ permissions: ["clipboard-read", "clipboard-write"] })
test.beforeEach(async ({ page }) => {
const sessions = fixture.sessions.map((session) => ({ ...session }))
await mockOpenCodeServer(page, {
@@ -95,7 +97,7 @@ test("renames and closes the session tab from its context menu", async ({ page }
await expect(tab).toBeFocused()
await tab.press("Shift+F10")
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
await expect(input).toBeFocused()
await input.fill("Renamed from tab")
await input.press("Enter")
@@ -112,6 +114,28 @@ test("renames and closes the session tab from its context menu", async ({ page }
).toBeVisible()
})
test("pastes rich text into the session tab title as plain text", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
await expect(input).toBeFocused()
await page.evaluate(async () => {
await navigator.clipboard.write([
new ClipboardItem({
"text/html": new Blob(['<span style="font-size: 48px">Rich title</span>'], { type: "text/html" }),
"text/plain": new Blob(["Rich title"], { type: "text/plain" }),
}),
])
})
await input.press("ControlOrMeta+A")
await input.press("ControlOrMeta+V")
await expect(input).toHaveText("Rich title")
await expect(input.locator("*")).toHaveCount(0)
await input.press("Enter")
await expect(page.getByRole("heading", { name: "Rich title", exact: true })).toBeVisible()
})
test("renames an inactive tab without switching sessions", async ({ page }) => {
await page.getByRole("button", { name: "Home", exact: true }).click()
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
@@ -119,7 +143,7 @@ test("renames an inactive tab without switching sessions", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
await expect(input).toBeFocused()
await input.fill("Inactive tab renamed")
await input.press("Tab")
+7
View File
@@ -197,6 +197,13 @@ const Group = HttpApiGroup.make("mock")
success: Json,
}),
)
.add(
HttpApiEndpoint.post("sessionGenerate", "/api/session/:sessionID/generate", {
params: SessionParams,
payload: Schema.Struct({ prompt: Schema.String }),
success: Json,
}),
)
.add(
HttpApiEndpoint.post("sessionSwitchAgent", "/api/session/:sessionID/agent", {
params: SessionParams,
+7
View File
@@ -42,6 +42,7 @@ export interface MockServerConfig {
sessionStatus?: Record<string, unknown> | (() => Record<string, unknown>)
inbox?: unknown[] | (() => unknown[])
onPrompt?: (input: { sessionID: string; body: Record<string, unknown> }) => void
generate?: (input: { sessionID: string; prompt: string }) => { text: string } | Promise<{ text: string }>
onInboxChange?: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => void
}
@@ -456,6 +457,12 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
},
}
}),
sessionGenerate: (ctx) =>
Effect.promise(async () => ({
data: (await config.generate?.({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt })) ?? {
text: "Side-question answer",
},
})),
sessionInboxCancel: (ctx) =>
Effect.sync(() =>
config.onInboxChange?.({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID, action: "cancel" }),
@@ -0,0 +1,32 @@
import { describe, expect, test } from "bun:test"
import { parseClientSlashCommand } from "./client-slash-command"
const options = [
{ id: "session.btw", trigger: "btw", arguments: true, type: "builtin" as const },
{ id: "custom.btw", trigger: "custom", type: "custom" as const },
{ id: "model.choose", trigger: "model", type: "builtin" as const },
]
describe("parseClientSlashCommand", () => {
test("parses inline and multiline arguments", () => {
expect(parseClientSlashCommand(options, "/btw why this approach?")).toEqual({
id: "session.btw",
input: "why this approach?",
})
expect(parseClientSlashCommand(options, "/btw\nwhy this approach?")).toEqual({
id: "session.btw",
input: "why this approach?",
})
})
test("accepts a bare argument command", () => {
expect(parseClientSlashCommand(options, "/btw")).toEqual({ id: "session.btw", input: "" })
})
test("rejects prefixes, custom commands, and ordinary slash commands", () => {
expect(parseClientSlashCommand(options, "/btwx nope")).toBeUndefined()
expect(parseClientSlashCommand(options, "/custom nope")).toBeUndefined()
expect(parseClientSlashCommand(options, "/model opus")).toBeUndefined()
expect(parseClientSlashCommand(options, "ask /btw later")).toBeUndefined()
})
})
@@ -0,0 +1,24 @@
type ClientSlashCommand = {
id: string
trigger: string
arguments?: boolean
type: "builtin" | "custom"
}
export function parseSlashCommand(text: string) {
if (!text.startsWith("/")) return
const separator = text.search(/\s/)
const name = text.slice(1, separator === -1 ? undefined : separator)
return { name, input: separator === -1 ? "" : text.slice(separator).trim() }
}
export function parseClientSlashCommand(options: readonly ClientSlashCommand[], text: string) {
const command = parseSlashCommand(text)
if (!command) return
const option = options.find((item) => item.type === "builtin" && item.arguments && item.trigger === command.name)
if (!option) return
return {
id: option.id,
input: command.input,
}
}
+9 -3
View File
@@ -24,6 +24,7 @@ import { createComposerHistory } from "./history/store"
import { composerPlaceholder } from "./placeholder"
import { createComposerSubmit } from "./submit"
import { useAttachmentDestination } from "./attachments/destination"
import { parseClientSlashCommand } from "./client-slash-command"
export type ComposerModel = ComposerEditorModel & {
readonly model: ComposerControls["model"]
@@ -73,9 +74,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
return [...result, path]
}, [])
})
const attachments = createMemo(() =>
prompt.current().filter(isAttachment),
)
const attachments = createMemo(() => prompt.current().filter(isAttachment))
const commentCount = createMemo(() => {
if (mode() === "shell") return 0
return prompt.context.items().filter((item) => !!item.comment?.trim()).length
@@ -242,6 +241,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
trigger: item.slash!,
title: item.title,
description: item.description,
arguments: item.slashArguments,
type: "builtin" as const,
})),
])
@@ -299,6 +299,11 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
clear: comments.clear,
restore: restoreHistoryComments,
},
clientCommand: (text) => {
const selected = parseClientSlashCommand(slashCommands(), text)
if (!selected) return
return () => command.trigger(selected.id, "slash", selected.input)
},
})
const controller = createComposerEditor({
store: prompt.store,
@@ -340,6 +345,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
if (item.kind !== "command") return
const selected = slashCommands().find((entry) => entry.id === item.id)
if (!selected || selected.type === "custom") return
if (selected.arguments) return
return () => command.trigger(selected.id, "slash")
},
attachments: {
+60 -7
View File
@@ -54,14 +54,17 @@ function submitInput(
mode: "normal" | "shell" = "normal",
commands: () => readonly { name: string }[] | undefined = () => [],
history: string[] = [],
clientCommand?: (text: string) => (() => void | Promise<void>) | undefined,
) {
return createComposerSubmit({
adapter,
mode: () => mode,
commands,
clientCommand,
editor: () => undefined,
queueScroll() {},
addToHistory: (prompt) => history.push(`add:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
addToHistory: (prompt) =>
history.push(`add:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
removeFromHistory: (prompt) =>
history.push(`remove:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
resetHistory() {},
@@ -118,6 +121,61 @@ function session(input: {
}
describe("Composer submission", () => {
test("runs a client argument command without admitting it to the session", async () => {
const state = createMemoryComposerState().capture()
state.set([
{ type: "text", content: "/btw why this approach?", start: 0, end: 23 },
{
type: "image",
id: "attachment",
filename: "diagram.png",
mime: "image/png",
blob: { id: "attachment", url: "data:image/png;base64,YQ==" },
},
])
state.context.add({ type: "file", path: "src/retry.ts" })
const calls: string[] = []
const target = session({
calls,
prompt: async () => {
throw new Error("client command must not call prompt")
},
})
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
const history: string[] = []
await submitInput(adapter, undefined, "normal", undefined, history, (text) => {
expect(text).toBe("/btw why this approach?")
return () => {
calls.push("btw")
}
}).submit(new Event("submit"))
expect(calls).toEqual(["btw"])
expect(history).toEqual([])
expect(state.current()).toEqual([
{ type: "text", content: "", start: 0, end: 0 },
{
type: "image",
id: "attachment",
filename: "diagram.png",
mime: "image/png",
blob: { id: "attachment", url: "data:image/png;base64,YQ==" },
},
])
expect(state.context.items()).toHaveLength(1)
})
test("applies the captured agent and model before a custom command without passing over its overrides", async () => {
const state = createMemoryComposerState({ prompt: "/review changes" }).capture()
const calls: string[] = []
@@ -625,12 +683,7 @@ describe("Composer submission", () => {
},
}
await submitInput(
adapter,
undefined,
"normal",
() => catalog,
).submit(new Event("submit"))
await submitInput(adapter, undefined, "normal", () => catalog).submit(new Event("submit"))
expect(await sent.promise).toBe("command")
expect(requests).toEqual([
+39 -10
View File
@@ -11,6 +11,7 @@ import { setCursorPosition } from "./editor/dom"
import { blobDataUrl, resolveBlobUrl } from "@/runtime/persistence/drafts"
import { isAttachment } from "./prompt-parts"
import type { ModelSelection } from "@/providers/models/selection"
import { parseSlashCommand } from "./client-slash-command"
const submitting = new WeakSet<object>()
@@ -37,6 +38,7 @@ type ComposerSubmitInput = {
setMode: (mode: "normal" | "shell") => void
closePopover: () => void
delivery?: (alternate: boolean) => ComposerDelivery
clientCommand?: (text: string) => (() => void | Promise<void>) | undefined
notify: {
missingSelection: () => void
failed: (kind: "shell" | "command" | "prompt", error: unknown) => void
@@ -52,15 +54,31 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
const submit = async (event: globalThis.Event, options?: { alternate?: boolean }) => {
event.preventDefault()
const prompt = clonePrompt(input.adapter.state.current())
const text = submissionText(prompt)
const clientCommand = input.mode() === "normal" ? input.clientCommand?.(text) : undefined
if (clientCommand) {
if (submitting.has(input.adapter.state)) return
submitting.add(input.adapter.state)
try {
clearClientCommand(input, prompt)
await clientCommand()
} catch (error) {
input.notify.failed("command", error)
} finally {
submitting.delete(input.adapter.state)
}
return
}
const submission = createComposerSubmission({
target: input.adapter.state,
prompt: clonePrompt(input.adapter.state.current()),
prompt,
context: input.adapter.state.context.items().map((item) => ({
...item,
selection: item.selection ? { ...item.selection } : undefined,
})),
})
const read = readSubmission(input, submission.prompt, submission.context, options?.alternate ?? false)
const read = readSubmission(input, submission.prompt, submission.context, text, options?.alternate ?? false)
if (!read) {
if (input.adapter.working() && input.adapter.kind === "active-session") void input.adapter.interrupt()
return
@@ -150,6 +168,17 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
}
}
function clearClientCommand(input: ComposerSubmitInput, prompt: Prompt) {
input.adapter.state.set([{ type: "text", content: "", start: 0, end: 0 }, ...prompt.filter(isAttachment)], 0)
input.adapter.state.mode.set("normal")
input.setMode("normal")
input.closePopover()
}
function submissionText(prompt: Prompt) {
return prompt.map((part) => ("content" in part ? part.content : "")).join("")
}
function handoffMessage(value: ComposerSubmission): SessionMessageUser {
return {
id: value.id,
@@ -193,9 +222,9 @@ function readSubmission(
input: ComposerSubmitInput,
prompt: Prompt,
context: ComposerSubmission["context"],
text: string,
alternate: boolean,
): ComposerSubmission | undefined {
const text = prompt.map((part) => ("content" in part ? part.content : "")).join("")
const mode = input.mode()
if (mode === "shell" && !text.trim()) return
const images = prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
@@ -298,14 +327,11 @@ async function sendShell(session: ComposerSession, value: ComposerSubmission) {
}
function findCommand(commands: ReturnType<ComposerSubmitInput["commands"]>, text: string) {
if (!text.startsWith("/")) return
const [name, ...arguments_] = text.split(" ")
const command = name.slice(1)
if (!commands?.some((item) => item.name === command)) return
return { command, arguments: arguments_.join(" ") }
const parsed = parseSlashCommand(text)
if (!parsed || !commands?.some((item) => item.name === parsed.name)) return
return { command: parsed.name, arguments: parsed.input }
}
async function sendCommand(
session: ComposerSession,
value: ComposerSubmission,
@@ -390,7 +416,10 @@ async function sendPrompt(
async function buildSubmissionRequest(session: ComposerSession, value: ComposerSubmission) {
const images = await Promise.all(
value.images.map(async (attachment) => ({ ...attachment, dataUrl: await blobDataUrl(attachment.blob, attachment.mime) })),
value.images.map(async (attachment) => ({
...attachment,
dataUrl: await blobDataUrl(attachment.blob, attachment.mime),
})),
)
return buildPromptRequest({
prompt: value.prompt,
@@ -47,7 +47,7 @@ export function HomeCommandPalette(props: {
state.cleanup = undefined
dialog.close()
if (item.type === "command") {
item.option?.onSelect?.("palette")
void item.option?.onSelect?.("palette")
return
}
if (item.type === "session") props.onSelectSession(item)
+7
View File
@@ -151,6 +151,8 @@ export const dict = {
"command.session.compact.description": "Summarize the session to reduce context size",
"command.session.fork": "Fork from message",
"command.session.fork.description": "Create a new session from a previous message",
"command.session.btw": "Ask a side question",
"command.session.btw.description": "Get a one-shot answer without adding to the conversation",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"command.session.import": "Import session",
@@ -756,6 +758,7 @@ export const dict = {
"session.tab.browser": "Browser",
"session.tab.add": "Add tab",
"session.tab.context": "Context",
"session.tab.btw": "/btw",
"session.tab.unknown": "Unknown Session",
"session.panel.reviewAndFiles": "Review and files",
"session.error.notFound": "This session cannot be found",
@@ -933,6 +936,10 @@ export const dict = {
"common.dismiss": "Dismiss",
"common.moreCountSuffix": " (+{{count}} more)",
"common.requestFailed": "Request failed",
"session.btw.questionRequired": "Add a question after /btw",
"session.btw.error": "Couldnt answer that question",
"session.btw.retry": "Retry",
"session.btw.copy": "Copy answer",
"common.moreOptions": "More options",
"common.learnMore": "Learn more",
"common.rename": "Rename",
+114
View File
@@ -0,0 +1,114 @@
import { createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/runtime/i18n/language"
import { useServerSDK } from "@/runtime/server/client"
import { useCommand } from "@/shell/commands/command"
import { showToast } from "@/shell/notifications/toast"
import { SESSION_BTW_TAB } from "@/session/helpers"
import type { SessionModel } from "../model"
const instructions = [
"The user is asking a quick side question about the conversation so far.",
"Answer directly and concisely in markdown from what you already know.",
"Do not call any tools and do not take any actions.",
].join(" ")
const empty = {
question: "",
answer: "",
error: false,
pending: false,
}
export function createSessionBtw(session: SessionModel) {
const command = useCommand()
const language = useLanguage()
const server = useServerSDK()
const [states, setStates] = createStore<Record<string, typeof empty>>({})
const requests = new Map<string, number>()
const controllers = new Map<string, AbortController>()
const state = () => states[session.identity.sessionKey()] ?? empty
createEffect(() => {
const key = session.identity.sessionKey()
onCleanup(() => {
const controller = controllers.get(key)
if (!controller) return
controller.abort()
controllers.delete(key)
if (states[key]?.pending) setStates(key, { pending: false, error: true })
})
})
const open = () => {
session.layout.view().reviewPanel.open()
const tabs = session.layout.tabs()
if (tabs.active() !== SESSION_BTW_TAB) tabs.open(SESSION_BTW_TAB)
}
const ask = (value?: string) => {
const question = value?.trim()
if (!question) {
showToast({ title: language.t("session.btw.questionRequired") })
return
}
open()
const sessionID = session.identity.sessionID()
if (!sessionID) return
const key = session.identity.sessionKey()
const request = (requests.get(key) ?? 0) + 1
requests.set(key, request)
controllers.get(key)?.abort()
const controller = new AbortController()
controllers.set(key, controller)
const owner = session.ownership.capture()
setStates(key, { question, answer: "", error: false, pending: true })
return server.api.session
.generate(
{
sessionID,
prompt: [instructions, question].join("\n\n"),
},
{ signal: controller.signal },
)
.then((result) => {
owner.run(() => {
if (requests.get(key) !== request) return
setStates(key, { answer: result.text.trim(), pending: false })
})
})
.catch(() => {
owner.run(() => {
if (controller.signal.aborted || requests.get(key) !== request) return
setStates(key, { error: true, pending: false })
})
})
.finally(() => {
if (controllers.get(key) === controller) controllers.delete(key)
})
}
command.register("session.btw", () => [
{
id: "session.btw",
title: language.t("command.session.btw"),
description: language.t("command.session.btw.description"),
category: language.t("command.category.session"),
slash: "btw",
slashArguments: true,
hidden: true,
disabled: !session.isDesktop(),
onSelect: (_source, input) => ask(input),
},
])
return {
answer: () => state().answer,
error: () => state().error,
pending: () => state().pending,
question: () => state().question,
retry: () => ask(state().question),
}
}
export type SessionBtwModel = ReturnType<typeof createSessionBtw>
+80
View File
@@ -0,0 +1,80 @@
import { createEffect, createSignal, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode/ui/button"
import { Icon } from "@opencode/ui/icon"
import { IconButton } from "@opencode/ui/icon-button"
import { ScrollView } from "@opencode/ui/scroll-view"
import { TextShimmer } from "@opencode/ui/text-shimmer"
import { Tooltip } from "@opencode/ui/tooltip"
import { Markdown } from "@opencode/session-ui/markdown"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { showToast } from "@/shell/notifications/toast"
import type { SessionBtwModel } from "./model"
export function SessionBtwPanel(props: { btw: SessionBtwModel }) {
const language = useLanguage()
const platform = usePlatform()
const [copied, setCopied] = createSignal(false)
createEffect(() => {
props.btw.answer()
setCopied(false)
})
const copy = () => {
const answer = props.btw.answer()
if (!answer) return
void (platform.writeClipboardText?.(answer) ?? navigator.clipboard.writeText(answer)).then(
() => setCopied(true),
() => showToast({ title: language.t("common.requestFailed") }),
)
}
return (
<div class="flex h-full min-h-0 flex-col bg-v2-background-bg-base" data-slot="session-btw-panel">
<div class="flex shrink-0 items-start justify-between gap-3 border-b border-v2-border-border-base px-5 py-4">
<div class="min-w-0 text-13-regular text-text-weak">{props.btw.question()}</div>
<Show when={props.btw.answer()}>
<Tooltip value={copied() ? language.t("common.copied") : language.t("session.btw.copy")}>
<IconButton
size="small"
variant="ghost-muted"
icon={<Icon name={copied() ? "check" : "outline-copy"} />}
aria-label={copied() ? language.t("common.copied") : language.t("session.btw.copy")}
onClick={copy}
/>
</Tooltip>
</Show>
</div>
<div class="relative min-h-0 flex-1">
<Switch>
<Match when={props.btw.pending()}>
<div
data-component="session-working"
role="status"
class="flex h-9 items-center px-5 pt-3 text-[13px] font-[530] leading-text-compact"
>
<TextShimmer text={language.t("session.timeline.working")} active />
</div>
</Match>
<Match when={props.btw.error()}>
<div class="flex h-full flex-col items-center justify-center gap-3 px-8 pb-24 text-center">
<div class="text-13-regular text-text-weak">{language.t("session.btw.error")}</div>
<Button size="small" variant="outline" onClick={props.btw.retry}>
{language.t("session.btw.retry")}
</Button>
</div>
</Match>
<Match when={props.btw.answer()}>
<ScrollView class="absolute inset-0">
<div class="px-5 py-4 pb-8">
<Markdown text={props.btw.answer()} class="text-14-regular" />
</div>
</ScrollView>
</Match>
</Switch>
</div>
</div>
)
}
@@ -37,6 +37,7 @@ import { useSettings } from "@/settings/model"
import { createFileTabListSync } from "@/session/files/file-tab-scroll"
import {
SESSION_OPEN_FILE_TAB,
SESSION_BTW_TAB,
isSessionBrowserTab,
sessionBrowserTab,
createOpenSessionFileTab,
@@ -74,6 +75,7 @@ export function SessionSidePanel(props: {
size: Sizing
stacked?: boolean
browser: ReturnType<typeof createSessionBrowser>
btwPanel: () => JSX.Element
}) {
const layout = useLayout()
const settings = useSettings()
@@ -227,7 +229,7 @@ export function SessionSidePanel(props: {
})
const fileBrowserVisible = createMemo(() => {
const active = activeTab()
return active !== "review" && active !== "context" && active !== "empty" && !isSessionBrowserTab(active)
return active === SESSION_OPEN_FILE_TAB || active === activeFileTab()
})
const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
const openBrowserKeybind = createMemo(() => command.keybindParts("browser.open"))
@@ -385,6 +387,14 @@ export function SessionSidePanel(props: {
/>
}
>
<Match when={tab === SESSION_BTW_TAB}>
<SortableTab tab={tab} index={tabs().all().indexOf(tab)} onTabClose={tabs().close}>
<div class="flex items-center gap-1.5">
<Icon name="bubble-5" size="small" />
<span>{language.t("session.tab.btw")}</span>
</div>
</SortableTab>
</Match>
<Match when={isSessionBrowserTab(tab)}>
<Show when={props.browser.tabs().find((item) => sessionBrowserTab(item.id) === tab)}>
{(item) => (
@@ -583,6 +593,12 @@ export function SessionSidePanel(props: {
</Tabs.Content>
</Show>
<Show when={activeTab() === SESSION_BTW_TAB}>
<Tabs.Content value={SESSION_BTW_TAB} class="flex h-full min-h-0 flex-col overflow-hidden">
{props.btwPanel()}
</Tabs.Content>
</Show>
<Show when={props.browser.opened()}>
<div
id={browserTabPanelID}
+19
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { createMemo, createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import {
SESSION_BTW_TAB,
SESSION_BROWSER_TAB,
sessionBrowserTab,
SESSION_OPEN_FILE_TAB,
@@ -236,6 +237,24 @@ describe("createSessionTabs", () => {
})
})
test("exposes the BTW tab without treating it as a file tab", () => {
createRoot((dispose) => {
const tabs = createMemo(() => ({ active: () => SESSION_BTW_TAB, all: () => [SESSION_BTW_TAB] }))
const result = createSessionTabs({
tabs,
pathFromTab: () => undefined,
normalizeTab: (tab) => tab,
})
expect(result.panelTabs()).toEqual([SESSION_BTW_TAB])
expect(result.openedTabs()).toEqual([])
expect(result.activeTab()).toBe(SESSION_BTW_TAB)
expect(result.activeFileTab()).toBeUndefined()
expect(result.closableTab()).toBe(SESSION_BTW_TAB)
dispose()
})
})
test("exposes one browser tab without treating it as a file tab", () => {
createRoot((dispose) => {
const tabs = createMemo(() => ({ active: () => SESSION_BROWSER_TAB, all: () => [SESSION_BROWSER_TAB] }))
+8 -2
View File
@@ -2,10 +2,11 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { same } from "@/runtime/persistence/equality"
import { isSessionBrowserTab, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
import { isSessionBrowserTab, SESSION_BTW_TAB, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
export {
SESSION_BROWSER_TAB,
SESSION_BTW_TAB,
SESSION_OPEN_FILE_TAB,
sessionBrowserTab,
isSessionBrowserTab,
@@ -63,13 +64,17 @@ export const createSessionTabs = (input: TabsInput) => {
{ equals: same },
)
const openedTabs = createMemo(
() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB && !isSessionBrowserTab(tab)),
() =>
panelTabs().filter(
(tab) => tab !== SESSION_OPEN_FILE_TAB && tab !== SESSION_BTW_TAB && !isSessionBrowserTab(tab),
),
emptyTabs,
{ equals: same },
)
const activeTab = createMemo(() => {
const active = input.tabs().active()
if (active === "context") return active
if (active === SESSION_BTW_TAB) return active
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
if (active && isSessionBrowserTab(active) && browser()) return active
if (active === "review" && review()) return active
@@ -89,6 +94,7 @@ export const createSessionTabs = (input: TabsInput) => {
const closableTab = createMemo<string | undefined>(() => {
const active = activeTab()
if (active === "context") return active
if (active === SESSION_BTW_TAB) return active
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
if (active && isSessionBrowserTab(active) && browser()) return active
if (!openedTabs().includes(active)) return
+4
View File
@@ -14,6 +14,8 @@ import { ReviewPanel } from "./panel"
import { SessionReviewTab } from "./review-tab"
import type { ChangeMode, SessionReviewModel } from "./model"
import type { createSessionBrowser } from "../browser/model"
import type { SessionBtwModel } from "../btw/model"
import { SessionBtwPanel } from "../btw/panel"
const MobilePanelDrawer = lazy(async () => {
const { MobilePanelDrawer } = await import("@/shell/mobile-panel-drawer")
@@ -127,6 +129,7 @@ export function SessionMobileReview(props: { review: SessionReviewModel }) {
export function SessionDesktopReview(props: {
review: SessionReviewModel
browser: ReturnType<typeof createSessionBrowser>
btw: SessionBtwModel
present?: boolean
}) {
return (
@@ -153,6 +156,7 @@ export function SessionDesktopReview(props: {
size={props.review.screen.size}
stacked={props.review.screen.side.layout().stacked}
browser={props.browser}
btwPanel={() => <SessionBtwPanel btw={props.btw} />}
/>
</Suspense>
)
+8 -1
View File
@@ -39,6 +39,7 @@ import { createAnimatedPresence } from "@/runtime/animated-presence"
import { createSessionBrowser } from "./browser/model"
import { createTimelineCache } from "./timeline/cache"
import { ArtifactMarkdownProvider, ArtifactOpenerProvider } from "./files/open-artifact"
import { createSessionBtw } from "./btw/model"
const SessionMobileFiles = lazy(async () => {
const { SessionMobileFiles } = await import("./files/session-mobile-files")
@@ -71,6 +72,7 @@ function SessionScreenContent(props: { session: SessionModel; browser: ReturnTyp
return info ? projectForSession(info, server.ctx.sync.data.project) : undefined
})
const isDesktop = session.isDesktop
const btw = createSessionBtw(session)
const screen = createSessionScreenLayout(session)
const timeline = createSessionTimelineInteraction(session)
const timelineSearch = createTimelineSearchController({
@@ -451,7 +453,12 @@ function SessionScreenContent(props: { session: SessionModel; browser: ReturnTyp
setStore("sideReviewPresent", false)
}}
>
<SessionDesktopReview review={review} browser={browser} present={store.sideReviewPresent} />
<SessionDesktopReview
review={review}
browser={browser}
btw={btw}
present={store.sideReviewPresent}
/>
</div>
</Show>
</div>
+7 -6
View File
@@ -88,11 +88,12 @@ export interface CommandOption {
category?: string
keybind?: KeybindConfig
slash?: string
slashArguments?: boolean
suggested?: boolean
disabled?: boolean
hidden?: boolean
when?: (event: KeyboardEvent) => boolean
onSelect?: (source?: "palette" | "keybind" | "slash") => void
onSelect?: (source?: "palette" | "keybind" | "slash", input?: string) => void | Promise<void>
onHighlight?: () => (() => void) | void
}
@@ -389,9 +390,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
return map
})
const run = (id: string, source?: CommandSource) => {
const run = (id: string, source?: CommandSource, input?: string) => {
const option = optionMap().get(id)
option?.onSelect?.(source)
return option?.onSelect?.(source, input)
}
const showPalette = () => {
@@ -420,7 +421,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
if (!option) return
event.preventDefault()
event.stopPropagation()
option.onSelect?.("keybind")
void option.onSelect?.("keybind")
}
onMount(() => {
@@ -454,8 +455,8 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
return {
register,
trigger(id: string, source?: CommandSource) {
run(id, source)
trigger(id: string, source?: CommandSource, input?: string) {
return run(id, source, input)
},
keybind(id: string) {
const config = keybindConfig(id)
+1 -1
View File
@@ -166,7 +166,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
state.cleanup = undefined
dialog.close()
if (item.type === "command") {
item.option?.onSelect?.("palette")
void item.option?.onSelect?.("palette")
return
}
if (item.type === "session") {
+1 -1
View File
@@ -96,7 +96,7 @@ describe("layout persistence", () => {
test("keeps scoped state and salvages valid tab entries", () => {
const key = "local\u0000L3Byb2plY3Q/session"
const value = decode({
sessionTabs: { old: { all: ["old"] }, [key]: { all: ["a", null, "a", "b"], active: 12 } },
sessionTabs: { old: { all: ["old"] }, [key]: { all: ["a", null, "a", "b", "btw"], active: "btw" } },
sessionView: { old: { scroll: {} }, [key]: { scroll: {}, reviewOpen: ["a", null, "b"] } },
})
expect(value.sessionTabs).toEqual({ [key]: { all: ["a", "b"], active: undefined } })
+4 -3
View File
@@ -18,7 +18,7 @@ import type { ProjectAvatarVariant } from "@opencode/ui/project-avatar"
import { SessionStateKey } from "@/runtime/server/scope"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./helpers"
import { requireServerKey } from "@/shell/routes/session"
import { closeSessionTab, openSessionTab, previewSessionTab, type SessionTabs } from "./session-tabs"
import { closeSessionTab, openSessionTab, previewSessionTab, SESSION_BTW_TAB, type SessionTabs } from "./session-tabs"
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
@@ -97,8 +97,9 @@ const normalizeSessionTabList = (path: ReturnType<typeof createPathHelpers> | un
const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
const path = sessionPath(key)
return {
all: normalizeSessionTabList(path, tabs.all),
active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
all: normalizeSessionTabList(path, tabs.all).filter((tab) => tab !== SESSION_BTW_TAB),
active:
tabs.active === SESSION_BTW_TAB ? undefined : tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
}
}
@@ -1,5 +1,6 @@
export const SESSION_OPEN_FILE_TAB = "open-file"
export const SESSION_BROWSER_TAB = "browser"
export const SESSION_BTW_TAB = "btw"
export const sessionBrowserTab = (tabID: string) => `${SESSION_BROWSER_TAB}:${tabID}`
export const isSessionBrowserTab = (tab: string | undefined) =>
!!tab && (tab === SESSION_BROWSER_TAB || tab.startsWith(`${SESSION_BROWSER_TAB}:`))
+1 -1
View File
@@ -276,7 +276,7 @@ export function TabNavItem(props: {
"overflow-hidden text-clip whitespace-nowrap": !editing(),
"select-text": editing(),
}}
contenteditable={editing() ? true : undefined}
contenteditable={editing() ? "plaintext-only" : undefined}
onDblClick={openRename}
onKeyDown={(event) => {
event.stopPropagation()
@@ -298,7 +298,7 @@ export function TitlebarTabStrip(props: {
: [new PointerActivationConstraints.Distance({ value: 4 })],
preventActivation: (event) =>
isTabCloseTarget(event.target) ||
(event.target instanceof Element && !!event.target.closest('[contenteditable="true"]')),
(event.target instanceof Element && !!event.target.closest("[contenteditable]")),
}),
]}
modifiers={[
+13 -3
View File
@@ -55,7 +55,7 @@ try {
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
await fs.writeFile(plugin, pluginSource())
await waitForPlugin(info.url, headers)
await waitForPlugin(info.url, headers, plugin)
const unauthorizedInfo = await fetch(new URL("/api/info", info.url), {
signal: AbortSignal.timeout(5_000),
@@ -139,7 +139,13 @@ async function waitForReady(url: string, headers: HeadersInit) {
}
function exitsWithin(process: Bun.Subprocess, milliseconds: number) {
return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)])
return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => resolve(false), milliseconds)
process.exited.then(() => {
clearTimeout(timeout)
resolve(true)
})
})
}
function pluginSource() {
@@ -159,11 +165,15 @@ async function pluginIDs(url: string, headers: HeadersInit) {
)
}
async function waitForPlugin(url: string, headers: HeadersInit) {
async function waitForPlugin(url: string, headers: HeadersInit, plugin: string) {
const deadline = Date.now() + 10_000
let attempt = 0
while (Date.now() < deadline) {
if ((await pluginIDs(url, headers)).includes("smoke")) return
await Bun.sleep(25)
// Native watchers may coalesce a single creation edge. Keep changing valid source so
// the smoke proves that a later native event is delivered.
if (++attempt % 10 === 0) await fs.writeFile(plugin, `${pluginSource()}// watcher retry ${attempt}\n`)
}
throw new Error("Compiled service did not discover the created plugin")
}
+2 -2
View File
@@ -7,7 +7,7 @@ import { produce, type Draft } from "immer"
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
import path from "path"
import { ConfigMigration } from "./migrate"
import { Info, SchemaURL } from "./schema"
import { Info, normalizeLegacyTabs, SchemaURL } from "./schema"
export * from "./schema"
@@ -119,7 +119,7 @@ function merge(...values: readonly (Info | undefined)[]) {
return Option.getOrElse(
decode(
values.reduce<Record<string, unknown>>(
(result, value) => mergeRecords(result, value ?? {}),
(result, value) => mergeRecords(result, normalizeLegacyTabs(value) ?? {}),
{},
),
),
+8
View File
@@ -8,3 +8,11 @@ export const Info = Schema.Struct({
...Config.Info.fields,
})
export type Info = Schema.Schema.Type<typeof Info>
export function normalizeLegacyTabs(info: Info | undefined) {
if (info?.tabs?.enabled === undefined) return info
const tabs = { ...info.tabs }
tabs.mode ??= tabs.enabled ? "on" : "off"
delete tabs.enabled
return { ...info, tabs }
}
+22 -2
View File
@@ -78,7 +78,7 @@ test("merges inline CLI config content over the global config", async () => {
await Bun.write(
file,
JSON.stringify({
tabs: { enabled: true, scope: "global" },
tabs: { mode: "on", scope: "global" },
keybinds: { "app.exit": "ctrl+q" },
plugins: ["global"],
animations: true,
@@ -105,7 +105,7 @@ test("merges inline CLI config content over the global config", async () => {
}),
)
expect(result.loaded.tabs).toEqual({ enabled: false, scope: "global" })
expect(result.loaded.tabs).toEqual({ mode: "off", scope: "global" })
expect(result.loaded.keybinds).toEqual({ "app.exit": "ctrl+q", "help.show": false })
expect(result.loaded.plugins).toEqual(["inline"])
expect(result.updated).toMatchObject({ animations: false, mouse: false })
@@ -116,6 +116,26 @@ test("merges inline CLI config content over the global config", async () => {
}
})
test("reads the legacy tabs toggle without rewriting it", async () => {
await using directory = await tmpdir()
const file = path.join(directory.path, "cli.json")
await Bun.write(file, JSON.stringify({ tabs: { enabled: false } }))
const config = await run(
directory.path,
Effect.gen(function* () {
const service = yield* Config.Service
expect((yield* service.get()).tabs).toEqual({ mode: "off" })
return yield* service.update((draft) => {
draft.animations = false
})
}),
)
expect(config.tabs).toEqual({ mode: "off" })
expect(await Bun.file(file).json()).toEqual({ tabs: { enabled: false }, animations: false })
})
test("migrates tui and kv config into cli.json", async () => {
await using directory = await tmpdir()
await Bun.write(
+2
View File
@@ -6,6 +6,8 @@
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.
- State model-visible diagnostics, logs, tool descriptions, and instructions directly. The execution context is already clear; do not repeat `Code Mode` or `CodeMode` unless the distinction is necessary.
- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR.
- Program values are `Value` (`src/interpreter/objects.ts`); host values are `unknown` and are copied in at the boundaries (`fromHost`, `fromJson`). Do not widen program-facing signatures back to `unknown`.
- A built-in kind of object is one `Obj` subclass (`Wrapper` for host-backed data such as Date or Map, `Opaque` for machinery such as functions and promises) that overrides `tag`, `toString`, `toPrimitive`, `inspect`, `toHost`, and `iterator` as needed. Do not add `instanceof` ladders over the built-in classes elsewhere; ask the object.
## OpenAPI
+48 -19
View File
@@ -87,7 +87,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and
`const { slice } = values` find the inherited built-in.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
- [ ] `IteratorClose` during destructuring should throw a `TypeError` when `return()` yields a non-object.
## Statements and control flow
@@ -125,8 +124,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `Boolean`, `Number`, `String`, `parseInt`, `parseFloat`, `isFinite`, `isNaN`, and URI helpers as callbacks.
- [x] Built-in method references as callbacks, such as `values.map(Math.abs)`, `records.map(JSON.stringify)`,
`items.forEach(console.log)`, and `Promise.resolve(-1).then(Math.abs)`. Extra callback arguments a built-in
does not consume are ignored, like JS; consumed arguments stay strictly validated (`Math.floor` still rejects a
string). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
does not consume are ignored, like JS, and consumed arguments coerce, like JS (`"3.7".replace(/\d\.\d/,
Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
because `includes` is called without a string `this`.
- [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`),
and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Headers`, `Promise`) throw a `TypeError`,
@@ -146,10 +145,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
assignments, object literal keys, and destructuring or parameter defaults.
- [x] Built-in functions are objects too, with `name` and `length` (`Math.max.length === 2`,
`Array.prototype.push.name === "push"`).
- [ ] A named function expression's name is not bound inside its own body.
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
rather than at the call, so their errors are not thrown synchronously.
- [x] A named function expression's name is bound read-only inside its own body; assigning to it throws a
`TypeError`, as in strict mode.
- [x] Redeclaring a function in the same scope, or alongside a `var`, is allowed: the last declaration wins.
- [x] Generator functions have their own `prototype` (inheriting the shared generator prototype), so
`g() instanceof g` holds. Plain functions have none, since they cannot construct.
- [x] Generator and async generator functions bind parameters (defaults, destructuring) at the call and defer only the
body to the first `next()`, so a bad argument throws synchronously from the call site, as in JS.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
@@ -162,7 +164,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
rejected by every synchronous consumer.
- [x] Synchronous iterator acquisition and result validation follow `IteratorClose` boundaries: consumer errors and
intentional early stops invoke `return()`, acquisition/`next()` failures do not, and an original consumer error
wins over a cleanup failure. Async iterator consumption remains limited to `for await...of` and async `yield*`.
wins over a cleanup failure. A generator's `return()` is an intentional stop, so a `return()` that throws or
yields a non-object surfaces from it as a `TypeError`. Async iterator consumption remains limited to `for await...of` and async `yield*`.
- [x] Portable generator protocol coverage is adapted from pinned Test262 cases for suspended-start, suspended-yield,
and completed states; sync and async `next`/`return`/`throw`; finally yields and completion overrides; rejected
yielded promises; mixed async request queues; sync and async `yield*` forwarding; malformed methods/results;
@@ -196,13 +199,14 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length. Deleting a non-configurable property (`length`) or
assigning a read-only one (`Math.PI`, `fn.name`) throws a `TypeError`, as in strict mode.
assigning a read-only one (`Math.PI`, `fn.name`) throws a `TypeError`, as in strict mode. `delete` of a
non-reference (`delete 0`, `delete f()`) evaluates the operand and is `true`; `delete x` on a variable throws.
- [ ] Operators, `switch` discriminants, template interpolation, and coercion helpers such as `String` and `isNaN`
applied to functions and namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
- [ ] ToPrimitive on object operands: operators, `Error(message)`, `Date` arguments, and `parseInt` radix should call
`valueOf`/`toString` in spec order and surface their throws.
- [ ] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via `toString`) become string keys; only
strings and numbers are accepted.
- [x] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via their built-in string form) become
string keys.
## Promises and tools
@@ -238,6 +242,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
resolving with the promise itself rejects with a `TypeError`. Resolver callables work anywhere callbacks are
accepted, including `.then`/`.catch` handlers and collection callbacks, and vanish at the data boundary like
any function.
- [x] `Promise.withResolvers()`: the same promise and resolver callables as the constructor, as a `{ promise, resolve,
reject }` object.
- [x] `Promise.try(fn, ...args)`: calls `fn` synchronously; a throw rejects, a return fulfils, and a returned promise or
thenable is adopted.
- [x] Recursive assimilation of objects with an own callable `then` field across `Promise.resolve`, combinators,
constructors, reactions, `finally`, `await`, and async returns. Thenable methods run deferred, receive
first-call-wins resolve/reject functions, and ignore throws after settlement. Inherited/accessor `then` fields
@@ -258,7 +266,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
not construct.
- [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged;
primitive wrapper objects (`Object(1)`) are rejected explicitly.
- [x] Computed property names and object spread.
- [x] Computed property names and object spread. Any value works as a key (ToPropertyKey): strings, numbers, and the
two confined symbols as themselves, everything else as its string form (`o[null]` is `o["null"]`, `o[{}]` is
`o["[object Object]"]`), in reads, writes, literals, `in`, and destructuring.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with
synchronous iterator support for `fromEntries`. Sources follow ToObject: strings enumerate by index, other
primitives and wrappers contribute nothing, and `null`/`undefined` throw. `Object.assign` accepts array
@@ -275,7 +285,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and plain-object results.
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString` (calls the value's
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`, `"[object Map]"`, `"[object Promise]"`, and so
on for every built-in kind, as JS reports through `Symbol.toStringTag`), `toLocaleString` (calls the value's
`toString`, as in JS), `valueOf`, `hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
## Arrays
@@ -308,10 +319,12 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
form, like `JSON.stringify`.
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
`undefined`; `includes()` still requires a value.
- [x] Numeric arguments coerce as in JS (ToIntegerOrInfinity): `indexOf(x, "1")`, `slice("1", "3")`, `at(null)`,
`flat(1.9)`, `with(1.5, v)`, `Math.max("3", "2")`, `parseInt("11", "2")`, `(1.5).toFixed("2")`,
`String.fromCharCode("65")`, and the Uint8Array equivalents. `join(sep)` and `JSON.parse(text)` apply ToString
(`join(null)` is `"1null2"`, `JSON.parse(123)` is `123`). `Array.from({ length: "2" })` applies ToLength; a
promise source still throws with an `await` hint rather than JS's silent `[]`. A program object's own
`valueOf`/`toString` is not consulted yet (see ToPrimitive above).
## Strings
@@ -369,7 +382,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `JSON.stringify` function and array replacers. Function replacers receive `(key, value)` in preorder, including
the root, but no `this` holder. Array replacers preserve requested property order, deduplicate names, coerce
number primitives, and ignore non-string/non-number entries. Primitive wrapper entries remain unsupported.
- [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`.
- [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`. An Error prints as
`Error.prototype.toString` would show it (`Error: boom`), wherever it appears in the logged value.
- [x] Captured `console.dir` and `console.table`.
## Date
@@ -412,15 +426,30 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
- [x] `RegExp.escape`.
## Iterator
- [x] `Iterator.prototype.map`, `filter`, `take`, `drop`, and `flatMap` on any iterator or generator: lazy, one source
step per result, closing the source when a callback throws, on early `return()`, or when `for...of` or
destructuring finishes with it early. Once done or closed a helper stays done, and a callback that re-enters its
own helper is a `TypeError`. `take`/`drop` coerce their count and reject `NaN` or negative counts with a
`RangeError`; `flatMap` callbacks must return an iterable or iterator, not a string.
- [x] `Iterator.prototype.reduce`, `toArray`, `forEach`, `some`, `every`, and `find`, closing the source on early exit.
- [x] `Iterator.from(value)` returns iterators and generators as they are, and wraps strings, iterables, and objects
with a `next` method. `Iterator` itself is abstract: calling or constructing it is a `TypeError`.
- [x] Helpers and `Iterator.from` wrappers have `return()`; collection iterators (`array.values()`) do not, as in JS,
so an early exit from them leaves them where they were.
- [ ] `Iterator.concat`, `Iterator.zip`, and `Iterator.zipKeyed` (stage 3 proposals).
## Map and Set
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
- [x] `new Map()` from synchronous iterables of entries.
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, `forEach`, `getOrInsert`, and `getOrInsertComputed`.
`forEach` is live: entries deleted during the walk are skipped and entries added are visited, as in JS.
- [x] `new Set()` from synchronous iterables.
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
- [x] Live `keys`, `values`, `entries`, and `[Symbol.iterator]` iterators for Map and Set; a Set-like operand's `keys()`
may return a built-in iterator or an array.
may return any iterator or an array.
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
- [x] Map and Set values serialize to `{}` at host/JSON boundaries.
- [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
+6 -5
View File
@@ -15,6 +15,7 @@ import {
Obj,
PromiseObj,
record,
type Value,
SetObj,
URLSearchParamsObj,
HeadersObj,
@@ -23,14 +24,14 @@ import { typeofValue } from "./interpreter/references.js"
export type Json = Schema.Json
type Replacer<R> = (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
type Replacer<R> = (args: Array<Value>) => Effect.Effect<Value, unknown, R>
/**
* What `JSON.stringify` would serialize for a program value, as host JSON: `toJSON` is honored, functions and
* `undefined` vanish, non-finite numbers become null, and everything else is copied. Two departures from JS
* so a mistake is not a silent `{}`: an Error serializes as `{ name, message, ...own }`, and a promise throws.
*/
export const toJson = <R>(ctx: Interpreter<R>, value: unknown, replacer?: Replacer<R>) =>
export const toJson = <R>(ctx: Interpreter<R>, value: Value, replacer?: Replacer<R>) =>
walk(ctx, value, replacer, false)
/**
@@ -38,11 +39,11 @@ export const toJson = <R>(ctx: Interpreter<R>, value: unknown, replacer?: Replac
* awaited, a Set crosses as an array, a URLSearchParams as its query string, a Uint8Array asks to be encoded as
* text first, and a `__proto__` key is dropped so host code can never receive one.
*/
export const toBoundary = <R>(ctx: Interpreter<R>, value: unknown) => walk(ctx, value, undefined, true)
export const toBoundary = <R>(ctx: Interpreter<R>, value: Value) => walk(ctx, value, undefined, true)
const walk = <R>(
ctx: Interpreter<R>,
value: unknown,
value: Value,
replacer: Replacer<R> | undefined,
boundary: boolean,
): Effect.Effect<Json | undefined, unknown, R> => {
@@ -104,7 +105,7 @@ const walk = <R>(
}
/** Host JSON as program values: objects and arrays are copied, primitives pass through. */
export const fromJson = <R>(ctx: Interpreter<R>, value: unknown): unknown => {
export const fromJson = <R>(ctx: Interpreter<R>, value: Json | undefined): Value => {
if (value === null || typeof value !== "object") return value
if (Array.isArray(value))
return new Arr(
+33 -23
View File
@@ -1,25 +1,35 @@
import { Effect, Exit } from "effect"
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
import { Cause, Effect, Exit } from "effect"
import type { Interpreter } from "./interpreter.js"
import { primitivePrototype } from "./intrinsics.js"
import { typeError } from "./model.js"
import { Callable, get, Native, DateObj, Obj } from "./objects.js"
import { GeneratorReturn, typeError } from "./model.js"
import {
Callable,
get,
Native,
DateObj,
Obj,
coerceToNumber,
coerceToString,
type Cursor,
type Value,
} from "./objects.js"
import { typeofValue } from "./references.js"
export type IteratorCursor<R> = {
readonly next: Effect.Effect<{ readonly done: boolean; readonly value: unknown }, unknown, R>
readonly close: Effect.Effect<void, unknown, R>
}
/** IteratorClose: a consumer failure closes the iterator and wins over any close failure, except that a generator's
* return() is a return completion, so a failing close wins over it, as after `break`. */
export const preserveConsumerError = <A, R>(
cursor: IteratorCursor<R>,
close: Cursor<R>["close"],
effect: Effect.Effect<A, unknown, R>,
): Effect.Effect<A, unknown, R> =>
Effect.flatMap(Effect.exit(effect), (exit) =>
Exit.isSuccess(exit)
? Effect.succeed(exit.value)
: Effect.andThen(Effect.exit(cursor.close), Effect.failCause(exit.cause)),
)
Effect.flatMap(Effect.exit(effect), (exit) => {
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
return Effect.flatMap(Effect.exit(close), (closed) => {
if (!Exit.isSuccess(closed) && Cause.squash(exit.cause) instanceof GeneratorReturn) {
return Effect.failCause(closed.cause)
}
return Effect.failCause(exit.cause)
})
})
/**
* ToPrimitive: calls `valueOf`/`toString` in hint order and returns the first primitive result. Dates treat the
@@ -27,9 +37,9 @@ export const preserveConsumerError = <A, R>(
*/
export const toPrimitive = <R>(
ctx: Interpreter<R>,
value: unknown,
value: Value,
hint: "number" | "string" | "default",
): Effect.Effect<unknown, unknown, R> => {
): Effect.Effect<Value, unknown, R> => {
if (!(value instanceof Obj)) return Effect.succeed(value)
const asString = hint === "string" || (hint === "default" && value instanceof DateObj)
const order = asString ? ["toString", "valueOf"] : ["valueOf", "toString"]
@@ -45,30 +55,30 @@ export const toPrimitive = <R>(
}
/** Invoke(value, name): calls the method the value would find through its prototype. */
export const invoke = <R>(ctx: Interpreter<R>, value: unknown, name: string, label: string) => {
export const invoke = <R>(ctx: Interpreter<R>, value: Value, name: string, label: string) => {
const target = value instanceof Obj ? value : primitivePrototype(ctx.builtins, value)
if (target === undefined) throw typeError(`${label} called on null or undefined.`)
return ctx.call(get(target, name), value, [])
}
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: unknown) =>
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: Value) =>
Effect.map(toPrimitive(ctx, value, "string"), coerceToString)
export const toPrimitiveNumber = <R>(ctx: Interpreter<R>, value: unknown) =>
export const toPrimitiveNumber = <R>(ctx: Interpreter<R>, value: Value) =>
Effect.map(toPrimitive(ctx, value, "number"), coerceToNumber)
// The single acceptance list for callbacks: collections, sort, string replacers,
// Array.from mappers, and promise reactions all admit exactly these callables.
// Admission means dispatchable, not necessarily invocable: new-requiring
// constructors pass the gate and throw a TypeError on call, like JS.
export const isSupportedCallback = (value: unknown): value is Callable =>
export const isSupportedCallback = (value: Value): value is Callable =>
value instanceof Callable && !(value instanceof Native && !value.callback)
export const applyCollectionCallback = <R>(
ctx: Interpreter<R>,
callback: unknown,
callback: Value,
name: string,
): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
): ((args: Array<Value>) => Effect.Effect<Value, unknown, R>) => {
if (!isSupportedCallback(callback)) {
if (typeofValue(callback) === "function") {
throw typeError(
+18 -7
View File
@@ -6,10 +6,21 @@ import { type AstNode, formatLocation, PendingThrow, Throw, sourceLocation, type
import { containsRuntimeReference } from "./references.js"
import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js"
import { constructor, methods, prototypeFrom, receiver } from "./native.js"
import { type Callable, define, get, has, hidden, type Native, Arr, ErrorObj, Obj } from "./objects.js"
import {
type Callable,
define,
get,
has,
hidden,
type Native,
Arr,
ErrorObj,
Obj,
coerceToString,
type Value,
} from "./objects.js"
import type { Interpreter } from "./interpreter.js"
import { formatValue } from "../stdlib/console.js"
import { coerceToString } from "../stdlib/value.js"
export const normalizeError = (error: unknown): Diagnostic => {
if (error instanceof PendingThrow) {
@@ -86,7 +97,7 @@ export const locate = (error: unknown, node?: AstNode): unknown => {
}
/** The program value a handler receives for a failure; one failure always yields the same value. */
export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): unknown => {
export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): Value => {
if (thrown instanceof Throw) return thrown.value
const builtins = ctx.builtins
if (thrown instanceof PendingThrow) {
@@ -113,7 +124,7 @@ const errorToString = (self: Obj): string => {
export const createAggregateErrorValue = <R>(
ctx: Interpreter<R>,
errors: Array<unknown>,
errors: Array<Value>,
message: string,
proto: Obj = ctx.builtins.AggregateError,
) => {
@@ -124,13 +135,13 @@ export const createAggregateErrorValue = <R>(
const constructAggregateErrorValue = <R>(
ctx: Interpreter<R>,
args: Array<unknown>,
args: Array<Value>,
proto: Obj,
): Effect.Effect<ErrorObj, unknown, R> =>
Effect.gen(function* () {
const cursor = yield* ctx.iterate(args[0])
if (cursor === undefined) throw typeError("new AggregateError(...) expects a synchronous iterable of errors.")
const errors: Array<unknown> = []
const errors: Array<Value> = []
while (true) {
const step = yield* cursor.next
if (step.done) {
@@ -144,7 +155,7 @@ const constructAggregateErrorValue = <R>(
export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const prototype = builtins[type]
const construct = (args: Array<unknown>, newTarget: Callable) => {
const construct = (args: Array<Value>, newTarget: Callable) => {
const proto = prototypeFrom(newTarget, prototype)
const created =
type === "AggregateError"
+2 -1
View File
@@ -4,6 +4,7 @@ import type { DataValue, Diagnostic, ResolvedExecutionLimits, Result } from "../
import { toBoundary } from "../data.js"
import { ToolRuntime } from "../tool-runtime.js"
import { normalizeError } from "./errors.js"
import type { Value } from "./objects.js"
import { createBuiltins } from "./intrinsics.js"
import { Pending } from "./promises.js"
import { Interpreter } from "./interpreter.js"
@@ -13,7 +14,7 @@ export const executeProgram = <R>(
prepared: ToolRuntime.Prepared<R>,
limits: ResolvedExecutionLimits,
hooks: ToolRuntime.Hooks<R>,
globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>,
globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, Value]>,
): Effect.Effect<Result, never, R> => {
if (code.trim().length === 0) {
return Effect.succeed({
+13 -41
View File
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import type { Extension } from "../extension.js"
import { coerceToString } from "../stdlib/value.js"
import { type ExtensionInvocation, hooked } from "../tool-runtime.js"
import type { Interpreter } from "./interpreter.js"
import { createErrorValue, isErrorType } from "./intrinsics.js"
@@ -8,9 +7,7 @@ import { MAX_VALUE_DEPTH } from "./limits.js"
import { PendingThrow, Throw, typeError } from "./model.js"
import { fn } from "./native.js"
import {
Callable,
define,
entries,
get,
has,
hidden,
@@ -19,18 +16,17 @@ import {
Bytes,
DateObj,
ErrorObj,
GeneratorObj,
IteratorObj,
HeadersObj,
MapObj,
Obj,
PromiseObj,
RegExpObj,
SetObj,
URLObj,
URLSearchParamsObj,
HeadersObj,
coerceToString,
type Value,
} from "./objects.js"
import { describeValue } from "./references.js"
import { describeValue, isOpaque } from "./references.js"
/**
* The global bindings of one run's extensions. Everything crossing the boundary is converted: plain data and
@@ -40,35 +36,18 @@ import { describeValue } from "./references.js"
export const extensionGlobals = <R>(
ctx: Interpreter<R>,
extensions: ReadonlyArray<Extension>,
): ReadonlyArray<readonly [string, unknown]> => {
): ReadonlyArray<readonly [string, Value]> => {
const builtins = ctx.builtins
const toHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
const toHost = (value: Value, label: string, depth = 0, seen = new Set<object>()): unknown => {
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
if (value === null || typeof value !== "object") {
if (isPrimitive(value)) return value
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
}
if (value instanceof Bytes) return new Uint8Array(value.bytes)
if (value instanceof DateObj) return new Date(value.time)
if (value instanceof RegExpObj) return new RegExp(value.regex.source, value.regex.flags)
if (value instanceof URLObj) return new URL(value.url.href)
if (value instanceof URLSearchParamsObj) return new URLSearchParams(value.params)
if (value instanceof HeadersObj) return new Headers(value.headers)
const next = (item: unknown) => toHost(item, label, depth + 1, seen)
if (value instanceof MapObj) return new Map([...value.map].map(([key, item]) => [next(key), next(item)]))
if (value instanceof SetObj) return new Set([...value.set].map(next))
if (
!(value instanceof Obj) ||
value instanceof Callable ||
value instanceof GeneratorObj ||
value instanceof IteratorObj ||
value instanceof PromiseObj
) {
if (isPrimitive(value)) return value
if (!(value instanceof Obj) || isOpaque(value)) {
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
}
if (seen.has(value)) throw typeError(`${label} contains a circular value.`)
seen.add(value)
const next = (item: Value) => toHost(item, label, depth + 1, seen)
if (value instanceof ErrorObj) {
const name = coerceToString(get(value, "name"))
const message = get(value, "message")
@@ -89,19 +68,12 @@ export const extensionGlobals = <R>(
seen.delete(value)
return copied
}
const copied =
value instanceof Arr
? value.items.map(next)
: Object.fromEntries(
entries(value)
.filter(([key]) => key !== "__proto__")
.map(([key, item]) => [key, next(item)]),
)
const copied = value.toHost(next)
seen.delete(value)
return copied
}
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): Value => {
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
if (isPrimitive(value)) return value
if (typeof value === "function") return wrap(value, label)
@@ -200,7 +172,7 @@ export const extensionGlobals = <R>(
*/
const uncrossed = new Set(["stack", "constructor", "toString", "__proto__"])
const left = Symbol("left behind")
const crossing = (convert: () => unknown): unknown => {
const crossing = <T>(convert: () => T): T | typeof left => {
try {
return convert()
} catch (reason) {
@@ -219,7 +191,7 @@ const hostErrors = new Map<string, ErrorConstructor>([
])
// The primitives the interpreter operates on; symbols and BigInts are not among them.
const isPrimitive = (value: unknown): boolean =>
const isPrimitive = (value: unknown): value is string | number | boolean | null | undefined =>
value === null ||
value === undefined ||
typeof value === "string" ||
@@ -1,7 +1,7 @@
import { Effect } from "effect"
import { fn, type Method, methods, receiver } from "./native.js"
import { AsyncIteratorSymbol, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
import { define, hidden, GeneratorObj } from "./objects.js"
import { define, hidden, GeneratorObj, type Value } from "./objects.js"
import type { Interpreter } from "./interpreter.js"
/** `next`/`return`/`throw` on the generator prototypes; async generators answer with promises. */
@@ -13,9 +13,9 @@ export const generatorGlobals = <R>(ctx: Interpreter<R>): void => {
const request = (kind: GeneratorRequestKind): Method => [
kind,
1,
(thisValue: unknown, args: Array<unknown>) => {
(thisValue: Value, args: Array<Value>) => {
const generator = receiver(GeneratorObj, thisValue, `${label}.prototype.${kind}`)
const requested = generator.request(kind, args[0]) as Effect.Effect<unknown, unknown, R>
const requested = generator.request(kind, args[0]) as Effect.Effect<Value, unknown, R>
return generator.asynchronous ? ctx.pending.create(requested) : requested
},
]
+4 -3
View File
@@ -1,4 +1,5 @@
import { Effect } from "effect"
import type { Value } from "./objects.js"
import { arrayGlobal } from "../stdlib/array.js"
import { textDecoderGlobal, textEncoderGlobal, uint8ArrayGlobal } from "../stdlib/bytes.js"
import { mapGlobal, setGlobal } from "../stdlib/collections.js"
@@ -51,7 +52,7 @@ const symbolGlobal = <R>(ctx: Interpreter<R>) => {
return symbol
}
type Factory = <R>(ctx: Interpreter<R>) => unknown
type Factory = <R>(ctx: Interpreter<R>) => Value
// A table rather than a list so the names are known before any runtime exists.
const table: Record<string, Factory> = {
@@ -69,6 +70,7 @@ const table: Record<string, Factory> = {
console: (ctx) => consoleGlobal(ctx),
Promise: (ctx) => promiseGlobal(ctx),
Symbol: (ctx) => symbolGlobal(ctx),
Iterator: (ctx) => iteratorGlobals(ctx),
Number: (ctx) => numberGlobal(ctx),
String: (ctx) => stringGlobal(ctx),
Boolean: (ctx) => booleanGlobal(ctx),
@@ -100,8 +102,7 @@ const table: Record<string, Factory> = {
export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
/** The immutable global bindings of every program, in declaration order. */
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, unknown]> => {
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, Value]> => {
generatorGlobals(ctx)
iteratorGlobals(ctx)
return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const)
}
+182 -196
View File
@@ -32,6 +32,7 @@ import type {
Statement,
Super,
SwitchStatement,
Literal,
TemplateLiteral,
ThrowStatement,
TryStatement,
@@ -69,26 +70,26 @@ import {
Callable,
define,
get,
hostCursor,
IteratorObj,
type Cursor,
has,
hidden,
hasPrototype,
keys,
Native,
parseArrayIndex,
Arr,
Bytes,
DateObj,
Fn,
GeneratorObj,
IteratorObj,
MapObj,
Obj,
PromiseObj,
SetObj,
URLSearchParamsObj,
HeadersObj,
record,
remove,
set,
coerceToNumber,
coerceToString,
type Value,
} from "./objects.js"
import { preserveConsumerError } from "./callback.js"
import { Pending, resolvePromise, resolvePromiseValue } from "./promises.js"
@@ -96,7 +97,7 @@ import { containsOpaqueReference, describeValue, rejectCircularInsertion, typeof
import { ScopeStack } from "./scope.js"
import { constructRegExp } from "../stdlib/regexp.js"
import { enumerableSource } from "../stdlib/object.js"
import { coerceToNumber, coerceToString, compoundOperators } from "../stdlib/value.js"
import { compoundOperators } from "../stdlib/value.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.
@@ -132,7 +133,14 @@ const calleeDescription = (node: Expression | Super | undefined): string | undef
}
// OrdinaryHasInstance: walk the left operand's chain looking for the constructor's `prototype`.
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
// acorn types every literal as possibly a BigInt or RegExp; regex literals become RegExp objects before this is asked.
const literal = (node: Literal): Value => {
if (typeof node.value === "bigint") throw typeError("BigInt literals are not supported.", node)
if (node.value instanceof RegExp) throw unsupportedSyntax("RegExpLiteral", node)
return node.value
}
const instanceofValue = (lhs: Value, rhs: Value, node: AstNode): boolean => {
if (!(rhs instanceof Callable)) {
throw typeError("The right-hand side of 'instanceof' is not callable.", node)
}
@@ -229,7 +237,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
type CustomIterator = {
iterator: Obj
next: unknown
next: Value
asynchronous: boolean
}
@@ -237,13 +245,13 @@ type CustomIterator = {
type MemberReference = {
target: Obj
key: PropertyKey
receiver: unknown
receiver: Value
}
type GeneratorRequest = {
kind: GeneratorRequestKind
value: unknown
response: Deferred.Deferred<unknown, unknown>
value: Value
response: Deferred.Deferred<Value, unknown>
}
type GeneratorState = {
@@ -269,7 +277,7 @@ export class Interpreter<R> {
readonly pending: Pending<R>
readonly builtins: Builtins
readonly logs?: Array<string>
readonly globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>
readonly globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, Value]>
}) {
this.tools = options.tools
this.pending = options.pending
@@ -283,27 +291,31 @@ export class Interpreter<R> {
}
}
run(program: Program): Effect.Effect<unknown, unknown, R> {
run(program: Program): Effect.Effect<Value, unknown, R> {
return this.root.run(program)
}
call(callable: unknown, thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
call(callable: Value, thisValue: Value, args: Array<Value>): Effect.Effect<Value, unknown, R> {
return this.root.call(callable, thisValue, args)
}
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
await(promise: PromiseObj): Effect.Effect<Value, unknown, never> {
return this.root.await(promise)
}
iterate(value: unknown) {
iterate(value: Value) {
return this.root.iterate(value)
}
iterateDirect(value: Value) {
return this.root.iterateDirect(value)
}
/** Runs one host tool: arguments cross as JSON and the result comes back as program values. */
tool(
run: (args: Array<Json | undefined>) => Effect.Effect<Json | undefined, unknown, R>,
args: Array<unknown>,
): Effect.Effect<unknown, unknown, R> {
args: Array<Value>,
): Effect.Effect<Value, unknown, R> {
const ctx = this
return Effect.gen(function* () {
const json = yield* Effect.forEach(args, (arg) => toBoundary(ctx, arg))
@@ -326,7 +338,7 @@ class Frame<R> {
private depth = 0,
) {}
run(program: Program): Effect.Effect<unknown, unknown, R> {
run(program: Program): Effect.Effect<Value, unknown, R> {
const self = this
// Keep top-level declarations separate so they can shadow builtins.
this.scopes.push()
@@ -334,7 +346,7 @@ class Frame<R> {
self.predeclareLexical(program.body)
self.hoistFunctions(program.body)
self.hoistVars(program.body)
let value: unknown = undefined
let value: Value = undefined
for (const [index, statement] of program.body.entries()) {
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
value = yield* self.evaluateExpression(statement.expression)
@@ -359,15 +371,12 @@ class Frame<R> {
}
// Fork at the call site so admission and hooks occur when the call is made.
private createToolCallPromise(
path: ReadonlyArray<string>,
args: Array<unknown>,
): Effect.Effect<PromiseObj, never, R> {
private createToolCallPromise(path: ReadonlyArray<string>, args: Array<Value>): Effect.Effect<PromiseObj, never, R> {
return this.ctx.pending.create(this.ctx.tool((json) => this.ctx.tools.execute(path, json), args))
}
// Fiber exits make settlement idempotent; yielding prevents inline continuation.
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
await(promise: PromiseObj): Effect.Effect<Value, unknown, never> {
const pending = this.ctx.pending
return Effect.suspend(() => {
pending.markObserved(promise)
@@ -446,8 +455,9 @@ class Frame<R> {
node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? ""),
): Fn {
return new Fn(
this.ctx.builtins.Function,
const builtins = this.ctx.builtins
const fn = new Fn(
builtins.Function,
name,
node.params,
node.body,
@@ -455,20 +465,29 @@ class Frame<R> {
node.async,
node.generator,
)
// Each generator function gets its own prototype, so `g() instanceof g` holds as in JS.
if (node.generator)
define(fn, "prototype", new Obj(node.async ? builtins.AsyncGenerator : builtins.Generator), hidden)
// The body of a named function expression sees its own name, read-only.
if (node.type === "FunctionExpression" && node.id) {
fn.capturedScopes.push(new Map([[node.id.name, { mutable: false, value: fn, initialized: true }]]))
}
return fn
}
// NamedEvaluation: an anonymous function definition takes the name of what it is assigned to.
private evaluateNamed(node: Expression, name: string): Effect.Effect<unknown, unknown, R> {
private evaluateNamed(node: Expression, name: string): Effect.Effect<Value, unknown, R> {
if (node.type === "ArrowFunctionExpression" || (node.type === "FunctionExpression" && !node.id)) {
return Effect.sync(() => this.createFunction(node, name))
}
return this.evaluateExpression(node)
}
// Repeated `function` declarations and `var` clashes are legal: the last declaration wins.
private hoistFunctions(statements: ReadonlyArray<Statement | ModuleDeclaration>): void {
for (const node of statements) {
if (node.type !== "FunctionDeclaration") continue
this.scopes.declare(node.id.name, this.createFunction(node), true, node)
this.scopes.current().set(node.id.name, { mutable: true, value: this.createFunction(node), initialized: true })
}
}
@@ -656,11 +675,11 @@ class Frame<R> {
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
const right = yield* self.evaluateExpression(node.right)
const cursor = self.hostCursor(right)
const cursor = self.builtinCursor(right)
const iterator = cursor === undefined ? yield* self.customIterator(right, node, awaiting) : undefined
if (iterator === undefined && cursor === undefined) {
throw invalidData(
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, URLSearchParams, or Headers, or custom iterator value.`,
`${awaiting ? "for await...of" : "for...of"} requires an iterable value, received ${describeValue(right)}.`,
node,
)
}
@@ -676,7 +695,7 @@ class Frame<R> {
}
const assignment = left.type === "VariableDeclaration" ? undefined : left
const evaluateBody = (value: unknown) =>
const evaluateBody = (value: Value) =>
Effect.gen(function* () {
if (declared?.lexical) {
self.scopes.push()
@@ -705,10 +724,8 @@ class Frame<R> {
const bodyExit = yield* Effect.exit(evaluateBody(step.value))
if (!Exit.isSuccess(bodyExit)) {
// Process interruption must remain prompt; user cleanup cannot extend a timeout.
if (!Cause.hasInterruptsOnly(bodyExit.cause)) {
yield* Effect.exit(close())
}
return yield* Effect.failCause(bodyExit.cause)
if (Cause.hasInterruptsOnly(bodyExit.cause)) return yield* Effect.failCause(bodyExit.cause)
return yield* preserveConsumerError(close(), Effect.failCause(bodyExit.cause))
}
const exit = loopExit(bodyExit.value, labels)
if (exit !== undefined) {
@@ -725,7 +742,7 @@ class Frame<R> {
)
}
private awaitValue(value: unknown): Effect.Effect<unknown, unknown, R> {
private awaitValue(value: Value): Effect.Effect<Value, unknown, R> {
return Effect.flatMap(resolvePromise(this.ctx, value), (promise) =>
Effect.ensuring(
this.await(promise),
@@ -736,10 +753,10 @@ class Frame<R> {
private awaitAsyncFromSyncValue(
iterator: CustomIterator,
value: unknown,
value: Value,
node: AstNode | undefined,
closeOnRejection: boolean,
): Effect.Effect<unknown, unknown, R> {
): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
const settled = yield* Effect.exit(self.awaitValue(value))
@@ -751,54 +768,50 @@ class Frame<R> {
})
}
iterate(value: unknown, node?: AstNode) {
const cursor = this.hostCursor(value)
iterate(value: Value, node?: AstNode): Effect.Effect<Cursor<R> | undefined, unknown, R> {
const cursor = this.builtinCursor(value)
if (cursor !== undefined) return Effect.succeed(cursor)
const self = this
return Effect.map(this.customIterator(value, node, false), (iterator) =>
iterator === undefined
? undefined
: {
next: self.nextIteratorResult(iterator, node, false),
close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
},
iterator === undefined ? undefined : this.customCursor(iterator, node),
)
}
private hostCursor(value: unknown) {
const iterator =
value instanceof Arr
? value.items[Symbol.iterator]()
: typeof value === "string"
? value[Symbol.iterator]()
: value instanceof MapObj
? value.map.entries()
: value instanceof SetObj
? value.set.values()
: value instanceof URLSearchParamsObj
? value.params.entries()
: value instanceof HeadersObj
? value.headers.entries()
: value instanceof Bytes
? value.bytes.values()
: value instanceof IteratorObj
? value.iterator
: undefined
if (iterator === undefined) return undefined
const proto = this.ctx.builtins.Array
/** GetIteratorDirect: drive an iterator by its own `next`, without asking for `[Symbol.iterator]`. */
iterateDirect(value: Value, node?: AstNode): Cursor<R> {
if (value instanceof IteratorObj) return value.cursor as Cursor<R>
if (!(value instanceof Obj)) {
throw typeError(`An iterator must be an object, received ${describeValue(value)}.`, node)
}
return this.customCursor(
{
iterator: value,
next: this.requireIteratorMethod(get(value, "next"), "Iterator next", node),
asynchronous: false,
},
node,
)
}
private customCursor(iterator: CustomIterator, node: AstNode | undefined): Cursor<R> {
return {
next: Effect.sync(() => {
const step = iterator.next()
return {
done: Boolean(step.done),
value: Array.isArray(step.value) ? new Arr(proto, step.value) : step.value,
}
}),
close: Effect.void,
next: this.nextIteratorResult(iterator, node, false),
close: Effect.suspend(() => this.closeIterator(iterator, node, false)),
}
}
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
private builtinCursor(value: Value): Cursor<R> | undefined {
// Natives build their cursors without knowing R, like `lift` in native.ts.
if (value instanceof IteratorObj) return value.cursor as Cursor<R>
const iterator =
typeof value === "string"
? value[Symbol.iterator]()
: value instanceof Obj
? value.iterator(this.ctx.builtins)
: undefined
return iterator === undefined ? undefined : hostCursor(iterator)
}
private customIterator(value: Value, node: AstNode | undefined, allowAsync = true) {
if (!(value instanceof Obj)) return Effect.undefined
const asyncMethod = allowAsync ? get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? get(value, IteratorSymbol)
@@ -888,18 +901,18 @@ class Frame<R> {
})
}
private requireIteratorObject(value: unknown, context: string, node?: AstNode): Obj {
private requireIteratorObject(value: Value, context: string, node?: AstNode): Obj {
if (value instanceof Obj) return value
throw typeError(`${context} must be an object.`, node)
}
private requireIteratorMethod(value: unknown, context: string, node?: AstNode): unknown {
private requireIteratorMethod(value: Value, context: string, node?: AstNode): Value {
if (typeofValue(value) === "function") return value
throw typeError(`${context} must be a function.`, node)
}
// for...in over null/undefined iterates nothing, like JS.
private enumerableKeys(value: unknown, node: AstNode): Array<string> {
private enumerableKeys(value: Value, node: AstNode): Array<string> {
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
if (value === null || value === undefined) return []
return keys(enumerableSource(this.ctx, "for...in", value, node))
@@ -1063,7 +1076,7 @@ class Frame<R> {
private declarePattern(
pattern: Pattern,
value: unknown,
value: Value,
mutable: boolean,
node: AstNode,
initialize = false,
@@ -1123,7 +1136,7 @@ class Frame<R> {
})
}
private assignPattern(pattern: Pattern, value: unknown, node: AstNode): Effect.Effect<void, unknown, R> {
private assignPattern(pattern: Pattern, value: Value, node: AstNode): Effect.Effect<void, unknown, R> {
const self = this
return Effect.gen(function* () {
if (pattern.type === "Identifier") {
@@ -1175,7 +1188,7 @@ class Frame<R> {
})
}
private evaluateDefault(pattern: AssignmentPattern): Effect.Effect<unknown, unknown, R> {
private evaluateDefault(pattern: AssignmentPattern): Effect.Effect<Value, unknown, R> {
return pattern.left.type === "Identifier"
? this.evaluateNamed(pattern.right, pattern.left.name)
: this.evaluateExpression(pattern.right)
@@ -1183,8 +1196,8 @@ class Frame<R> {
private destructureArrayPattern(
pattern: ArrayPattern,
value: unknown,
consume: (target: Pattern, value: unknown, context: AstNode) => Effect.Effect<void, unknown, R>,
value: Value,
consume: (target: Pattern, value: Value, context: AstNode) => Effect.Effect<void, unknown, R>,
): Effect.Effect<void, unknown, R> {
const self = this
return Effect.gen(function* () {
@@ -1208,7 +1221,7 @@ class Frame<R> {
done = step.done
if (element === null) continue
if (element.type === "RestElement") {
const rest: Array<unknown> = []
const rest: Array<Value> = []
if (!step.done) rest.push(step.value)
while (!done) {
const next = yield* cursor.next
@@ -1219,7 +1232,7 @@ class Frame<R> {
return
}
const consumed = consume(element, step.done ? undefined : step.value, pattern)
yield* step.done ? consumed : preserveConsumerError(cursor, consumed)
yield* step.done ? consumed : preserveConsumerError(cursor.close, consumed)
}
if (!done) yield* cursor.close
})
@@ -1231,20 +1244,19 @@ class Frame<R> {
}
const keyNode = property.key
if (property.computed) {
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode))
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value))
}
if (keyNode.type === "Identifier") return Effect.succeed(keyNode.name)
if (keyNode.type === "Literal") return Effect.succeed(String(keyNode.value))
throw unsupportedSyntax(keyNode.type, keyNode)
}
private evaluateExpression(node: Expression): Effect.Effect<unknown, unknown, R> {
private evaluateExpression(node: Expression): Effect.Effect<Value, unknown, R> {
switch (node.type) {
case "Literal": {
const regex = node.regex
if (regex) return Effect.sync(() => constructRegExp(this.ctx.builtins, [regex.pattern, regex.flags]))
if (typeof node.value === "bigint") throw typeError("BigInt literals are not supported.", node)
return Effect.succeed(node.value)
return Effect.succeed(literal(node))
}
case "Identifier":
return Effect.sync(() => this.scopes.get(node.name, node))
@@ -1259,7 +1271,7 @@ class Frame<R> {
case "SequenceExpression": {
const self = this
return Effect.gen(function* () {
let result: unknown
let result: Value
for (const expression of node.expressions) {
result = yield* self.evaluateExpression(expression)
}
@@ -1300,7 +1312,7 @@ class Frame<R> {
}
}
private evaluateNewExpression(node: NewExpression): Effect.Effect<unknown, unknown, R> {
private evaluateNewExpression(node: NewExpression): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
const callee = yield* self.evaluateExpression(node.callee)
@@ -1324,7 +1336,7 @@ class Frame<R> {
})
}
private evaluateBinaryExpression(node: BinaryExpression): Effect.Effect<unknown, unknown, R> {
private evaluateBinaryExpression(node: BinaryExpression): Effect.Effect<Value, unknown, R> {
const operator = node.operator
const left = node.left
if (left.type === "PrivateIdentifier") throw unsupportedSyntax(left.type, left)
@@ -1337,7 +1349,7 @@ class Frame<R> {
})
}
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
private applyBinaryOperator(operator: string, lhs: Value, rhs: Value, node: AstNode): Value {
if (operator === "===") return lhs === rhs
if (operator === "!==") return lhs !== rhs
if (operator === "in" && rhs instanceof Obj && !containsOpaqueReference(lhs)) {
@@ -1346,14 +1358,9 @@ class Frame<R> {
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
throw invalidData("Binary operators require data values.", node)
}
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
// Dates use their default string hint for addition and loose equality, and epoch time elsewhere.
const coerceOperand = (operand: unknown): unknown => {
if (operand instanceof DateObj) {
return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time
}
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
}
// Addition and loose equality use the default hint; every other operator asks for a number.
const hint = operator === "+" || operator === "==" || operator === "!=" ? "default" : "number"
const coerceOperand = (operand: Value) => (operand instanceof Obj ? operand.toPrimitive(hint) : operand)
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
const l = coerceOperand(lhs)
const r = coerceOperand(rhs)
@@ -1407,7 +1414,7 @@ class Frame<R> {
}
}
private evaluateLogicalExpression(node: LogicalExpression): Effect.Effect<unknown, unknown, R> {
private evaluateLogicalExpression(node: LogicalExpression): Effect.Effect<Value, unknown, R> {
const operator = node.operator
return Effect.flatMap(this.evaluateExpression(node.left), (left) => {
if (operator === "&&") return left ? this.evaluateExpression(node.right) : Effect.succeed(left)
@@ -1418,7 +1425,7 @@ class Frame<R> {
})
}
private evaluateUnaryExpression(node: UnaryExpression): Effect.Effect<unknown, unknown, R> {
private evaluateUnaryExpression(node: UnaryExpression): Effect.Effect<Value, unknown, R> {
const operator = node.operator
const argument = node.argument
if (operator === "delete") return this.evaluateDeleteExpression(argument)
@@ -1433,13 +1440,8 @@ class Frame<R> {
if (containsOpaqueReference(value)) {
throw invalidData("Unary operators require data values.", node)
}
const operand =
value instanceof DateObj
? value.time
: value !== null && typeof value === "object"
? coerceToString(value)
: value
let result: unknown
const operand = value instanceof Obj ? value.toPrimitive("number") : value
let result: Value
switch (operator) {
case "+":
result = +(operand as number)
@@ -1457,7 +1459,7 @@ class Frame<R> {
})
}
private evaluateAssignmentExpression(node: AssignmentExpression): Effect.Effect<unknown, unknown, R> {
private evaluateAssignmentExpression(node: AssignmentExpression): Effect.Effect<Value, unknown, R> {
const left = node.left
const operator = node.operator
const self = this
@@ -1497,9 +1499,9 @@ class Frame<R> {
node: AssignmentExpression,
left: Pattern,
operator: string,
): Effect.Effect<unknown, unknown, R> {
): Effect.Effect<Value, unknown, R> {
const self = this
const shouldAssign = (current: unknown): boolean =>
const shouldAssign = (current: Value): boolean =>
operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current)
if (left.type === "Identifier") {
const name = left.name
@@ -1524,7 +1526,7 @@ class Frame<R> {
throw typeError("Assignment target must be an Identifier or MemberExpression.", left)
}
private evaluateUpdateExpression(node: UpdateExpression): Effect.Effect<unknown, unknown, R> {
private evaluateUpdateExpression(node: UpdateExpression): Effect.Effect<Value, unknown, R> {
const operator = node.operator
const argument = node.argument
const prefix = node.prefix
@@ -1537,7 +1539,7 @@ class Frame<R> {
// CodeMode numeric coercion, not host Number(): null-prototype data objects would make
// the host throw during ToPrimitive, and opaque runtime references must reject clearly.
const operand = (current: unknown): number => {
const operand = (current: Value): number => {
if (containsOpaqueReference(current)) {
throw invalidData(`'${operator}' requires a data value.`, argument)
}
@@ -1566,7 +1568,7 @@ class Frame<R> {
}
// EvaluateCall: a member callee supplies its base object as `this`; anything else calls with undefined.
private evaluateCallExpression(node: CallExpression): Effect.Effect<unknown, unknown, R> {
private evaluateCallExpression(node: CallExpression): Effect.Effect<Value, unknown, R> {
const callee = node.callee
const self = this
@@ -1584,7 +1586,7 @@ class Frame<R> {
})
}
private readMethod(node: MemberExpression): Effect.Effect<{ callable: unknown; thisValue: unknown }, unknown, R> {
private readMethod(node: MemberExpression): Effect.Effect<{ callable: Value; thisValue: Value }, unknown, R> {
return Effect.map(this.getMemberReference(node), (reference) => {
if (reference === OptionalShortCircuit) return { callable: OptionalShortCircuit, thisValue: undefined }
if (reference instanceof ToolReference) return { callable: reference, thisValue: undefined }
@@ -1595,12 +1597,12 @@ class Frame<R> {
// The single dispatch for every invocation: call expressions and callbacks share it.
call(
callable: unknown,
thisValue: unknown,
args: Array<unknown>,
callable: Value,
thisValue: Value,
args: Array<Value>,
node?: AstNode,
callee?: Expression,
): Effect.Effect<unknown, unknown, R> {
): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
if (callable instanceof ToolReference) {
@@ -1618,7 +1620,7 @@ class Frame<R> {
}
// Built-ins throw without a location, synchronously or inside their Effect; the call site supplies it.
private native(body: () => Effect.Effect<unknown, unknown, R>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
private native(body: () => Effect.Effect<Value, unknown, R>, node?: AstNode): Effect.Effect<Value, unknown, R> {
return Effect.provideService(
Effect.catchDefect(Effect.suspend(body), (defect) => Effect.die(locate(defect, node))),
CallSite,
@@ -1628,10 +1630,10 @@ class Frame<R> {
private evaluateCallArguments(
argNodes: ReadonlyArray<Expression | SpreadElement>,
): Effect.Effect<Array<unknown>, unknown, R> {
): Effect.Effect<Array<Value>, unknown, R> {
const self = this
return Effect.gen(function* () {
const args: Array<unknown> = []
const args: Array<Value> = []
for (const argNode of argNodes) {
if (argNode.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(argNode.argument)
@@ -1651,20 +1653,20 @@ class Frame<R> {
}
// A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
invokeFunction(fn: Fn, args: Array<unknown>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
invokeFunction(fn: Fn, args: Array<Value>, node?: AstNode): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.flatMap(CallSite, (site) => {
const depth = Math.max(self.depth, site.depth) + 1
if (depth > MAX_CALL_DEPTH) throw rangeError("Maximum call stack size exceeded", node)
const invocation = new Frame(this.ctx, new ScopeStack([...fn.capturedScopes, new Map()]), depth)
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
}
const bind = Effect.gen(function* () {
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
yield* invocation.declarePattern(
@@ -1678,7 +1680,8 @@ class Frame<R> {
}
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
}
})
const body = Effect.gen(function* () {
if (fn.body.type === "BlockStatement") {
invocation.scopes.push()
invocation.hoistVars(fn.body.body, paramScope)
@@ -1688,7 +1691,9 @@ class Frame<R> {
return yield* invocation.evaluateExpression(fn.body)
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
// Generators bind parameters at the call and defer only the body to the first `next()`, as in JS.
if (fn.generator) return Effect.map(bind, () => this.createGenerator(invocation, body, fn))
const run = Effect.andThen(bind, body)
if (!fn.async) return run
return this.ctx.pending.createWithSelf((self) =>
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.ctx, value, self)),
@@ -1696,18 +1701,15 @@ class Frame<R> {
})
}
private createGenerator(
invocation: Frame<R>,
run: Effect.Effect<unknown, unknown, R>,
asynchronous: boolean,
): GeneratorObj {
private createGenerator(invocation: Frame<R>, run: Effect.Effect<Value, unknown, R>, fn: Fn): GeneratorObj {
const asynchronous = fn.async
const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
invocation.generatorState = state
invocation.generatorAsync = asynchronous
const builtins = this.ctx.builtins
const result = (value: unknown, done: boolean) => record(builtins.Object, { value, done })
const request = (kind: GeneratorRequestKind, value: unknown) => {
const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
const result = (value: Value, done: boolean) => record(builtins.Object, { value, done })
const request = (kind: GeneratorRequestKind, value: Value) => {
const request = { kind, value, response: Deferred.makeUnsafe<Value, unknown>() }
if (!asynchronous && state.active) return Effect.die(typeError("Generator is already running."))
if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
state.started = true
@@ -1768,17 +1770,13 @@ class Frame<R> {
}
return Deferred.await(request.response)
}
const generator = new GeneratorObj(
asynchronous ? builtins.AsyncGenerator : builtins.Generator,
asynchronous,
request,
)
return generator
const proto = get(fn, "prototype")
return new GeneratorObj(proto instanceof Obj ? proto : builtins.Generator, asynchronous, request)
}
private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
const self = this
const result = (value: unknown, done: boolean) => record(self.ctx.builtins.Object, { value, done })
const result = (value: Value, done: boolean) => record(self.ctx.builtins.Object, { value, done })
return Effect.gen(function* () {
while (true) {
const pending = self.dequeueGeneratorRequest(state)
@@ -1824,7 +1822,7 @@ class Frame<R> {
return request
}
private evaluateYieldExpression(node: YieldExpression): Effect.Effect<unknown, unknown, R> {
private evaluateYieldExpression(node: YieldExpression): Effect.Effect<Value, unknown, R> {
const argument = node.argument
const self = this
return Effect.gen(function* () {
@@ -1839,7 +1837,7 @@ class Frame<R> {
})
}
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
private suspendGenerator(value: Value, node: AstNode): Effect.Effect<Value, unknown, R> {
const state = this.generatorState
if (!state?.active) throw typeError("Generator has no active request.", node)
Deferred.doneUnsafe(state.active.response, Exit.succeed(record(this.ctx.builtins.Object, { value, done: false })))
@@ -1854,20 +1852,11 @@ class Frame<R> {
})
}
private delegateYield(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
private delegateYield(value: Value, node: AstNode): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
if (
value instanceof Arr ||
typeof value === "string" ||
value instanceof MapObj ||
value instanceof SetObj ||
value instanceof URLSearchParamsObj ||
value instanceof HeadersObj ||
value instanceof Bytes
) {
const cursor = yield* self.iterate(value, node)
if (!cursor) throw typeError("Built-in iterator is unavailable.", node)
const cursor = self.builtinCursor(value)
if (cursor !== undefined) {
while (true) {
const step = yield* cursor.next
if (step.done) return undefined
@@ -1891,7 +1880,7 @@ class Frame<R> {
const iterator = yield* self.customIterator(value, node, self.generatorAsync)
if (!iterator) throw typeError("yield* requires a compatible iterable value.", node)
let kind: GeneratorRequestKind = "next"
let input: unknown = undefined
let input: Value = undefined
while (true) {
const method = kind === "next" ? iterator.next : get(iterator.iterator, kind)
if (method === undefined || method === null) {
@@ -1911,7 +1900,7 @@ class Frame<R> {
node,
)
const done = Boolean(get(result, "done"))
const resultValue: unknown =
const resultValue: Value =
self.generatorAsync && !iterator.asynchronous
? yield* self.awaitAsyncFromSyncValue(iterator, get(result, "value"), node, kind !== "return" && !done)
: get(result, "value")
@@ -1920,7 +1909,7 @@ class Frame<R> {
return resultValue
}
const resumed: Exit.Exit<unknown, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
const resumed: Exit.Exit<Value, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
if (Exit.isSuccess(resumed)) {
kind = "next"
input = resumed.value
@@ -1957,11 +1946,11 @@ class Frame<R> {
let key: PropertyKey
if (property.computed) {
key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode)
key = self.toPropertyKey(yield* self.evaluateExpression(keyNode))
} else if (keyNode.type === "Identifier") {
key = keyNode.name
} else if (keyNode.type === "Literal") {
key = self.toPropertyKey(keyNode.value, keyNode)
key = self.toPropertyKey(literal(keyNode))
} else {
throw typeError("Unsupported object property key shape.", keyNode)
}
@@ -1980,7 +1969,7 @@ class Frame<R> {
}
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<Arr, unknown, R> {
const values: Array<unknown> = []
const values: Array<Value> = []
const self = this
return Effect.gen(function* () {
@@ -2035,13 +2024,13 @@ class Frame<R> {
})
}
private evaluateConditionalExpression(node: ConditionalExpression): Effect.Effect<unknown, unknown, R> {
private evaluateConditionalExpression(node: ConditionalExpression): Effect.Effect<Value, unknown, R> {
return Effect.flatMap(this.evaluateExpression(node.test), (test) =>
this.evaluateExpression(test ? node.consequent : node.alternate),
)
}
private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown {
private applyCompoundAssignment(operator: string, current: Value, incoming: Value, node: AstNode): Value {
if (!compoundOperators.has(operator)) {
throw typeError(`Unsupported assignment operator '${operator}'.`, node)
}
@@ -2050,7 +2039,7 @@ class Frame<R> {
private getMemberReference(
node: MemberExpression,
): Effect.Effect<MemberReference | ToolReference | { value: unknown } | typeof OptionalShortCircuit, unknown, R> {
): Effect.Effect<MemberReference | ToolReference | { value: Value } | typeof OptionalShortCircuit, unknown, R> {
const objectNode = node.object
const propertyNode = node.property
if (objectNode.type === "Super") throw unsupportedSyntax(objectNode.type, objectNode)
@@ -2062,10 +2051,10 @@ class Frame<R> {
if ((objectValue === null || objectValue === undefined) && node.optional) return OptionalShortCircuit
const key = node.computed
? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
? self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
: propertyNode.type === "Identifier"
? propertyNode.name
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
@@ -2092,7 +2081,7 @@ class Frame<R> {
})
}
private readReference(reference: MemberReference, node: MemberExpression): unknown {
private readReference(reference: MemberReference, node: MemberExpression): Value {
// Reject unknown promise properties so a missing await cannot hide.
if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
throw invalidData(
@@ -2104,7 +2093,7 @@ class Frame<R> {
}
// Accessors throw without a location; the member or pattern that read them supplies it.
private readProperty(target: Obj, key: PropertyKey, node: AstNode, receiver: unknown = target): unknown {
private readProperty(target: Obj, key: PropertyKey, node: AstNode, receiver: Value = target): Value {
try {
return get(target, key, receiver)
} catch (error) {
@@ -2112,7 +2101,7 @@ class Frame<R> {
}
}
private readMember(node: MemberExpression): Effect.Effect<unknown, unknown, R> {
private readMember(node: MemberExpression): Effect.Effect<Value, unknown, R> {
return Effect.map(this.getMemberReference(node), (reference) => {
if (reference === OptionalShortCircuit) return OptionalShortCircuit
if (reference instanceof ToolReference) return reference
@@ -2121,15 +2110,15 @@ class Frame<R> {
})
}
private writeMember(node: MemberExpression, value: unknown): Effect.Effect<unknown, unknown, R> {
private writeMember(node: MemberExpression, value: Value): Effect.Effect<Value, unknown, R> {
return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
}
private evaluateDeleteExpression(argument: Expression): Effect.Effect<boolean, unknown, R> {
const target = argument.type === "ChainExpression" ? argument.expression : argument
if (target.type !== "MemberExpression") {
throw typeError("Only data fields may be deleted.", argument)
}
if (target.type === "Identifier") throw typeError("Only data fields may be deleted.", argument)
// `delete <non-reference>` evaluates the operand and is true, as in JS.
if (target.type !== "MemberExpression") return Effect.map(this.evaluateExpression(target), () => true)
return Effect.map(this.getMemberReference(target), (reference) => {
if (reference === OptionalShortCircuit) return true
if (reference instanceof ToolReference || "value" in reference || reference.receiver !== reference.target) {
@@ -2143,8 +2132,8 @@ class Frame<R> {
// Resolve side-effecting object and key expressions exactly once.
private modifyMember(
node: MemberExpression,
compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>,
): Effect.Effect<unknown, unknown, R> {
compute: (current: Value) => Effect.Effect<{ write: boolean; next: Value; result: Value }, unknown, R>,
): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
const reference = yield* self.getMemberReference(node)
@@ -2164,7 +2153,7 @@ class Frame<R> {
})
}
private assignToReference(target: Obj, key: PropertyKey, next: unknown, node: AstNode): void {
private assignToReference(target: Obj, key: PropertyKey, next: Value, node: AstNode): void {
const written = (() => {
try {
rejectCircularInsertion(
@@ -2182,12 +2171,9 @@ class Frame<R> {
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
}
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
if (typeof value === "string" || typeof value === "number") {
return value
}
if (value === AsyncIteratorSymbol || value === IteratorSymbol) return value
throw typeError("Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.", node)
// ToPropertyKey: anything else becomes its string form, so `counts[row.category]` works when the field is null.
private toPropertyKey(value: Value): PropertyKey {
if (typeof value === "string" || typeof value === "number" || typeof value === "symbol") return value
return coerceToString(value)
}
}
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define, hidden, Native, Arr, ErrorObj, Obj } from "./objects.js"
import { define, hidden, Native, Arr, ErrorObj, Obj, type Value } from "./objects.js"
export const errorTypes = [
"Error",
@@ -35,6 +35,7 @@ const builtins = [
"TextDecoder",
"Promise",
"Iterator",
"IteratorHelper",
"AsyncIterator",
"Generator",
"AsyncGenerator",
@@ -53,7 +54,7 @@ export const createErrorValue = (prototype: Obj, message: string | undefined): E
}
/** The prototype a primitive reads its methods from without being boxed; none for null, undefined, and symbols. */
export const primitivePrototype = (builtins: Builtins, value: unknown): Obj | undefined => {
export const primitivePrototype = (builtins: Builtins, value: Value): Obj | undefined => {
if (typeof value === "string") return builtins.String
if (typeof value === "number") return builtins.Number
if (typeof value === "boolean") return builtins.Boolean
@@ -95,6 +96,7 @@ export const createBuiltins = (): Builtins => {
TextDecoder: plain(),
Promise: plain(),
Iterator: iterator,
IteratorHelper: new Obj(iterator),
AsyncIterator: asyncIterator,
Generator: new Obj(iterator),
AsyncGenerator: new Obj(asyncIterator),
+5 -8
View File
@@ -2,7 +2,7 @@ import type { Node } from "acorn"
import { Context } from "effect"
import type { ErrorType } from "./intrinsics.js"
import type { DiagnosticKind } from "../codemode.js"
import type { ErrorObj } from "./objects.js"
import type { ErrorObj, Value } from "./objects.js"
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
export type AstNode = Node
@@ -14,13 +14,13 @@ export const CallSite = Context.Reference<{ readonly node?: AstNode; readonly de
export type Binding = {
mutable: boolean
value: unknown
value: Value
initialized?: boolean
}
export type StatementResult =
| { kind: "none" }
| { kind: "return"; value: unknown }
| { kind: "return"; value: Value }
| { kind: "break"; label?: string }
| { kind: "continue"; label?: string }
@@ -31,11 +31,11 @@ export const IteratorSymbol: unique symbol = Symbol("codemode.iterator")
export const IteratorSymbols = [AsyncIteratorSymbol, IteratorSymbol] as const
export class Throw {
constructor(readonly value: unknown) {}
constructor(readonly value: Value) {}
}
export class GeneratorReturn {
constructor(readonly value: unknown) {}
constructor(readonly value: Value) {}
}
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
@@ -83,9 +83,6 @@ export const unsupportedSyntax = (kind: string, node: AstNode): PendingThrow =>
[supportedSyntaxMessage],
)
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null
// Acorn lines are 1-based and its columns are 0-based. Diagnostics use 1-based columns of the submitted source.
export const sourceLocation = (node: AstNode): { readonly line: number; readonly column: number } => ({
line: node.loc?.start.line ?? 1,
+7 -7
View File
@@ -1,18 +1,18 @@
import { Effect } from "effect"
import type { Builtins } from "./intrinsics.js"
import { typeError } from "./model.js"
import { type Callable, define, frozen, hidden, Native, type NativeOptions, Obj } from "./objects.js"
import { type Callable, define, frozen, hidden, Native, type NativeOptions, Obj, type Value } from "./objects.js"
import { describeValue } from "./references.js"
/** A native function body: a plain value, a thrown `PendingThrow`, or an Effect. */
export type Impl = (thisValue: unknown, args: Array<unknown>) => unknown
/** A native function body: a value, a thrown `PendingThrow`, or an Effect of a value. */
export type Impl = (thisValue: Value, args: Array<Value>) => Value | Effect.Effect<Value, unknown, unknown>
// The dispatch in `Frame.call` suspends every native call, so a synchronous throw here is a defect.
const lift =
<R>(impl: Impl) =>
(thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
(thisValue: Value, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
const result = impl(thisValue, args)
return Effect.isEffect(result) ? (result as Effect.Effect<unknown, unknown, R>) : Effect.succeed(result)
return Effect.isEffect(result) ? (result as Effect.Effect<Value, unknown, R>) : Effect.succeed(result)
}
export const native = <R>(builtins: Builtins, options: NativeOptions<R>): Native<R> =>
@@ -27,7 +27,7 @@ export const methods = (builtins: Builtins, target: Obj, table: ReadonlyArray<Me
for (const [name, length, impl] of table) define(target, name, fn(builtins, name, length, impl), hidden)
}
export const constants = (target: Obj, table: Record<string, unknown>): void => {
export const constants = (target: Obj, table: Record<string, Value>): void => {
for (const [name, value] of Object.entries(table)) define(target, name, value, frozen)
}
@@ -54,7 +54,7 @@ export const prototypeFrom = (newTarget: Callable, fallback: Obj): Obj => {
/** Narrows a method receiver to the built-in it belongs to, or throws the TypeError JS would. */
export const receiver = <T extends Obj>(
cls: abstract new (...args: never) => T,
thisValue: unknown,
thisValue: Value,
method: string,
): T => {
if (thisValue instanceof cls) return thisValue
+271 -49
View File
@@ -1,5 +1,7 @@
import type { BlockStatement, Expression, Pattern } from "acorn"
import type { Effect, Fiber } from "effect"
import { Effect, type Fiber } from "effect"
import { ToolReference } from "../tool-runtime.js"
import type { Builtins } from "./intrinsics.js"
import { checkArrayLength } from "./limits.js"
import {
AsyncIteratorSymbol,
@@ -16,12 +18,12 @@ export type Attributes = {
readonly configurable: boolean
}
export type Getter = (receiver: unknown) => unknown
export type Setter = (receiver: unknown, value: unknown) => void
export type Getter = (receiver: Value) => Value
export type Setter = (receiver: Value, value: Value) => void
/** One own property: a data slot or a native accessor pair. */
export type Slot =
| { value: unknown; writable: boolean; enumerable: boolean; configurable: boolean }
| { value: Value; writable: boolean; enumerable: boolean; configurable: boolean }
| { get: Getter | undefined; set: Setter | undefined; enumerable: boolean; configurable: boolean }
/** Ordinary assignment: writable, enumerable, configurable. */
@@ -33,33 +35,122 @@ export const readonly: Attributes = { writable: false, enumerable: false, config
/** Constants such as `Math.PI` and a constructor's `prototype`. */
export const frozen: Attributes = { writable: false, enumerable: false, configurable: false }
/** An object owned by the program: own properties plus a prototype link. */
/**
* An object owned by the program: own properties plus a prototype link. Subclasses answer, in one place, how a
* built-in kind of object prints, coerces, iterates, and crosses to the host.
*/
export class Obj {
readonly props = new Map<string | symbol, Slot>()
constructor(public proto: Obj | null) {}
/** The class name `Object.prototype.toString` reports: `[object Map]`. */
readonly tag: string = "Object"
/** How diagnostics refer to a value of this kind. */
get describe(): string {
if (this.tag === "Object") return "a data object"
return `${/^[AEIO]/.test(this.tag) ? "an" : "a"} ${this.tag}`
}
/** ToString without consulting program-defined methods. */
toString(): string {
return `[object ${this.tag}]`
}
/** ToPrimitive without consulting program-defined methods: only a Date answers a number hint differently. */
toPrimitive(hint: "default" | "number" | "string"): string | number {
return this.toString()
}
/** ToNumber without consulting program-defined methods. */
toNumber(): number {
return Number(this.toPrimitive("number"))
}
/** How `console.log` shows the value; `item` formats a child with cycle and depth tracking. */
inspect(item: (value: Value) => string): string {
return `{${entries(this)
.map(([key, value]) => `${JSON.stringify(key)}:${item(value)}`)
.join(",")}}`
}
/** A copy the host can hold; `item` converts a child. A `__proto__` key never reaches host code. */
toHost(item: (value: Value) => unknown): unknown {
return Object.fromEntries(
entries(this)
.filter(([key]) => key !== "__proto__")
.map(([key, value]) => [key, item(value)]),
)
}
/** The built-in iteration `for...of` and spread use, when this kind of object has one. */
iterator(builtins: Builtins): Iterator<Value, undefined> | undefined {
return undefined
}
}
export class Arr extends Obj {
override readonly tag = "Array"
constructor(
proto: Obj,
readonly items: Array<unknown> = [],
readonly items: Array<Value> = [],
) {
super(proto)
}
override get describe() {
return "an array"
}
override toString() {
return this.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
}
override inspect(item: (value: Value) => string) {
return `[${this.items.map(item).join(",")}]`
}
override toHost(item: (value: Value) => unknown) {
return this.items.map(item)
}
override iterator() {
return this.items.values()
}
}
/** An object with the [[ErrorData]] slot: what `Error.prototype.toString` and the host boundary recognize as an error. */
export class ErrorObj extends Obj {
override readonly tag = "Error"
/** The interpreter failure this error materialized from, so rethrowing it keeps the diagnostic kind and location. */
host?: PendingThrow
/** Error.prototype.toString: "name: message", or just one when the other is empty. */
override toString() {
const name = get(this, "name")
const message = get(this, "message")
const shownName = typeof name === "string" ? name : "Error"
const shownMessage = typeof message === "string" ? message : ""
if (shownMessage === "") return shownName
if (shownName === "") return shownMessage
return `${shownName}: ${shownMessage}`
}
override inspect() {
return this.toString()
}
}
export abstract class Callable extends Obj {
/** Interpreter machinery a program can hold but never inspect, serialize, or hand to the host. */
export abstract class Opaque extends Obj {
override inspect() {
return "[opaque reference]"
}
}
export abstract class Callable extends Opaque {
override readonly tag = "Function"
constructor(proto: Obj, name: string, length: number) {
super(proto)
define(this, "length", length, readonly)
define(this, "name", name, readonly)
}
override get describe() {
return "a function"
}
}
export class Fn extends Callable {
@@ -68,7 +159,7 @@ export class Fn extends Callable {
name: string,
readonly parameters: ReadonlyArray<Pattern>,
readonly body: BlockStatement | Expression,
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
readonly capturedScopes: Array<Map<string, Binding>>,
readonly async: boolean,
readonly generator: boolean,
) {
@@ -77,8 +168,8 @@ export class Fn extends Callable {
}
}
export type NativeCall<R> = (thisValue: unknown, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
export type NativeConstruct<R> = (args: Array<unknown>, newTarget: Callable) => Effect.Effect<unknown, unknown, R>
export type NativeCall<R> = (thisValue: Value, args: Array<Value>) => Effect.Effect<Value, unknown, R>
export type NativeConstruct<R> = (args: Array<Value>, newTarget: Callable) => Effect.Effect<Value, unknown, R>
export type NativeOptions<R> = {
readonly name: string
@@ -103,79 +194,182 @@ export class Native<R = never> extends Callable {
}
}
export class PromiseObj extends Obj {
export class PromiseObj extends Opaque {
override readonly tag = "Promise"
constructor(
proto: Obj,
readonly fiber: Fiber.Fiber<unknown, unknown>,
readonly fiber: Fiber.Fiber<Value, unknown>,
) {
super(proto)
}
override get describe() {
return "an un-awaited Promise"
}
override inspect() {
return "[Promise (await it to get its value)]"
}
}
export class GeneratorObj extends Obj {
export class GeneratorObj extends Opaque {
override readonly tag = "Generator"
constructor(
proto: Obj,
readonly asynchronous: boolean,
readonly request: (kind: GeneratorRequestKind, value: unknown) => Effect.Effect<unknown, unknown, unknown>,
readonly request: (kind: GeneratorRequestKind, value: Value) => Effect.Effect<Value, unknown, unknown>,
) {
super(proto)
}
override get describe() {
return "a generator"
}
}
/** A built-in collection iterator: live over the host collection, yielding program values. */
export class IteratorObj extends Obj {
/** One pull from an iterator, as `for...of` sees it. */
export type Step = { readonly done: boolean; readonly value: Value }
/** How the interpreter drives any iterator: pull the next step, or close it early. */
export type Cursor<R = unknown> = {
readonly next: Effect.Effect<Step, unknown, R>
readonly close: Effect.Effect<void, unknown, R>
}
/** A cursor over a host iterator; there is nothing to close. */
export const hostCursor = (iterator: Iterator<Value, undefined>): Cursor<never> => ({
next: Effect.sync(() => {
const step = iterator.next()
return { done: Boolean(step.done), value: step.value }
}),
close: Effect.void,
})
/** A built-in iterator: a live cursor over a host collection or an iterator helper, yielding program values. */
export class IteratorObj extends Opaque {
override readonly tag = "Iterator"
constructor(
proto: Obj,
readonly iterator: IteratorObject<unknown>,
readonly cursor: Cursor,
) {
super(proto)
}
override get describe() {
return "an iterator"
}
}
export class DateObj extends Obj {
/** A built-in iterator over a host iterator, e.g. `array.values()`. */
export const hostIterator = (builtins: Builtins, iterator: Iterator<Value, undefined>): IteratorObj =>
new IteratorObj(builtins.Iterator, hostCursor(iterator))
/** A built-in object around a host value: data-like, so it prints as itself and crosses to extensions as a copy. */
export abstract class Wrapper extends Obj {
override inspect(item: (value: Value) => string) {
return this.toString()
}
}
export class DateObj extends Wrapper {
override readonly tag = "Date"
constructor(
proto: Obj,
public time: number,
) {
super(proto)
}
override toString() {
return Number.isFinite(this.time) ? new Date(this.time).toISOString() : "Invalid Date"
}
override toPrimitive(hint: "default" | "number" | "string") {
return hint === "number" ? this.time : this.toString()
}
override toHost() {
return new Date(this.time)
}
}
export class RegExpObj extends Obj {
export class RegExpObj extends Wrapper {
override readonly tag = "RegExp"
readonly regex: RegExp
constructor(proto: Obj, pattern: string, flags: string) {
super(proto)
this.regex = new RegExp(pattern, flags)
}
override toString() {
return `/${this.regex.source}/${this.regex.flags}`
}
override toHost() {
return new RegExp(this.regex.source, this.regex.flags)
}
}
export class MapObj extends Obj {
readonly map = new Map<unknown, unknown>()
export class MapObj extends Wrapper {
override readonly tag = "Map"
readonly map = new Map<Value, Value>()
override inspect(item: (value: Value) => string) {
return `Map(${this.map.size}) [${[...this.map].map(([key, value]) => `[${item(key)},${item(value)}]`).join(",")}]`
}
override toHost(item: (value: Value) => unknown) {
return new Map([...this.map].map(([key, value]) => [item(key), item(value)]))
}
override iterator(builtins: Builtins) {
return this.map.entries().map((entry) => new Arr(builtins.Array, entry))
}
}
export class SetObj extends Obj {
readonly set = new Set<unknown>()
export class SetObj extends Wrapper {
override readonly tag = "Set"
readonly set = new Set<Value>()
override inspect(item: (value: Value) => string) {
return `Set(${this.set.size}) [${[...this.set].map(item).join(",")}]`
}
override toHost(item: (value: Value) => unknown) {
return new Set([...this.set].map(item))
}
override iterator() {
return this.set.values()
}
}
export class URLSearchParamsObj extends Obj {
export class URLSearchParamsObj extends Wrapper {
override readonly tag = "URLSearchParams"
constructor(
proto: Obj,
readonly params: URLSearchParams,
) {
super(proto)
}
override toString() {
return this.params.toString()
}
override toHost() {
return new URLSearchParams(this.params)
}
override iterator(builtins: Builtins) {
return this.params.entries().map((entry) => new Arr(builtins.Array, entry))
}
}
export class HeadersObj extends Obj {
export class HeadersObj extends Wrapper {
override readonly tag = "Headers"
constructor(
proto: Obj,
readonly headers: Headers,
) {
super(proto)
}
override inspect() {
return `Headers ${JSON.stringify(Object.fromEntries(this.headers))}`
}
override toHost() {
return new Headers(this.headers)
}
override iterator(builtins: Builtins) {
// Bun's Headers typings lack the iterator helpers, so the host iterator is lifted first.
return Iterator.from(this.headers.entries()).map((entry) => new Arr(builtins.Array, entry))
}
}
export class URLObj extends Obj {
export class URLObj extends Wrapper {
override readonly tag = "URL"
readonly searchParams: URLSearchParamsObj
constructor(
proto: Obj,
@@ -185,30 +379,58 @@ export class URLObj extends Obj {
super(proto)
this.searchParams = new URLSearchParamsObj(searchParamsProto, url.searchParams)
}
override toString() {
return this.url.href
}
override toHost() {
return new URL(this.url.href)
}
}
/** A `Uint8Array`: the host array does the byte clamping and ignores out-of-range writes, as JS does. */
export class Bytes extends Obj {
export class Bytes extends Wrapper {
override readonly tag = "Uint8Array"
constructor(
proto: Obj,
readonly bytes: Uint8Array,
) {
super(proto)
}
override toString() {
return this.bytes.join(",")
}
override inspect() {
return `Uint8Array(${this.bytes.length}) [${this.bytes.join(",")}]`
}
override toHost() {
return new Uint8Array(this.bytes)
}
override iterator() {
return this.bytes.values()
}
}
/** Built-in objects that wrap a host value; data-like, but never plain data. */
export const isWrapper = (
value: unknown,
): value is DateObj | RegExpObj | MapObj | SetObj | URLObj | URLSearchParamsObj | HeadersObj | Bytes =>
value instanceof DateObj ||
value instanceof RegExpObj ||
value instanceof MapObj ||
value instanceof SetObj ||
value instanceof URLObj ||
value instanceof URLSearchParamsObj ||
value instanceof HeadersObj ||
value instanceof Bytes
/** Every value a program can hold. Host values never appear here; they are copied in at the boundaries. */
export type Value = string | number | boolean | null | undefined | symbol | Obj | ToolReference
/** ToString without consulting program-defined methods. */
export const coerceToString = (value: Value): string => (value instanceof Obj ? value.toString() : String(value))
/** ToNumber without consulting program-defined methods; tool references are not numbers. */
export const coerceToNumber = (value: Value): number => {
if (value instanceof Obj) return value.toNumber()
return value instanceof ToolReference ? Number.NaN : Number(value)
}
/** ToIntegerOrInfinity: NaN is 0, fractions truncate. */
export const coerceToInteger = (value: Value): number => {
const number = coerceToNumber(value)
return Number.isNaN(number) ? 0 : Math.trunc(number)
}
/** Values that cannot cross the data boundary: opaque machinery and host-backed wrappers. */
export const isRuntimeReference = (value: Value): boolean =>
value instanceof Opaque || value instanceof Wrapper || value instanceof ToolReference
const MAX_ARRAY_INDEX = 4_294_967_295
@@ -226,7 +448,7 @@ type Indexed = Arr | Bytes
const isIndexed = (target: Obj): target is Indexed => target instanceof Arr || target instanceof Bytes
const elements = (target: Indexed): Array<unknown> | Uint8Array => (target instanceof Arr ? target.items : target.bytes)
const elements = (target: Indexed): Array<Value> | Uint8Array => (target instanceof Arr ? target.items : target.bytes)
const index = (target: Obj, key: string | symbol): number | undefined =>
isIndexed(target) && typeof key === "string" ? parseArrayIndex(key) : undefined
@@ -247,18 +469,18 @@ export const own = (target: Obj, key: PropertyKey): Slot | undefined => {
return target.props.get(name)
}
const read = (slot: Slot, receiver: unknown): unknown =>
const read = (slot: Slot, receiver: Value): Value =>
"value" in slot ? slot.value : slot.get === undefined ? undefined : slot.get(receiver)
export const hasOwn = (target: Obj, key: PropertyKey): boolean => own(target, key) !== undefined
export const getOwn = (target: Obj, key: PropertyKey): unknown => {
export const getOwn = (target: Obj, key: PropertyKey): Value => {
const slot = own(target, key)
return slot === undefined ? undefined : read(slot, target)
}
/** [[Get]]: walks the prototype chain; accessors see `receiver`, which is the primitive for wrapper prototypes. */
export const get = (target: Obj, key: PropertyKey, receiver: unknown = target): unknown => {
export const get = (target: Obj, key: PropertyKey, receiver: Value = target): Value => {
for (let current: Obj | null = target; current !== null; current = current.proto) {
const slot = own(current, key)
if (slot !== undefined) return read(slot, receiver)
@@ -273,14 +495,14 @@ export const has = (target: Obj, key: PropertyKey): boolean => {
return false
}
export const hasPrototype = (value: unknown, proto: Obj): boolean => {
export const hasPrototype = (value: Value, proto: Obj): boolean => {
for (let current = value instanceof Obj ? value.proto : null; current !== null; current = current.proto) {
if (current === proto) return true
}
return false
}
const writeElement = (target: Indexed, name: string | symbol, value: unknown): boolean | undefined => {
const writeElement = (target: Indexed, name: string | symbol, value: Value): boolean | undefined => {
const at = index(target, name)
if (at !== undefined) {
if (target instanceof Bytes) target.bytes[at] = typeof value === "number" ? value : Number(value)
@@ -296,7 +518,7 @@ const writeElement = (target: Indexed, name: string | symbol, value: unknown): b
}
/** [[Set]]: an inherited setter or read-only property decides before an own data property is created. */
export const set = (target: Obj, key: PropertyKey, value: unknown): boolean => {
export const set = (target: Obj, key: PropertyKey, value: Value): boolean => {
const name = canonical(key)
for (let current: Obj | null = target; current !== null; current = current.proto) {
const slot = own(current, name)
@@ -324,7 +546,7 @@ export const set = (target: Obj, key: PropertyKey, value: unknown): boolean => {
}
/** [[DefineOwnProperty]] for a data property, ignoring the chain. */
export const define = (target: Obj, key: PropertyKey, value: unknown, attrs: Attributes = data): void => {
export const define = (target: Obj, key: PropertyKey, value: Value, attrs: Attributes = data): void => {
const name = canonical(key)
if (isIndexed(target) && writeElement(target, name, value) !== undefined) return
target.props.set(name, { value, ...attrs })
@@ -375,9 +597,9 @@ export const keys = (target: Obj): Array<string> =>
ownKeys(target).filter((key): key is string => typeof key === "string" && enumerable(target, key))
/** Own enumerable string entries: `Object.entries` and serialization. */
export const entries = (target: Obj): Array<[string, unknown]> => keys(target).map((key) => [key, getOwn(target, key)])
export const entries = (target: Obj): Array<[string, Value]> => keys(target).map((key) => [key, getOwn(target, key)])
export const record = (proto: Obj, fields: Record<string, unknown>): Obj => {
export const record = (proto: Obj, fields: Record<string, Value>): Obj => {
const target = new Obj(proto)
for (const [key, value] of Object.entries(fields)) define(target, key, value)
return target
+53 -29
View File
@@ -2,15 +2,15 @@ import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
import type { Diagnostic } from "../codemode.js"
import { MAX_PENDING_PROMISES } from "./limits.js"
import { CallSite, Throw, rangeError, typeError } from "./model.js"
import { Callable, define, get, hidden, Arr, Fn, Obj, PromiseObj, record } from "./objects.js"
import { Callable, define, get, hidden, Arr, Fn, Obj, PromiseObj, record, type Value } from "./objects.js"
import { constructor, fn, methods, native, receiver, requiresNew } from "./native.js"
import { createAggregateErrorValue, locate, materialize, normalizeError } from "./errors.js"
import { typeofValue } from "./references.js"
import { describeValue, typeofValue } from "./references.js"
import { applyCollectionCallback, isSupportedCallback } from "./callback.js"
import type { Interpreter } from "./interpreter.js"
// A `resolve`/`reject` handed to an executor or thenable: calling it settles the capability.
const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: unknown) => void) =>
const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: Value) => void) =>
fn(ctx.builtins, name, 1, (_, args) => {
settle(args[0])
return undefined
@@ -31,7 +31,7 @@ export class Pending<R> {
// Resolution bodies need the promise's own identity to reject `resolve(promise)` self-resolution.
createWithSelf(
body: (self: { promise?: PromiseObj }) => Effect.Effect<unknown, unknown, R>,
body: (self: { promise?: PromiseObj }) => Effect.Effect<Value, unknown, R>,
): Effect.Effect<PromiseObj, never, R> {
const self: { promise?: PromiseObj } = {}
return Effect.map(this.create(body(self)), (promise) => {
@@ -40,7 +40,7 @@ export class Pending<R> {
})
}
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<PromiseObj, never, R> {
create(effect: Effect.Effect<Value, unknown, R>): Effect.Effect<PromiseObj, never, R> {
return Effect.flatMap(CallSite, (site) => {
if (this.active.size >= MAX_PENDING_PROMISES) {
throw rangeError(
@@ -79,7 +79,7 @@ export class Pending<R> {
if (id !== undefined) this.failures.delete(id)
}
await(promise: PromiseObj): Effect.Effect<Exit.Exit<unknown, unknown>> {
await(promise: PromiseObj): Effect.Effect<Exit.Exit<Value, unknown>> {
return Fiber.await(promise.fiber)
}
@@ -105,9 +105,9 @@ export class Pending<R> {
export const resolvePromiseValue = <R>(
ctx: Interpreter<R>,
value: unknown,
value: Value,
own?: { promise?: PromiseObj },
): Effect.Effect<unknown, unknown, R> => {
): Effect.Effect<Value, unknown, R> => {
if (own?.promise !== undefined && value === own.promise) {
return Effect.die(typeError("Chaining cycle detected: a promise cannot resolve with itself."))
}
@@ -119,7 +119,7 @@ export const resolvePromiseValue = <R>(
return Effect.gen(function* () {
// Promise resolution invokes a thenable's method in a later job.
yield* Effect.yieldNow
const deferred = Deferred.makeUnsafe<unknown, unknown>()
const deferred = Deferred.makeUnsafe<Value, unknown>()
const resolve = capability(ctx, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)))
const reject = capability(ctx, "reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(reason))))
const executed = yield* Effect.exit(ctx.call(then, value, [resolve, reject]))
@@ -131,29 +131,46 @@ export const resolvePromiseValue = <R>(
})
}
export const resolvePromise = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<PromiseObj, never, R> => {
export const resolvePromise = <R>(ctx: Interpreter<R>, value: Value): Effect.Effect<PromiseObj, never, R> => {
if (value instanceof PromiseObj) return Effect.succeed(value)
return ctx.pending.createWithSelf((self) => resolvePromiseValue(ctx, value, self))
}
const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"] as const
const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject", "withResolvers", "try"] as const
const invokePromiseMethod = <R>(
ctx: Interpreter<R>,
name: (typeof promiseStatics)[number],
args: Array<unknown>,
): Effect.Effect<unknown, unknown, R> => {
args: Array<Value>,
): Effect.Effect<Value, unknown, R> => {
if (name === "resolve") {
return resolvePromise(ctx, args[0])
}
if (name === "reject") {
return ctx.pending.create(Effect.fail(new Throw(args[0])))
}
if (name === "withResolvers") {
return Effect.map(promiseCapability(ctx), (made) =>
record(ctx.builtins.Object, { promise: made.promise, resolve: made.resolve, reject: made.reject }),
)
}
if (name === "try") {
if (typeofValue(args[0]) !== "function") {
throw typeError(`Promise.try expects a function, received ${describeValue(args[0])}.`)
}
return Effect.flatMap(Effect.exit(ctx.call(args[0], undefined, args.slice(1))), (called) => {
if (Exit.isSuccess(called)) return resolvePromise(ctx, called.value)
if (Cause.hasInterruptsOnly(called.cause)) return Effect.failCause(called.cause)
return ctx.pending.create(Effect.fail(Cause.squash(called.cause)))
})
}
return ctx.pending.create(
Effect.gen(function* () {
const cursor = yield* ctx.iterate(args[0])
if (cursor === undefined) throw typeError(`Promise.${name} expects an array or other synchronous iterable.`)
if (cursor === undefined) {
throw typeError(`Promise.${name} expects a synchronous iterable, received ${describeValue(args[0])}.`)
}
const items: Array<PromiseObj> = []
while (true) {
const step = yield* cursor.next
@@ -175,7 +192,7 @@ const invokePromiseMethod = <R>(
)
}
if (name === "allSettled") {
const outcomes: Array<unknown> = []
const outcomes: Array<Value> = []
for (const item of items) {
const exit = yield* ctx.pending.await(item)
if (Exit.isSuccess(exit)) {
@@ -223,8 +240,8 @@ const invokePromiseMethod = <R>(
const instanceMethod = <R>(
ctx: Interpreter<R>,
name: "then" | "catch" | "finally",
thisValue: unknown,
args: Array<unknown>,
thisValue: Value,
args: Array<Value>,
): Effect.Effect<PromiseObj, unknown, R> => {
const method = `Promise.prototype.${name}`
const promise = receiver(PromiseObj, thisValue, method)
@@ -237,23 +254,30 @@ const instanceMethod = <R>(
return chainReaction(ctx, promise, onFulfilled, onRejected, method)
}
const constructPromise = <R>(ctx: Interpreter<R>, executor: unknown): Effect.Effect<PromiseObj, unknown, R> => {
if (!(executor instanceof Fn)) {
throw typeError("new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).")
}
return Effect.gen(function* () {
const deferred = Deferred.makeUnsafe<unknown, unknown>()
/** NewPromiseCapability: a pending promise with the resolve/reject callables that settle it exactly once. */
const promiseCapability = <R>(ctx: Interpreter<R>) =>
Effect.gen(function* () {
const deferred = Deferred.makeUnsafe<Value, unknown>()
const promise = yield* ctx.pending.createWithSelf((self) =>
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)),
)
const resolve = capability(ctx, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)))
const reject = capability(ctx, "reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(value))))
const executed = yield* Effect.exit(ctx.call(executor, undefined, [resolve, reject]))
return { promise, resolve, reject, deferred }
})
const constructPromise = <R>(ctx: Interpreter<R>, executor: Value): Effect.Effect<PromiseObj, unknown, R> => {
if (!(executor instanceof Fn)) {
throw typeError("new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).")
}
return Effect.gen(function* () {
const made = yield* promiseCapability(ctx)
const executed = yield* Effect.exit(ctx.call(executor, undefined, [made.resolve, made.reject]))
if (!Exit.isSuccess(executed)) {
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
Deferred.doneUnsafe(made.deferred, Exit.fail(Cause.squash(executed.cause)))
}
return promise
return made.promise
})
}
@@ -262,10 +286,10 @@ const settleAfterTurn = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A
Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit))
class PromiseAnyFulfilled {
constructor(readonly value: unknown) {}
constructor(readonly value: Value) {}
}
const reactionHandler = (value: unknown, method: string): Callable | undefined => {
const reactionHandler = (value: Value, method: string): Callable | undefined => {
if (isSupportedCallback(value)) return value
if (typeofValue(value) === "function") {
throw typeError(
@@ -279,7 +303,7 @@ const reactionHandler = (value: unknown, method: string): Callable | undefined =
const reactionExit = <R>(
ctx: Interpreter<R>,
source: PromiseObj,
): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> =>
): Effect.Effect<Exit.Exit<Value, unknown>, unknown, R> =>
Effect.gen(function* () {
const exit = yield* ctx.pending.await(source)
if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
+21 -60
View File
@@ -1,47 +1,18 @@
import { ToolReference } from "../tool-runtime.js"
import { invalidData } from "./model.js"
import {
Callable,
getOwn,
isWrapper,
ownKeys,
Arr,
Bytes,
DateObj,
GeneratorObj,
IteratorObj,
MapObj,
Obj,
PromiseObj,
RegExpObj,
SetObj,
URLObj,
URLSearchParamsObj,
HeadersObj,
} from "./objects.js"
import { Callable, getOwn, isRuntimeReference, Obj, Opaque, ownKeys, type Value } from "./objects.js"
/** Values that cannot cross the data boundary. */
export const isRuntimeReference = (value: unknown): boolean =>
value instanceof Callable ||
value instanceof GeneratorObj ||
value instanceof IteratorObj ||
value instanceof ToolReference ||
value instanceof PromiseObj ||
isWrapper(value)
function* childValues(value: object): Generator {
if (!(value instanceof Obj)) return
for (const key of ownKeys(value)) yield getOwn(value, key)
}
/** Interpreter machinery that is never data, unlike a Date or Map, which cross some boundaries as copies. */
export const isOpaque = (value: Value): boolean => value instanceof Opaque || value instanceof ToolReference
// Depth-first search over a value tree. `match` stops the walk; `skip` prunes a subtree without matching it.
const find = (
value: unknown,
match: (current: unknown) => boolean,
skip: (current: unknown) => boolean,
value: Value,
match: (current: Value) => boolean,
skip: (current: Value) => boolean,
seen: Set<object>,
): boolean => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const pending: Array<Iterator<Value>> = [[value].values()]
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
@@ -50,25 +21,28 @@ const find = (
}
const current = next.value
if (match(current)) return true
if (current === null || typeof current !== "object" || skip(current) || seen.has(current)) continue
if (!(current instanceof Obj) || skip(current) || seen.has(current)) continue
seen.add(current)
pending.push(childValues(current))
pending.push(
ownKeys(current)
.map((key) => getOwn(current, key))
.values(),
)
}
return false
}
const never = () => false
export const containsRuntimeReference = (value: unknown): boolean => find(value, isRuntimeReference, never, new Set())
export const containsRuntimeReference = (value: Value): boolean => find(value, isRuntimeReference, never, new Set())
// Wrapper values are data here, not opaque interpreter references.
export const containsOpaqueReference = (value: unknown): boolean =>
find(value, (current) => !isWrapper(current) && isRuntimeReference(current), isWrapper, new Set())
export const containsOpaqueReference = (value: Value): boolean =>
find(value, isOpaque, (current) => isRuntimeReference(current) && !isOpaque(current), new Set())
// Reject cycles before mutation so later boundary walks remain safe.
export const rejectCircularInsertion = (
container: object,
value: unknown,
container: Obj,
value: Value,
label: string,
seen = new Set<object>(),
): void => {
@@ -77,27 +51,14 @@ export const rejectCircularInsertion = (
}
}
export const describeValue = (value: unknown): string => {
export const describeValue = (value: Value): string => {
if (value === null || value === undefined) return String(value)
if (value instanceof Arr) return "an array"
if (value instanceof PromiseObj) return "an un-awaited Promise"
if (value instanceof Obj) return value.describe
if (value instanceof ToolReference) return "a tool reference"
if (value instanceof DateObj) return "a Date"
if (value instanceof RegExpObj) return "a RegExp"
if (value instanceof MapObj) return "a Map"
if (value instanceof SetObj) return "a Set"
if (value instanceof URLObj) return "a URL"
if (value instanceof URLSearchParamsObj) return "a URLSearchParams"
if (value instanceof HeadersObj) return "a Headers"
if (value instanceof Bytes) return "a Uint8Array"
if (value instanceof GeneratorObj) return "a generator"
if (value instanceof IteratorObj) return "an iterator"
if (isRuntimeReference(value)) return "a function"
if (typeof value === "object") return "a data object"
return `a ${typeof value}`
}
export const typeofValue = (value: unknown): string => {
export const typeofValue = (value: Value): string => {
if (value instanceof Callable) return "function"
if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
return typeof value
+5 -4
View File
@@ -1,4 +1,5 @@
import { type AstNode, type Binding, referenceError, typeError } from "./model.js"
import type { Value } from "./objects.js"
export class ScopeStack {
private readonly scopes: Array<Map<string, Binding>>
@@ -15,7 +16,7 @@ export class ScopeStack {
scope.set(name, { mutable, value: undefined, initialized: false })
}
initialize(name: string, value: unknown, node: AstNode): void {
initialize(name: string, value: Value, node: AstNode): void {
const binding = this.current().get(name)
if (!binding || binding.initialized !== false) {
throw typeError(`Identifier '${name}' has not been reserved for initialization.`, node)
@@ -24,7 +25,7 @@ export class ScopeStack {
binding.initialized = true
}
declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
declare(name: string, value: Value, mutable: boolean, node: AstNode): void {
const scope = this.current()
if (scope.has(name)) {
throw typeError(`Identifier '${name}' has already been declared.`, node)
@@ -32,7 +33,7 @@ export class ScopeStack {
scope.set(name, { mutable, value, initialized: true })
}
get(name: string, node: AstNode): unknown {
get(name: string, node: AstNode): Value {
const binding = this.resolve(name)
if (!binding) {
@@ -46,7 +47,7 @@ export class ScopeStack {
return binding.value
}
set(name: string, value: unknown, node: AstNode): unknown {
set(name: string, value: Value, node: AstNode): Value {
const binding = this.resolve(name)
if (!binding) {
+67 -84
View File
@@ -2,26 +2,36 @@ import { Effect } from "effect"
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
import { checkArrayLength, checkStringLength, MAX_ARRAY_LENGTH } from "../interpreter/limits.js"
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
import { define, get, hidden, Arr, GeneratorObj, IteratorObj, Obj } from "../interpreter/objects.js"
import {
define,
get,
hidden,
Arr,
GeneratorObj,
hostIterator,
Obj,
PromiseObj,
coerceToInteger,
coerceToNumber,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
import { applyCollectionCallback, invoke, preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { compareText } from "../tool-runtime.js"
import { coerceToNumber, coerceToString } from "./value.js"
const arrayLikeSource = (source: unknown): { readonly length: number; readonly source: Obj } => {
if (source instanceof Obj && typeof get(source, "length") === "number") {
const length = get(source, "length") as number
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
checkArrayLength(normalized)
return { length: normalized, source }
const arrayLikeSource = (source: Value): { readonly length: number; readonly source: Obj } => {
// JS would treat a promise as an empty array-like; that would hide a missing `await`.
if (source instanceof Obj && !(source instanceof PromiseObj)) {
const length = Math.max(0, coerceToInteger(get(source, "length")))
checkArrayLength(length)
return { length, source }
}
throw invalidData(
`Array.from expects an array, string, Map, Set, or array-like value, received ${describeValue(source)}.`,
)
throw invalidData(`Array.from expects an iterable or array-like value, received ${describeValue(source)}.`)
}
const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
const source = args[0]
const proto = ctx.builtins.Array
const apply =
@@ -30,22 +40,26 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<
const cursor = yield* ctx.iterate(source)
if (cursor === undefined) {
if (source instanceof GeneratorObj) {
throw typeError("Array.from expects a synchronous iterable or array-like value.")
throw typeError(
`Array.from expects a synchronous iterable or array-like value, received ${describeValue(source)}.`,
)
}
const arrayLike = arrayLikeSource(source)
const values: Array<unknown> = []
const values: Array<Value> = []
for (let index = 0; index < arrayLike.length; index += 1) {
const item = get(arrayLike.source, index)
values.push(apply === undefined ? item : yield* apply([item, index]))
}
return new Arr(proto, values)
}
const values: Array<unknown> = []
const values: Array<Value> = []
let index = 0
while (true) {
const step = yield* cursor.next
if (step.done) return new Arr(proto, values)
values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
values.push(
apply === undefined ? step.value : yield* preserveConsumerError(cursor.close, apply([step.value, index])),
)
index += 1
}
})
@@ -53,21 +67,21 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<
export const sortArray = <R>(
ctx: Interpreter<R>,
target: Array<unknown>,
comparator: unknown,
target: Array<Value>,
comparator: Value,
name: string,
): Effect.Effect<Array<unknown>, unknown, R> => {
): Effect.Effect<Array<Value>, unknown, R> => {
if (comparator === undefined) {
return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b))))
}
const apply = applyCollectionCallback(ctx, comparator, name)
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
const mergeSort = (items: Array<Value>): Effect.Effect<Array<Value>, unknown, R> => {
if (items.length <= 1) return Effect.succeed(items)
const midpoint = Math.floor(items.length / 2)
return Effect.gen(function* () {
const left = yield* mergeSort(items.slice(0, midpoint))
const right = yield* mergeSort(items.slice(midpoint))
const merged: Array<unknown> = []
const merged: Array<Value> = []
let leftIndex = 0
let rightIndex = 0
while (leftIndex < left.length && rightIndex < right.length) {
@@ -88,8 +102,8 @@ export const sortArray = <R>(
export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const proto = builtins.Array
const wrap = (items: Array<unknown>) => new Arr(proto, items)
const construct = (args: Array<unknown>, into: Obj): Arr => {
const wrap = (items: Array<Value>) => new Arr(proto, items)
const construct = (args: Array<Value>, into: Obj): Arr => {
if (args.length !== 1) return new Arr(into, [...args])
const first = args[0]
if (typeof first !== "number") return new Arr(into, [first])
@@ -109,24 +123,18 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
["from", 1, (_, args) => arrayFrom(ctx, args)],
])
const self = (thisValue: unknown, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number") {
throw typeError(`Array.${name} expects ${label} to be a number.`)
}
return value
}
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
// Callback methods fix the iteration length while reading existing elements live.
const iterate = (
name: string,
length: number,
body: (
target: Array<unknown>,
target: Array<Value>,
receiver: Arr,
apply: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
args: Array<unknown>,
) => Effect.Effect<unknown, unknown, R>,
apply: (args: Array<Value>) => Effect.Effect<Value, unknown, R>,
args: Array<Value>,
) => Effect.Effect<Value, unknown, R>,
): Method => [
name,
length,
@@ -141,13 +149,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"join",
1,
(thisValue, args) => {
const target = self(thisValue, "join").items
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw typeError("Array.join expects zero arguments or one string separator.")
}
const joined = target
.map((item) => coerceToString(item ?? ""))
.join(args.length === 0 ? "," : (args[0] as string))
const joined = self(thisValue, "join")
.items.map((item) => coerceToString(item ?? ""))
.join(args[0] === undefined ? "," : coerceToString(args[0]))
checkStringLength(joined.length)
return joined
},
@@ -164,40 +168,23 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"includes",
1,
(thisValue, args) => {
const target = self(thisValue, "includes").items
if (args.length === 0 || args.length > 2) {
throw typeError("Array.includes expects a value and optional start index.")
}
return target.includes(args[0], optNumber("includes", args[1], "start index"))
return self(thisValue, "includes").items.includes(args[0], optNumber(args[1]))
},
],
[
"indexOf",
1,
(thisValue, args) =>
self(thisValue, "indexOf").items.indexOf(args[0], optNumber("indexOf", args[1], "start index")),
],
["indexOf", 1, (thisValue, args) => self(thisValue, "indexOf").items.indexOf(args[0], optNumber(args[1]))],
[
"lastIndexOf",
1,
(thisValue, args) => {
const target = self(thisValue, "lastIndexOf").items
return args[1] === undefined
? target.lastIndexOf(args[0])
: target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index"))
return args[1] === undefined ? target.lastIndexOf(args[0]) : target.lastIndexOf(args[0], optNumber(args[1]))
},
],
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber("at", args[0], "index") ?? 0)],
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber(args[0]) ?? 0)],
[
"slice",
2,
(thisValue, args) =>
wrap(
self(thisValue, "slice").items.slice(
optNumber("slice", args[0], "start"),
optNumber("slice", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "slice").items.slice(optNumber(args[0]), optNumber(args[1]))),
],
[
"concat",
@@ -214,9 +201,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"flat",
0,
(thisValue, args) => {
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
const flatten = (items: Array<Value>, depth: number): Array<Value> =>
items.flatMap((item) => (item instanceof Arr && depth > 0 ? flatten(item.items, depth - 1) : [item]))
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
const flattened = flatten(self(thisValue, "flat").items, optNumber(args[0]) ?? 1)
checkArrayLength(flattened.length)
return wrap(flattened)
},
@@ -262,7 +249,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
2,
(thisValue, args) => {
const target = self(thisValue, "with").items
const index = optNumber("with", args[0], "index") ?? 0
const index = optNumber(args[0]) ?? 0
const resolved = index < 0 ? target.length + index : index
if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.")
const copied = [...target]
@@ -297,9 +284,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const target = self(thisValue, "splice")
if (args.length === 0) return wrap(target.items.splice(0, 0))
const start = optNumber("splice", args[0], "start") ?? 0
const start = optNumber(args[0]) ?? 0
if (args.length === 1) return wrap(target.items.splice(start))
const deleteCount = optNumber("splice", args[1], "delete count") ?? 0
const deleteCount = optNumber(args[1]) ?? 0
const inserted = args.slice(2)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result")
return wrap(target.items.splice(start, deleteCount, ...inserted))
@@ -311,9 +298,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const copied = [...self(thisValue, "toSpliced").items]
if (args.length === 0) return wrap(copied)
const start = optNumber("toSpliced", args[0], "start") ?? 0
const start = optNumber(args[0]) ?? 0
if (args.length === 1) copied.splice(start)
else copied.splice(start, optNumber("toSpliced", args[1], "delete count") ?? 0, ...args.slice(2))
else copied.splice(start, optNumber(args[1]) ?? 0, ...args.slice(2))
return wrap(copied)
},
],
@@ -323,7 +310,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const target = self(thisValue, "fill")
rejectCircularInsertion(target, args[0], "Array.fill result")
target.items.fill(args[0], optNumber("fill", args[1], "start"), optNumber("fill", args[2], "end"))
target.items.fill(args[0], optNumber(args[1]), optNumber(args[2]))
return target
},
],
@@ -332,11 +319,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
2,
(thisValue, args) => {
const target = self(thisValue, "copyWithin")
target.items.copyWithin(
optNumber("copyWithin", args[0], "target index") ?? 0,
optNumber("copyWithin", args[1], "start") ?? 0,
optNumber("copyWithin", args[2], "end"),
)
target.items.copyWithin(optNumber(args[0]) ?? 0, optNumber(args[1]) ?? 0, optNumber(args[2]))
return target
},
],
@@ -353,14 +336,14 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(parts) => parts.join(","),
),
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").items.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").items.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").items.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").items.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.items.entries()
.map(([index, item]) => wrap([index, item])),
@@ -369,7 +352,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
iterate("map", 1, (target, receiver, apply) =>
Effect.gen(function* () {
const length = target.length
const values: Array<unknown> = []
const values: Array<Value> = []
values.length = length
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
@@ -381,7 +364,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
iterate("flatMap", 1, (target, receiver, apply) =>
Effect.gen(function* () {
const length = target.length
const values: Array<unknown> = []
const values: Array<Value> = []
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const mapped = yield* apply([target[index], index, receiver])
@@ -394,7 +377,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
iterate("filter", 1, (target, receiver, apply) =>
Effect.gen(function* () {
const length = target.length
const values: Array<unknown> = []
const values: Array<Value> = []
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const item = target[index]
+34 -51
View File
@@ -2,13 +2,25 @@ import { Effect } from "effect"
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
import { IteratorSymbol, rangeError, syntaxError, typeError } from "../interpreter/model.js"
import { define, defineAccessor, get, hidden, Arr, Bytes, IteratorObj, Obj } from "../interpreter/objects.js"
import {
define,
defineAccessor,
get,
hidden,
Arr,
Bytes,
hostIterator,
Obj,
coerceToInteger,
coerceToNumber,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToNumber, coerceToString } from "./value.js"
/** The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. */
const collectBytes = <R>(ctx: Interpreter<R>, source: unknown, name: string): Effect.Effect<Uint8Array, unknown, R> => {
const collectBytes = <R>(ctx: Interpreter<R>, source: Value, name: string): Effect.Effect<Uint8Array, unknown, R> => {
if (source instanceof Bytes) return Effect.succeed(new Uint8Array(source.bytes))
return Effect.gen(function* () {
const cursor = yield* ctx.iterate(source)
@@ -27,7 +39,7 @@ const collectBytes = <R>(ctx: Interpreter<R>, source: unknown, name: string): Ef
})
}
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<Value>, proto: Obj) => {
const source = args[0]
if (source !== null && typeof source === "object") {
return Effect.map(collectBytes(ctx, source, "new Uint8Array(...)"), (bytes) => new Bytes(proto, bytes))
@@ -48,7 +60,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
call: requiresNew("Uint8Array"),
construct: (args, newTarget) => constructBytes(ctx, args, prototypeFrom(newTarget, proto)),
})
const decode = (name: string, args: Array<unknown>, from: (text: string) => Uint8Array) => {
const decode = (name: string, args: Array<Value>, from: (text: string) => Uint8Array) => {
if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`)
try {
return wrap(from(args[0]))
@@ -63,45 +75,29 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
["fromHex", 1, (_, args) => decode("fromHex", args, (text) => Uint8Array.fromHex(text))],
])
const self = (thisValue: unknown, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number") throw typeError(`Uint8Array.${name} expects ${label} to be a number.`)
return value
}
const wrapAll = (items: Array<unknown>) => new Arr(builtins.Array, items)
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
const wrapAll = (items: Array<Value>) => new Arr(builtins.Array, items)
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
methods(builtins, proto, [
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber("at", args[0], "index") ?? 0)],
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber(args[0]) ?? 0)],
[
"slice",
2,
(thisValue, args) =>
wrap(
self(thisValue, "slice").bytes.slice(
optNumber("slice", args[0], "start"),
optNumber("slice", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "slice").bytes.slice(optNumber(args[0]), optNumber(args[1]))),
],
// A view on the same bytes, as in JS: writes through one are visible through the other.
[
"subarray",
2,
(thisValue, args) =>
wrap(
self(thisValue, "subarray").bytes.subarray(
optNumber("subarray", args[0], "start"),
optNumber("subarray", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "subarray").bytes.subarray(optNumber(args[0]), optNumber(args[1]))),
],
[
"set",
1,
(thisValue, args) => {
const target = self(thisValue, "set")
const offset = optNumber("set", args[1], "offset") ?? 0
const offset = optNumber(args[1]) ?? 0
return Effect.map(collectBytes(ctx, args[0], "Uint8Array.set"), (source) => {
if (!Number.isInteger(offset) || offset < 0 || source.length + offset > target.bytes.length) {
throw rangeError("Uint8Array.set: the source does not fit at that offset.")
@@ -116,11 +112,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
1,
(thisValue, args) => {
const target = self(thisValue, "fill")
target.bytes.fill(
coerceToNumber(args[0]),
optNumber("fill", args[1], "start"),
optNumber("fill", args[2], "end"),
)
target.bytes.fill(coerceToNumber(args[0]), optNumber(args[1]), optNumber(args[2]))
return target
},
],
@@ -136,8 +128,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
[
"indexOf",
1,
(thisValue, args) =>
self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber("indexOf", args[1], "start index")),
(thisValue, args) => self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber(args[1])),
],
[
"lastIndexOf",
@@ -146,26 +137,19 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "lastIndexOf").bytes
return args[1] === undefined
? target.lastIndexOf(coerceToNumber(args[0]))
: target.lastIndexOf(coerceToNumber(args[0]), optNumber("lastIndexOf", args[1], "start index"))
: target.lastIndexOf(coerceToNumber(args[0]), optNumber(args[1]))
},
],
[
"includes",
1,
(thisValue, args) =>
self(thisValue, "includes").bytes.includes(
coerceToNumber(args[0]),
optNumber("includes", args[1], "start index"),
),
(thisValue, args) => self(thisValue, "includes").bytes.includes(coerceToNumber(args[0]), optNumber(args[1])),
],
[
"join",
1,
(thisValue, args) => {
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw typeError("Uint8Array.join expects zero arguments or one string separator.")
}
const joined = self(thisValue, "join").bytes.join(args.length === 0 ? "," : (args[0] as string))
const joined = self(thisValue, "join").bytes.join(args[0] === undefined ? "," : coerceToString(args[0]))
checkStringLength(joined.length)
return joined
},
@@ -173,14 +157,14 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
["toString", 0, (thisValue) => self(thisValue, "toString").bytes.join(",")],
["toBase64", 0, (thisValue) => self(thisValue, "toBase64").bytes.toBase64()],
["toHex", 0, (thisValue) => self(thisValue, "toHex").bytes.toHex()],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").bytes.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").bytes.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").bytes.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").bytes.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.bytes.entries()
.map(([index, byte]) => wrapAll([index, byte])),
@@ -222,8 +206,7 @@ const utf8Labels = new Set(["unicode-1-1-utf-8", "unicode11utf8", "unicode20utf8
export const textDecoderGlobal = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const proto = builtins.TextDecoder
const self = (thisValue: unknown, name: string) =>
receiver(TextDecoderObj, thisValue, `TextDecoder.prototype.${name}`)
const self = (thisValue: Value, name: string) => receiver(TextDecoderObj, thisValue, `TextDecoder.prototype.${name}`)
defineAccessor(proto, "encoding", (thisValue) => self(thisValue, "encoding").decoder.encoding)
defineAccessor(proto, "fatal", (thisValue) => self(thisValue, "fatal").decoder.fatal)
defineAccessor(proto, "ignoreBOM", (thisValue) => self(thisValue, "ignoreBOM").decoder.ignoreBOM)
+44 -48
View File
@@ -7,15 +7,17 @@ import {
get,
getOwn,
hidden,
isWrapper,
Arr,
IteratorObj,
coerceToString,
hostCursor,
hostIterator,
MapObj,
Obj,
PromiseObj,
SetObj,
type Value,
} from "../interpreter/objects.js"
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
import { describeValue, isOpaque } from "../interpreter/references.js"
import {
applyCollectionCallback,
isSupportedCallback,
@@ -25,9 +27,9 @@ import {
} from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
const coerceGroupByPropertyKey = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<string, unknown, R> => {
if (value instanceof PromiseObj) return Effect.succeed("[object Promise]")
if (!isWrapper(value) && isRuntimeReference(value)) {
const coerceGroupByPropertyKey = <R>(ctx: Interpreter<R>, value: Value): Effect.Effect<string, unknown, R> => {
if (value instanceof PromiseObj) return Effect.succeed(coerceToString(value))
if (isOpaque(value)) {
throw invalidData(`Object.groupBy callback must return a data value, received ${describeValue(value)}.`)
}
return toPrimitiveString(ctx, value)
@@ -54,7 +56,7 @@ export const groupBy = <R>(ctx: Interpreter<R>, namespace: "Map" | "Object") =>
const step = yield* cursor.next
if (step.done) return result
const item = step.value
const key = yield* preserveConsumerError(cursor, apply([item, index]))
const key = yield* preserveConsumerError(cursor.close, apply([item, index]))
const group = result.map.get(key)
if (group === undefined) result.map.set(key, new Arr(builtins.Array, [item]))
else (group as Arr).items.push(item)
@@ -70,7 +72,7 @@ export const groupBy = <R>(ctx: Interpreter<R>, namespace: "Map" | "Object") =>
if (step.done) return result
const item = step.value
const key = yield* preserveConsumerError(
cursor,
cursor.close,
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(ctx, value)),
)
const group = getOwn(result, key)
@@ -81,19 +83,19 @@ export const groupBy = <R>(ctx: Interpreter<R>, namespace: "Map" | "Object") =>
})
})
const constructMap = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
const constructMap = <R>(ctx: Interpreter<R>, init: Value, proto: Obj) => {
const target = new MapObj(proto)
if (init === undefined || init === null) return Effect.succeed(target)
return Effect.gen(function* () {
const cursor = yield* ctx.iterate(init)
if (cursor === undefined) {
throw typeError("new Map(...) expects an iterable of [key, value] pairs or no argument.")
throw typeError(`new Map(...) expects an iterable of [key, value] pairs, received ${describeValue(init)}.`)
}
while (true) {
const step = yield* cursor.next
if (step.done) return target
yield* preserveConsumerError(
cursor,
cursor.close,
Effect.sync(() => {
if (!(step.value instanceof Obj)) {
throw typeError("new Map(...) expects [key, value] pairs as entry objects.")
@@ -105,13 +107,13 @@ const constructMap = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
})
}
const constructSet = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
const constructSet = <R>(ctx: Interpreter<R>, init: Value, proto: Obj) => {
const target = new SetObj(proto)
if (init === undefined || init === null) return Effect.succeed(target)
return Effect.gen(function* () {
const cursor = yield* ctx.iterate(init)
if (cursor === undefined) {
throw typeError("new Set(...) expects a synchronous iterable or no argument.")
throw typeError(`new Set(...) expects a synchronous iterable, received ${describeValue(init)}.`)
}
while (true) {
const step = yield* cursor.next
@@ -130,8 +132,7 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
construct: (args, newTarget) => constructMap(ctx, args[0], prototypeFrom(newTarget, proto)),
})
define(map, "groupBy", groupBy(ctx, "Map"), hidden)
const self = (thisValue: unknown, name: string) => receiver(MapObj, thisValue, `Map.prototype.${name}`)
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
const self = (thisValue: Value, name: string) => receiver(MapObj, thisValue, `Map.prototype.${name}`)
defineAccessor(proto, "size", (thisValue) => receiver(MapObj, thisValue, "Map.prototype.size").map.size)
methods(builtins, proto, [
["get", 1, (thisValue, args) => self(thisValue, "get").map.get(args[0])],
@@ -177,19 +178,9 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").map.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").map.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
self(thisValue, "entries")
.map.entries()
.map(([key, item]) => wrap([key, item])),
),
],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").map.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").map.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
[
"forEach",
1,
@@ -197,7 +188,7 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "forEach")
const apply = applyCollectionCallback(ctx, args[0], "Map.forEach")
return Effect.gen(function* () {
for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
for (const [key, item] of target.map.entries()) yield* apply([item, key, target])
return undefined
})
},
@@ -209,26 +200,26 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
type SetRecord<R> = {
readonly size: number
readonly has: (item: unknown) => Effect.Effect<boolean, unknown, R>
readonly keys: () => Effect.Effect<Iterable<unknown>, unknown, R>
readonly has: (item: Value) => Effect.Effect<boolean, unknown, R>
readonly keys: () => Effect.Effect<Iterable<Value>, unknown, R>
}
const loadSetRecord = <R>(
ctx: Interpreter<R>,
source: unknown,
source: Value,
name: string,
): Effect.Effect<SetRecord<R>, unknown, R> => {
if (source instanceof SetObj) {
return Effect.succeed({
size: source.set.size,
has: (item: unknown) => Effect.succeed(source.set.has(item)),
has: (item: Value) => Effect.succeed(source.set.has(item)),
keys: () => Effect.succeed(source.set.values()),
})
}
if (source instanceof MapObj) {
return Effect.succeed({
size: source.map.size,
has: (item: unknown) => Effect.succeed(source.map.has(item)),
has: (item: Value) => Effect.succeed(source.map.has(item)),
keys: () => Effect.succeed(source.map.keys()),
})
}
@@ -247,12 +238,17 @@ const loadSetRecord = <R>(
}
return {
size: Math.max(Math.trunc(size), 0),
has: (item: unknown) => Effect.map(ctx.call(has, source, [item]), Boolean),
has: (item: Value) => Effect.map(ctx.call(has, source, [item]), Boolean),
keys: () =>
Effect.flatMap(ctx.call(keys, source, []), (result): Effect.Effect<Iterable<unknown>> => {
if (result instanceof IteratorObj) return Effect.succeed(result.iterator)
if (result instanceof Arr) return Effect.succeed(result.items)
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
Effect.gen(function* () {
const result = yield* ctx.call(keys, source, [])
const cursor = result instanceof Arr ? hostCursor(result.items.values()) : ctx.iterateDirect(result)
const items: Array<Value> = []
while (true) {
const step = yield* cursor.next
if (step.done) return items
items.push(step.value)
}
}),
}
})
@@ -262,8 +258,8 @@ const setOperation = <R>(
ctx: Interpreter<R>,
target: SetObj,
name: string,
source: unknown,
): Effect.Effect<unknown, unknown, R> =>
source: Value,
): Effect.Effect<Value, unknown, R> =>
Effect.gen(function* () {
const other = yield* loadSetRecord(ctx, source, name)
const copy = () => {
@@ -342,8 +338,8 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
call: requiresNew("Set"),
construct: (args, newTarget) => constructSet(ctx, args[0], prototypeFrom(newTarget, proto)),
})
const self = (thisValue: unknown, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`)
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
const self = (thisValue: Value, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`)
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
const operation = (name: string): Method => [
name,
1,
@@ -370,14 +366,14 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").set.values())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").set.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").set.values())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").set.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.set.values()
.map((item) => wrap([item, item])),
@@ -390,7 +386,7 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "forEach")
const apply = applyCollectionCallback(ctx, args[0], "Set.forEach")
return Effect.gen(function* () {
for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
for (const item of target.set.values()) yield* apply([item, item, target])
return undefined
})
},
+13 -60
View File
@@ -1,22 +1,8 @@
import { type Method, methods } from "../interpreter/native.js"
import {
entries,
get,
Arr,
Bytes,
DateObj,
MapObj,
Obj,
PromiseObj,
RegExpObj,
SetObj,
URLObj,
URLSearchParamsObj,
HeadersObj,
} from "../interpreter/objects.js"
import { containsOpaqueReference, isRuntimeReference } from "../interpreter/references.js"
import { entries, get, Arr, Obj, type Value } from "../interpreter/objects.js"
import { ToolReference } from "../tool-runtime.js"
import { containsOpaqueReference } from "../interpreter/references.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToString } from "./value.js"
const consoleMethods = ["log", "info", "debug", "warn", "error", "dir", "table"]
@@ -43,7 +29,7 @@ export const consoleGlobal = <R>(ctx: Interpreter<R>) => {
const MAX_CONSOLE_DEPTH = 32
const formatConsoleMessage = (name: string, args: Array<unknown>): string => {
const formatConsoleMessage = (name: string, args: Array<Value>): string => {
if (name === "dir") return args.length === 0 ? "undefined" : formatValue(args[0])
if (name === "table") return formatConsoleTable(args[0], args[1])
const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
@@ -51,60 +37,27 @@ const formatConsoleMessage = (name: string, args: Array<unknown>): string => {
}
/** One value as `console.log` shows it. */
export const formatValue = (value: unknown): string => {
export const formatValue = (value: Value): string => {
if (value === undefined) return "undefined"
if (typeof value === "string") return value
return formatConsoleValue(value, new Set(), 0)
}
const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): string => {
const formatConsoleValue = (value: Value, seen: Set<object>, depth: number): string => {
if (value === null || value === undefined) return "null"
if (typeof value === "string") return JSON.stringify(value)
if (typeof value === "number" || typeof value === "boolean") return String(value)
if (typeof value !== "object") return String(value)
if (value instanceof PromiseObj) return "[Promise (await it to get its value)]"
if (value instanceof DateObj) return coerceToString(value)
if (value instanceof RegExpObj) return coerceToString(value)
if (value instanceof URLObj) return coerceToString(value)
if (value instanceof URLSearchParamsObj) return coerceToString(value)
if (value instanceof HeadersObj) return `Headers ${JSON.stringify(Object.fromEntries(value.headers))}`
if (value instanceof Bytes) return `Uint8Array(${value.bytes.length}) [${value.bytes.join(",")}]`
if (!(value instanceof Obj)) return value instanceof ToolReference ? "[opaque reference]" : String(value)
if (depth > MAX_CONSOLE_DEPTH) return "..."
if (seen.has(value)) return "[Circular]"
if (value instanceof MapObj) {
seen.add(value)
try {
const items = Array.from(value.map.entries(), ([key, item]) => `[${formatItems([key, item], seen, depth + 1)}]`)
return `Map(${value.map.size}) [${items.join(",")}]`
} finally {
seen.delete(value)
}
}
if (value instanceof SetObj) {
seen.add(value)
try {
return `Set(${value.set.size}) [${formatItems([...value.set.values()], seen, depth + 1)}]`
} finally {
seen.delete(value)
}
}
if (isRuntimeReference(value)) return "[opaque reference]"
seen.add(value)
try {
if (value instanceof Arr) return `[${formatItems(value.items, seen, depth + 1)}]`
if (!(value instanceof Obj)) return "[object Object]"
return `{${entries(value)
.map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
.join(",")}}`
return value.inspect((item) => formatConsoleValue(item, seen, depth + 1))
} finally {
seen.delete(value)
}
}
const formatItems = (items: Array<unknown>, seen: Set<object>, depth: number): string =>
items.map((item) => formatConsoleValue(item, seen, depth)).join(",")
const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => {
const formatConsoleTable = (value: Value, columnsArgument: Value): string => {
if (value === undefined) return "undefined"
if (containsOpaqueReference(value)) return "[opaque reference]"
const columns = columnsArgument instanceof Arr ? columnsArgument.items.map(String) : undefined
@@ -118,9 +71,9 @@ const formatConsoleTable = (value: unknown, columnsArgument: unknown): string =>
}
const consoleTableRows = (
data: unknown,
data: Value,
columns: ReadonlyArray<string> | undefined,
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
): Array<{ readonly index: string; readonly values: Record<string, Value> }> => {
if (data instanceof Arr) {
return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
}
@@ -130,7 +83,7 @@ const consoleTableRows = (
return [{ index: "0", values: { Value: data } }]
}
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
const consoleTableValues = (value: Value, columns: ReadonlyArray<string> | undefined): Record<string, Value> => {
if (value instanceof Obj && !(value instanceof Arr)) {
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, get(value, column)]))
return Object.fromEntries(entries(value))
@@ -138,7 +91,7 @@ const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | und
return { Value: value }
}
const formatConsoleTableCell = (value: unknown): string => {
const formatConsoleTableCell = (value: Value): string => {
if (value === undefined) return ""
if (typeof value === "string") return value
return formatConsoleValue(value, new Set(), 0)
+3 -4
View File
@@ -1,12 +1,11 @@
import { Effect } from "effect"
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
import { rangeError } from "../interpreter/model.js"
import { DateObj, Obj } from "../interpreter/objects.js"
import { DateObj, Obj, coerceToNumber, coerceToString, type Value } from "../interpreter/objects.js"
import { toPrimitive, toPrimitiveNumber } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToNumber, coerceToString } from "./value.js"
const constructDate = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
const constructDate = <R>(ctx: Interpreter<R>, args: Array<Value>, proto: Obj) => {
if (args.length === 0) return Effect.succeed(new DateObj(proto, Date.now()))
if (args.length === 1) {
const arg = args[0]
@@ -83,7 +82,7 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
["UTC", 7, (_, args) => Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))],
])
const self = (thisValue: unknown, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
const self = (thisValue: Value, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
const iso = (value: DateObj) => {
if (!Number.isFinite(value.time)) throw rangeError("Invalid time value.")
return new Date(value.time).toISOString()
+27 -31
View File
@@ -1,11 +1,21 @@
import { Effect } from "effect"
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
import { IteratorSymbol, typeError } from "../interpreter/model.js"
import { define, entries, get, hidden, Arr, HeadersObj, IteratorObj, Obj } from "../interpreter/objects.js"
import {
define,
entries,
get,
hidden,
Arr,
HeadersObj,
hostIterator,
Obj,
coerceToString,
isRuntimeReference,
type Value,
} from "../interpreter/objects.js"
import { applyCollectionCallback } from "../interpreter/callback.js"
import { isRuntimeReference } from "../interpreter/references.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToString } from "./value.js"
import { readPairs } from "./url.js"
// The host validates header names and values and throws its own TypeError; the program gets one of its own.
@@ -17,7 +27,7 @@ const attempt = <T>(run: () => T): T => {
}
}
const constructHeaders = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj): Effect.Effect<HeadersObj, unknown, R> => {
const constructHeaders = <R>(ctx: Interpreter<R>, init: Value, proto: Obj): Effect.Effect<HeadersObj, unknown, R> => {
const wrap = (headers: Headers) => new HeadersObj(proto, headers)
if (init === undefined) return Effect.succeed(wrap(new Headers()))
return Effect.gen(function* () {
@@ -40,10 +50,10 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
call: requiresNew("Headers"),
construct: (args, newTarget) => constructHeaders(ctx, args[0], prototypeFrom(newTarget, proto)),
})
const self = (thisValue: unknown, name: string) => receiver(HeadersObj, thisValue, `Headers.prototype.${name}`)
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
const arg = (args: Array<unknown>, index: number): string => coerceToString(args[index])
const requireArgs = (name: string, args: Array<unknown>, count: number): void => {
const self = (thisValue: Value, name: string) => receiver(HeadersObj, thisValue, `Headers.prototype.${name}`)
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
const arg = (args: Array<Value>, index: number): string => coerceToString(args[index])
const requireArgs = (name: string, args: Array<Value>, count: number): void => {
if (args.length < count) throw typeError(`Headers.${name} requires ${count} argument${count === 1 ? "" : "s"}.`)
}
methods(builtins, proto, [
@@ -53,7 +63,8 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
requireArgs("append", args, 2)
const target = self(thisValue, "append").headers
return attempt(() => target.append(arg(args, 0), arg(args, 1)))
attempt(() => target.append(arg(args, 0), arg(args, 1)))
return undefined
},
],
[
@@ -62,7 +73,8 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
requireArgs("delete", args, 1)
const target = self(thisValue, "delete").headers
return attempt(() => target.delete(arg(args, 0)))
attempt(() => target.delete(arg(args, 0)))
return undefined
},
],
[
@@ -90,29 +102,13 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
requireArgs("set", args, 2)
const target = self(thisValue, "set").headers
return attempt(() => target.set(arg(args, 0), arg(args, 1)))
attempt(() => target.set(arg(args, 0), arg(args, 1)))
return undefined
},
],
// Iterator.from because Bun's Headers typings predate iterator helpers; the runtime iterators already have them.
[
"keys",
0,
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "keys").headers.keys())),
],
[
"values",
0,
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "values").headers.values())),
],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
Iterator.from(self(thisValue, "entries").headers.entries()).map(([key, value]) => wrap([key, value])),
),
],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").headers.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").headers.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
[
"forEach",
1,
+336 -7
View File
@@ -1,19 +1,348 @@
import { methods, receiver } from "../interpreter/native.js"
import { IteratorObj, record } from "../interpreter/objects.js"
import { Cause, Effect, Exit } from "effect"
import { applyCollectionCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { rangeError, typeError } from "../interpreter/model.js"
import { constructor, fn, methods, receiver, requiresNew } from "../interpreter/native.js"
import {
Arr,
coerceToNumber,
type Cursor,
define,
hasPrototype,
hidden,
IteratorObj,
Obj,
record,
type Step,
type Value,
} from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
// Every built-in collection iterator shares the Iterator prototype; JS gives each collection its own, which is only
const finished: Step = { done: true, value: undefined }
// Every built-in iterator and helper shares the Iterator prototype; JS gives each collection its own, which is only
// observable through getPrototypeOf.
export const iteratorGlobals = <R>(ctx: Interpreter<R>): void => {
export const iteratorGlobals = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
methods(builtins, builtins.Iterator, [
const proto = builtins.Iterator
const method = (name: string) => `Iterator.prototype.${name}`
// A step that runs program code: when it throws, close the source first, as IteratorClose does.
const guarded = <A>(source: Cursor<R>, body: Effect.Effect<A, unknown, R>) =>
Effect.flatMap(Effect.exit(body), (exit) => {
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
return Effect.andThen(Effect.exit(source.close), Effect.failCause(exit.cause))
})
// A lazy helper: `pull` advances it once. Once it reports done, throws, or is closed, it stays done. A callback
// that re-enters its own helper is a TypeError, as for a running generator.
const helper = (source: Cursor<R>, pull: Effect.Effect<Step, unknown, R>, close = source.close) => {
let done = false
let running = false
const enter = () => {
if (running) throw typeError("Iterator helper is already running.")
running = true
}
return new IteratorObj(builtins.IteratorHelper, {
next: Effect.suspend(() => {
if (done) return Effect.succeed(finished)
enter()
return Effect.flatMap(Effect.exit(pull), (exit) => {
running = false
done = Exit.isSuccess(exit) ? exit.value.done : true
return exit
})
}),
close: Effect.suspend(() => {
if (done) return Effect.void
enter()
done = true
return Effect.ensuring(
close,
Effect.sync(() => {
running = false
}),
)
}),
})
}
// GetIteratorFlattenable: an iterable by `[Symbol.iterator]`, otherwise the object itself as an iterator.
const flattenable = (value: Value, name: string, strings: boolean) =>
Effect.gen(function* () {
if (typeof value === "string" ? !strings : !(value instanceof Obj)) {
throw typeError(`${name} expects an iterable or iterator, received ${describeValue(value)}.`)
}
return (yield* ctx.iterate(value)) ?? ctx.iterateDirect(value)
})
const limit = (value: Value, name: string) => {
const count = Math.trunc(coerceToNumber(value))
if (Number.isNaN(count) || count < 0) {
throw rangeError(`${method(name)} expects a non-negative count, received ${count}.`)
}
return count
}
methods(builtins, proto, [
[
"next",
0,
(thisValue) =>
Effect.map(receiver(IteratorObj, thisValue, method("next")).cursor.next, (step) =>
record(builtins.Object, { value: step.value, done: step.done }),
),
],
[
"map",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("map"))
let counter = 0
return helper(
source,
Effect.gen(function* () {
const step = yield* source.next
if (step.done) return finished
return { done: false, value: yield* guarded(source, apply([step.value, counter++])) }
}),
)
},
],
[
"filter",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("filter"))
let counter = 0
return helper(
source,
Effect.gen(function* () {
while (true) {
const step = yield* source.next
if (step.done) return finished
if (yield* guarded(source, apply([step.value, counter++]))) return step
}
}),
)
},
],
[
"take",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
let remaining = limit(args[0], "take")
return helper(
source,
Effect.gen(function* () {
if (remaining === 0) {
yield* source.close
return finished
}
remaining -= 1
return yield* source.next
}),
)
},
],
[
"drop",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
let remaining = limit(args[0], "drop")
return helper(
source,
Effect.gen(function* () {
while (remaining > 0) {
remaining -= 1
const step = yield* source.next
if (step.done) return finished
}
return yield* source.next
}),
)
},
],
[
"flatMap",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("flatMap"))
let counter = 0
let inner: Cursor<R> | undefined
return helper(
source,
Effect.gen(function* () {
while (true) {
if (inner !== undefined) {
const step = yield* guarded(source, inner.next)
if (!step.done) return step
inner = undefined
}
const step = yield* source.next
if (step.done) return finished
inner = yield* guarded(
source,
Effect.flatMap(apply([step.value, counter++]), (mapped) =>
flattenable(mapped, method("flatMap"), false),
),
)
}
}),
Effect.suspend(() => (inner === undefined ? source.close : Effect.andThen(inner.close, source.close))),
)
},
],
[
"reduce",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("reduce"))
return Effect.gen(function* () {
let counter = 0
let accumulator = args[1]
if (args.length < 2) {
const first = yield* source.next
if (first.done) throw typeError("Iterator.prototype.reduce of an empty iterator with no initial value.")
accumulator = first.value
counter = 1
}
while (true) {
const step = yield* source.next
if (step.done) return accumulator
accumulator = yield* guarded(source, apply([accumulator, step.value, counter++]))
}
})
},
],
[
"toArray",
0,
(thisValue) => {
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").iterator.next()
return record(builtins.Object, { value: step.value, done: Boolean(step.done) })
const source = ctx.iterateDirect(thisValue)
return Effect.gen(function* () {
const items: Array<Value> = []
while (true) {
const step = yield* source.next
if (step.done) return new Arr(builtins.Array, items)
items.push(step.value)
}
})
},
],
[
"forEach",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("forEach"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return undefined
yield* guarded(source, apply([step.value, counter++]))
}
})
},
],
[
"some",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("some"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return false
if (yield* guarded(source, apply([step.value, counter++]))) {
yield* source.close
return true
}
}
})
},
],
[
"every",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("every"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return true
if (!(yield* guarded(source, apply([step.value, counter++])))) {
yield* source.close
return false
}
}
})
},
],
[
"find",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("find"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return undefined
if (yield* guarded(source, apply([step.value, counter++]))) {
yield* source.close
return step.value
}
}
})
},
],
])
// Helpers and `Iterator.from` wrappers can be closed; collection iterators, as in JS, cannot.
methods(builtins, builtins.IteratorHelper, [
[
"return",
0,
(thisValue) =>
Effect.map(receiver(IteratorObj, thisValue, "Iterator.prototype.return").cursor.close, () =>
record(builtins.Object, { value: undefined, done: true }),
),
],
])
const iterator = constructor<R>(builtins, proto, {
name: "Iterator",
length: 0,
call: requiresNew("Iterator"),
construct: () =>
Effect.sync(() => {
throw typeError("Iterator is abstract; use Iterator.from(...) or a built-in iterator.")
}),
})
define(
iterator,
"from",
fn(builtins, "from", 1, (_, args) =>
Effect.gen(function* () {
if (args[0] instanceof Obj && hasPrototype(args[0], proto)) return args[0]
return new IteratorObj(builtins.IteratorHelper, yield* flattenable(args[0], "Iterator.from", true))
}),
),
hidden,
)
return iterator
}
+6 -7
View File
@@ -3,10 +3,10 @@ import { methods } from "../interpreter/native.js"
import { applyCollectionCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { checkStringLength } from "../interpreter/limits.js"
import { syntaxError, typeError } from "../interpreter/model.js"
import { syntaxError } from "../interpreter/model.js"
import { typeofValue } from "../interpreter/references.js"
import { fromJson, toJson } from "../data.js"
import { get, keys, Arr, Obj, record, remove, set } from "../interpreter/objects.js"
import { get, keys, Arr, Obj, coerceToString, record, remove, set, type Value } from "../interpreter/objects.js"
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
const json = new Obj(ctx.builtins.Object)
@@ -17,9 +17,8 @@ export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
return json
}
const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
const text = args[0]
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
const parse = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
const text = coerceToString(args[0])
const parsed = (() => {
try {
@@ -31,7 +30,7 @@ const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unkn
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
const apply = applyCollectionCallback(ctx, args[1], "JSON.parse")
const visit = (holder: Obj, key: string): Effect.Effect<unknown, unknown, R> =>
const visit = (holder: Obj, key: string): Effect.Effect<Value, unknown, R> =>
Effect.gen(function* () {
const value = get(holder, key)
if (value instanceof Obj) {
@@ -46,7 +45,7 @@ const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unkn
return visit(record(ctx.builtins.Object, { "": parsed }), "")
}
const stringify = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
const stringify = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
const space = args[2]
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
const replacer = args[1]
+5 -18
View File
@@ -1,7 +1,7 @@
import { Effect } from "effect"
import { constants, type Method, methods } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import { Obj } from "../interpreter/objects.js"
import { Obj, coerceToNumber } from "../interpreter/objects.js"
import { preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
@@ -14,31 +14,18 @@ declare global {
// Validate only the arguments a method consumes; like JS, extras are ignored
// (so built-ins work as callbacks receiving (element, index, array)).
const number = (name: string, args: Array<unknown>, index: number): number => {
if (index >= args.length) return Number.NaN
const arg = args[index]
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
return arg
}
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(number(name, args, 0))]
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(coerceToNumber(args[0]))]
const binary = (name: string, op: (a: number, b: number) => number): Method => [
name,
2,
(_, args) => op(number(name, args, 0), number(name, args, 1)),
(_, args) => op(coerceToNumber(args[0]), coerceToNumber(args[1])),
]
const variadic = (name: string, op: (...values: Array<number>) => number): Method => [
name,
2,
(_, args) =>
op(
...args.map((arg) => {
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
return arg
}),
),
(_, args) => op(...args.map(coerceToNumber)),
]
export const mathGlobal = <R>(ctx: Interpreter<R>) => {
@@ -105,7 +92,7 @@ export const mathGlobal = <R>(ctx: Interpreter<R>) => {
const step = yield* cursor.next
if (step.done) return Math.sumPrecise(numbers)
yield* preserveConsumerError(
cursor,
cursor.close,
Effect.sync(() => {
if (typeof step.value !== "number") {
throw typeError("Math.sumPrecise expects an iterable of numbers.")
+10 -21
View File
@@ -1,7 +1,8 @@
import { constructor, constants, methods } from "../interpreter/native.js"
import { coerceToNumber, coerceToString, type Value } from "../interpreter/objects.js"
import { rangeError, typeError } from "../interpreter/model.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coercion, coerceToString } from "./value.js"
import { coercion } from "./value.js"
export const numberGlobal = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
@@ -29,39 +30,27 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
"parseInt",
2,
(_, args) => {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw typeError("Number.parseInt expects a numeric radix.")
}
return parseInt(coerceToString(args[0]), radix)
return parseInt(coerceToString(args[0]), coerceToNumber(args[1]))
},
],
["parseFloat", 1, (_, args) => parseFloat(coerceToString(args[0]))],
])
const self = (thisValue: unknown, name: string): number => {
const self = (thisValue: Value, name: string): number => {
if (typeof thisValue === "number") return thisValue
throw typeError(`Number.prototype.${name} requires that 'this' be a Number.`)
}
const optNum = (name: string, arg: unknown): number | undefined => {
if (arg === undefined) return undefined
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
return arg
}
const optNum = (arg: Value): number | undefined => (arg === undefined ? undefined : coerceToNumber(arg))
methods(builtins, builtins.Number, [
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum(args[0]))],
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
[
"toExponential",
1,
(thisValue, args) => self(thisValue, "toExponential").toExponential(optNum("toExponential", args[0])),
],
["toExponential", 1, (thisValue, args) => self(thisValue, "toExponential").toExponential(optNum(args[0]))],
[
"toPrecision",
1,
(thisValue, args) => {
const value = self(thisValue, "toPrecision")
const digits = optNum("toPrecision", args[0])
const digits = optNum(args[0])
return digits === undefined ? value.toString() : value.toPrecision(digits)
},
],
@@ -70,7 +59,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
1,
(thisValue, args) => {
const value = self(thisValue, "toString")
const radix = optNum("toString", args[0])
const radix = optNum(args[0])
if (radix !== undefined && (radix < 2 || radix > 36)) {
throw rangeError("Number.toString radix must be between 2 and 36.")
}
@@ -89,7 +78,7 @@ export const booleanGlobal = <R>(ctx: Interpreter<R>) => {
length: 1,
call: coercion(ctx, "Boolean").call,
})
const self = (thisValue: unknown, name: string): boolean => {
const self = (thisValue: Value, name: string): boolean => {
if (typeof thisValue === "boolean") return thisValue
throw typeError(`Boolean.prototype.${name} requires that 'this' be a Boolean.`)
}
+10 -19
View File
@@ -9,7 +9,6 @@ import {
typeError,
} from "../interpreter/model.js"
import {
Callable,
define,
entries,
enumerableKeys,
@@ -20,23 +19,20 @@ import {
keys,
own,
Arr,
Bytes,
DateObj,
ErrorObj,
Obj,
PromiseObj,
RegExpObj,
set,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
import { invoke, preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { ToolReference } from "../tool-runtime.js"
import { groupBy } from "./collections.js"
import { coerceToString } from "./value.js"
// ToObject for enumeration.
export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: unknown, node?: AstNode): Obj => {
export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: Value, node?: AstNode): Obj => {
if (value === null || value === undefined) {
throw typeError(`${label} cannot convert ${describeValue(value)} to an object.`, node)
}
@@ -54,7 +50,7 @@ export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: u
return new Obj(ctx.builtins.Object)
}
export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<unknown>): unknown => {
export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<Value>): Value => {
const target = args[0]
// JS would box a primitive target; wrappers and primitives cannot hold fields here.
if (!(target instanceof Obj)) {
@@ -75,7 +71,7 @@ export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<unknown>): unkn
return target
}
const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effect<Obj, unknown, R> => {
const objectFromEntries = <R>(ctx: Interpreter<R>, source: Value): Effect.Effect<Obj, unknown, R> => {
const out = new Obj(ctx.builtins.Object)
return Effect.gen(function* () {
const cursor = yield* ctx.iterate(source)
@@ -86,7 +82,7 @@ const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effe
const step = yield* cursor.next
if (step.done) return out
yield* preserveConsumerError(
cursor,
cursor.close,
Effect.sync(() => {
if (!(step.value instanceof Obj) || containsOpaqueReference(step.value)) {
throw typeError("Object.fromEntries expects [key, value] entry objects.")
@@ -98,29 +94,24 @@ const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effe
})
}
export const classTag = (value: unknown): string => {
const classTag = (value: Value): string => {
if (value === null) return "Null"
if (value === undefined) return "Undefined"
if (value instanceof Arr) return "Array"
if (value instanceof Callable) return "Function"
if (value instanceof ErrorObj) return "Error"
if (value instanceof DateObj) return "Date"
if (value instanceof RegExpObj) return "RegExp"
if (value instanceof Bytes) return "Uint8Array"
if (value instanceof Obj) return value.tag
if (typeof value === "string") return "String"
if (typeof value === "number") return "Number"
if (typeof value === "boolean") return "Boolean"
return "Object"
}
const propertyKey = (value: unknown): PropertyKey =>
const propertyKey = (value: Value): PropertyKey =>
value === AsyncIteratorSymbol || value === IteratorSymbol ? value : coerceToString(value)
// 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>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const construct = (args: Array<unknown>): unknown => {
const construct = (args: Array<Value>): Value => {
const first = args[0]
if (first === null || first === undefined) return new Obj(builtins.Object)
if (first instanceof Obj) return first
+14 -5
View File
@@ -2,9 +2,18 @@ import { Effect } from "effect"
import type { Builtins } from "../interpreter/intrinsics.js"
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
import { syntaxError, typeError } from "../interpreter/model.js"
import { define, defineAccessor, Arr, Obj, RegExpObj, record } from "../interpreter/objects.js"
import {
define,
defineAccessor,
Arr,
Obj,
RegExpObj,
record,
coerceToNumber,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToNumber, coerceToString } from "./value.js"
const flagProperties = [
"hasIndices",
@@ -23,7 +32,7 @@ const regexFailureReason = (error: unknown): string =>
const escapeRegexHint =
'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.'
export const toHostRegex = (arg: unknown, method: string, extraFlags = ""): RegExp => {
export const toHostRegex = (arg: Value, method: string, extraFlags = ""): RegExp => {
// Native parity: an undefined pattern behaves as an empty pattern.
if (arg === undefined) return new RegExp("", extraFlags)
if (arg instanceof RegExpObj) return arg.regex
@@ -53,7 +62,7 @@ export const matchToValue = (builtins: Builtins, match: RegExpMatchArray): Arr =
return result
}
export const constructRegExp = (builtins: Builtins, args: Array<unknown>, proto: Obj = builtins.RegExp): RegExpObj => {
export const constructRegExp = (builtins: Builtins, args: Array<Value>, proto: Obj = builtins.RegExp): RegExpObj => {
const first = args[0]
const pattern = first instanceof RegExpObj ? first.regex.source : first === undefined ? "" : coerceToString(first)
const flagsArg = args[1]
@@ -96,7 +105,7 @@ export const regexpGlobal = <R>(ctx: Interpreter<R>) => {
],
])
const self = (thisValue: unknown, name: string) => receiver(RegExpObj, thisValue, `RegExp.prototype.${name}`)
const self = (thisValue: Value, name: string) => receiver(RegExpObj, thisValue, `RegExp.prototype.${name}`)
defineAccessor(proto, "source", (thisValue) => self(thisValue, "source").regex.source)
defineAccessor(proto, "flags", (thisValue) => self(thisValue, "flags").regex.flags)
// The host regex holds the only lastIndex, so exec/test and the String methods share one counter.
+40 -42
View File
@@ -1,16 +1,26 @@
import { Effect } from "effect"
import { constructor, fn, type Method, methods } from "../interpreter/native.js"
import { constructor, fn, type Impl, type Method, methods } from "../interpreter/native.js"
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
import { define, hidden, Arr, IteratorObj, PromiseObj, RegExpObj, record } from "../interpreter/objects.js"
import {
define,
hidden,
Arr,
hostIterator,
RegExpObj,
record,
coerceToNumber,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { containsOpaqueReference, typeofValue } from "../interpreter/references.js"
import { applyCollectionCallback, isSupportedCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { matchToValue, toHostRegex } from "./regexp.js"
import { coerceToNumber, coerceToString, coercion } from "./value.js"
import { coercion } from "./value.js"
// console is intercepted by the interpreter before reaching here.
const requireDataArgument = (name: string, index: number, arg: unknown): unknown => {
const requireDataArgument = (name: string, index: number, arg: Value): Value => {
if (containsOpaqueReference(arg)) {
throw invalidData(`String.${name} expects argument ${index + 1} to be a data value.`)
}
@@ -29,21 +39,23 @@ const replaceWithCallback = <R>(
ctx: Interpreter<R>,
value: string,
name: "replace" | "replaceAll",
args: Array<unknown>,
): Effect.Effect<unknown, unknown, R> => {
args: Array<Value>,
): Effect.Effect<Value, unknown, R> => {
const builtins = ctx.builtins
const apply = applyCollectionCallback(ctx, args[1], `String.${name}`)
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
const collect = (...callbackArgs: Array<unknown>): string => {
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<Value> }> = []
// The host calls back with (match, ...captures, offset, string, groups?); only groups is not already a Value.
const collect = (
...callbackArgs: Array<string | number | undefined | Record<string, string | undefined>>
): string => {
const match = callbackArgs[0]
const groups = callbackArgs[callbackArgs.length - 1]
const hasGroups = groups !== null && typeof groups === "object"
const hasGroups = typeof callbackArgs.at(-1) === "object"
const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)]
if (typeof match !== "string" || typeof offset !== "number") {
throw typeError(`String.${name} produced an invalid replacement match.`)
}
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(builtins.Object, groups as Record<string, unknown>)
matches.push({ match, offset, args: callbackArgs })
const args = callbackArgs.map((arg) => (typeof arg === "object" ? record(builtins.Object, arg) : arg))
matches.push({ match, offset, args })
return match
}
@@ -63,10 +75,7 @@ const replaceWithCallback = <R>(
let end = 0
for (const match of matches) {
const replacement = yield* apply(match.args)
output.push(
value.slice(end, match.offset),
replacement instanceof PromiseObj ? "[object Promise]" : coerceToString(replacement),
)
output.push(value.slice(end, match.offset), coerceToString(replacement))
end = match.offset + match.match.length
}
output.push(value.slice(end))
@@ -84,22 +93,14 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
const codeUnits = (name: string, op: (...codes: Array<number>) => string): Method => [
name,
1,
(_, args) =>
op(
...args.map((arg) => {
if (typeof arg !== "number") {
throw typeError(`String.${name} expects number arguments.`)
}
return arg
}),
),
(_, args) => op(...args.map(coerceToNumber)),
]
methods(builtins, string, [
codeUnits("fromCharCode", String.fromCharCode),
codeUnits("fromCodePoint", String.fromCodePoint),
])
const self = (thisValue: unknown, name: string): string => {
const self = (thisValue: Value, name: string): string => {
if (typeof thisValue === "string") return thisValue
if (thisValue === null || thisValue === undefined) {
throw typeError(`String.prototype.${name} called on null or undefined.`)
@@ -107,26 +108,26 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
return coerceToString(thisValue)
}
// Coerce arguments like native JS; opaque runtime references still reject.
const str = (name: string, args: Array<unknown>, index: number): string =>
const str = (name: string, args: Array<Value>, index: number): string =>
coerceToString(requireDataArgument(name, index, args[index]))
const num = (name: string, args: Array<unknown>, index: number): number =>
const num = (name: string, args: Array<Value>, index: number): number =>
coerceToNumber(requireDataArgument(name, index, args[index]))
const optNum = (name: string, args: Array<unknown>, index: number): number | undefined =>
const optNum = (name: string, args: Array<Value>, index: number): number | undefined =>
args[index] === undefined ? undefined : num(name, args, index)
const optStr = (name: string, args: Array<unknown>, index: number): string | undefined =>
const optStr = (name: string, args: Array<Value>, index: number): string | undefined =>
args[index] === undefined ? undefined : str(name, args, index)
const rejectRegex = (name: string, args: Array<unknown>): void => {
const rejectRegex = (name: string, args: Array<Value>): void => {
if (args[0] instanceof RegExpObj) {
throw typeError(
`String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`,
)
}
}
const simple = (name: string, length: number, op: (value: string, args: Array<unknown>) => unknown): Method => [
name,
length,
(thisValue, args) => op(self(thisValue, name), args),
]
const simple = (
name: string,
length: number,
op: (value: string, args: Array<Value>) => ReturnType<Impl>,
): Method => [name, length, (thisValue, args) => op(self(thisValue, name), args)]
const replace = (name: "replace" | "replaceAll") =>
simple(name, 2, (value, args) => {
if (isSupportedCallback(args[1])) return replaceWithCallback(ctx, value, name, args)
@@ -218,7 +219,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
`String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`,
)
}
const matches: Array<unknown> = []
const matches: Array<Value> = []
for (const match of value.matchAll(pattern)) {
checkArrayLength(matches.length + 1)
matches.push(matchToValue(builtins, match))
@@ -263,11 +264,8 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
define(
builtins.String,
IteratorSymbol,
fn(
builtins,
"[Symbol.iterator]",
0,
(thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
fn(builtins, "[Symbol.iterator]", 0, (thisValue) =>
hostIterator(builtins, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
),
hidden,
)
+19 -30
View File
@@ -7,17 +7,17 @@ import {
entries,
get,
hidden,
isWrapper,
Arr,
IteratorObj,
hostIterator,
Obj,
URLObj,
URLSearchParamsObj,
coerceToString,
isRuntimeReference,
type Value,
} from "../interpreter/objects.js"
import { isRuntimeReference } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToString } from "./value.js"
const urlProperties = [
"href",
@@ -52,12 +52,12 @@ export const uriGlobal = <R>(ctx: Interpreter<R>, name: UriFunction) =>
}
})
const urlArgument = (value: unknown): string => (value instanceof URLObj ? value.url.href : coerceToString(value))
const urlArgument = (value: Value): string => (value instanceof URLObj ? value.url.href : coerceToString(value))
export const urlGlobal = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const proto = builtins.URL
const construct = (args: Array<unknown>, into: Obj): URLObj => {
const construct = (args: Array<Value>, into: Obj): URLObj => {
if (args.length === 0) {
throw typeError("new URL(...) requires a URL string and an optional base URL.")
}
@@ -92,7 +92,7 @@ export const urlGlobal = <R>(ctx: Interpreter<R>) => {
]
methods(builtins, url, [parse("canParse"), parse("parse")])
const self = (thisValue: unknown, name: string) => receiver(URLObj, thisValue, `URL.prototype.${name}`)
const self = (thisValue: Value, name: string) => receiver(URLObj, thisValue, `URL.prototype.${name}`)
for (const name of urlProperties) {
defineAccessor(
proto,
@@ -119,7 +119,7 @@ export const urlGlobal = <R>(ctx: Interpreter<R>) => {
return url
}
const readPair = <R>(ctx: Interpreter<R>, value: unknown, label: string): Effect.Effect<Array<string>, unknown, R> =>
const readPair = <R>(ctx: Interpreter<R>, value: Value, label: string): Effect.Effect<Array<string>, unknown, R> =>
Effect.gen(function* () {
const cursor = yield* ctx.iterate(value)
if (cursor === undefined) throw typeError(`${label} expects iterable [name, value] pairs.`)
@@ -129,7 +129,7 @@ const readPair = <R>(ctx: Interpreter<R>, value: unknown, label: string): Effect
if (step.done) return items
items.push(
yield* preserveConsumerError(
cursor,
cursor.close,
Effect.sync(() => coerceToString(step.value)),
),
)
@@ -142,7 +142,7 @@ const readPair = <R>(ctx: Interpreter<R>, value: unknown, label: string): Effect
*/
export const readPairs = <R>(
ctx: Interpreter<R>,
init: unknown,
init: Value,
label: string,
): Effect.Effect<Array<[string, string]> | undefined, unknown, R> =>
Effect.gen(function* () {
@@ -155,13 +155,13 @@ export const readPairs = <R>(
if (pairs.some((entry) => entry.length !== 2)) throw typeError(`${label} expects iterable [name, value] pairs.`)
return pairs as Array<[string, string]>
}
pairs.push(yield* preserveConsumerError(cursor, readPair(ctx, step.value, label)))
pairs.push(yield* preserveConsumerError(cursor.close, readPair(ctx, step.value, label)))
}
})
const constructURLSearchParams = <R>(
ctx: Interpreter<R>,
init: unknown,
init: Value,
proto: Obj,
): Effect.Effect<URLSearchParamsObj, unknown, R> => {
const wrap = (params: URLSearchParams) => new URLSearchParamsObj(proto, params)
@@ -177,7 +177,6 @@ const constructURLSearchParams = <R>(
if (isRuntimeReference(init)) {
throw typeError("new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.")
}
if (isWrapper(init)) return wrap(new URLSearchParams())
if (!(init instanceof Obj)) {
throw typeError(
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
@@ -197,11 +196,11 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
call: requiresNew("URLSearchParams"),
construct: (args, newTarget) => constructURLSearchParams(ctx, args[0], prototypeFrom(newTarget, proto)),
})
const self = (thisValue: unknown, name: string) =>
const self = (thisValue: Value, name: string) =>
receiver(URLSearchParamsObj, thisValue, `URLSearchParams.prototype.${name}`)
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
const arg = (args: Array<unknown>, index: number): string => coerceToString(args[index])
const requireArgs = (name: string, args: Array<unknown>, count: number): void => {
const wrap = (items: Array<Value>) => new Arr(builtins.Array, items)
const arg = (args: Array<Value>, index: number): string => coerceToString(args[index])
const requireArgs = (name: string, args: Array<Value>, count: number): void => {
if (args.length < count) {
throw typeError(`URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`)
}
@@ -270,19 +269,9 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").params.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").params.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
self(thisValue, "entries")
.params.entries()
.map(([key, value]) => wrap([key, value])),
),
],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").params.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").params.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
[
"forEach",
+3 -68
View File
@@ -1,64 +1,12 @@
import { fn } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import {
get,
isWrapper,
type Native,
Arr,
Bytes,
DateObj,
ErrorObj,
MapObj,
RegExpObj,
SetObj,
URLObj,
URLSearchParamsObj,
HeadersObj,
} from "../interpreter/objects.js"
import { coerceToNumber, coerceToString, type Native, type Value } from "../interpreter/objects.js"
import type { Interpreter } from "../interpreter/interpreter.js"
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
/** The built-in string form of a value, without consulting program-defined `toString` methods. */
export const coerceToString = (value: unknown): string => {
if (value === null) return "null"
if (value === undefined) return "undefined"
if (value instanceof DateObj) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
if (value instanceof RegExpObj) return `/${value.regex.source}/${value.regex.flags}`
if (value instanceof MapObj) return "[object Map]"
if (value instanceof SetObj) return "[object Set]"
if (value instanceof URLObj) return value.url.href
if (value instanceof URLSearchParamsObj) return value.params.toString()
if (value instanceof HeadersObj) return "[object Headers]"
if (value instanceof Bytes) return value.bytes.join(",")
if (value instanceof ErrorObj) {
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
const name = get(value, "name")
const message = get(value, "message")
const shownName = typeof name === "string" ? name : "Error"
const shownMessage = typeof message === "string" ? message : ""
if (shownMessage === "") return shownName
if (shownName === "") return shownMessage
return `${shownName}: ${shownMessage}`
}
if (value instanceof Arr) {
return value.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
}
if (typeof value === "object") return "[object Object]"
return String(value)
}
export const coerceToNumber = (value: unknown): number => {
if (value instanceof DateObj) return value.time
if (value instanceof Bytes) return Number(coerceToString(value))
if (isWrapper(value)) return Number.NaN
if (value instanceof Arr) return Number(coerceToString(value))
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
}
export type Coercion = "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN"
const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<unknown>): unknown => {
const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<Value>): Value => {
// Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the
// other coercers match native through the undefined-argument path below.
if (args.length === 0) {
@@ -66,25 +14,12 @@ const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<unknown>): u
if (name === "String") return ""
}
const raw = args[0]
if (isWrapper(raw)) {
if (name === "Boolean") return true
if (name === "Number") return coerceToNumber(raw)
if (name === "String") return coerceToString(raw)
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
if (name === "isNaN") return Number.isNaN(coerceToNumber(raw))
if (name === "parseInt") return parseInt(coerceToString(raw))
return parseFloat(coerceToString(raw))
}
if (name === "Number") return coerceToNumber(raw)
if (name === "Boolean") return Boolean(raw)
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
if (name === "isNaN") return Number.isNaN(coerceToNumber(raw))
if (name === "parseInt") {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw typeError("parseInt expects a numeric radix.")
}
return parseInt(coerceToString(raw), radix)
return parseInt(coerceToString(raw), coerceToNumber(args[1]))
}
if (name === "parseFloat") return parseFloat(coerceToString(raw))
return coerceToString(raw)
+1 -2
View File
@@ -1,9 +1,8 @@
import { fn, methods } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import { Bytes, Obj } from "../interpreter/objects.js"
import { Bytes, Obj, coerceToString } from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coerceToString } from "./value.js"
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies. Invalid input is a
// TypeError as well; browsers throw a DOMException named InvalidCharacterError, which CodeMode does not have.
+51 -39
View File
@@ -176,7 +176,7 @@ const describeTool = <R>(visible: VisibleTool<R>): ToolDescription => {
// Joining the final fragments avoids retaining the rendering's intermediate string ropes in JSC.
return (signature ??= [
toolExpression(visible.path),
isEmptyInput(visible.tool) ? "()" : `(input: ${inputTypeScript(visible.tool, true)})`,
isEmptyInput(visible.tool) ? "()" : `(${inputTypeScript(visible.tool, true)})`,
`: Promise<${outputTypeScript(visible.tool, true)}>`,
].join(""))
},
@@ -211,6 +211,32 @@ const termForms = (term: string): Array<string> => {
return forms
}
const rank = (entries: ReadonlyArray<SearchEntry>, query: string): Array<SearchEntry> => {
const terms = tokenize(query).map(termForms)
return entries
.map((entry) => {
const path = entry.description.path.toLowerCase()
const description = entry.description.description.toLowerCase()
const score = terms.reduce(
(total, forms) =>
total +
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
(forms.some((form) => entry.pathWords.includes(form)) ? 12 : 0) +
(forms.some((form) => path.includes(form)) ? 8 : 0) +
(forms.some((form) => description.includes(form)) ? 4 : 0) +
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0),
0,
)
return { entry, score }
})
.filter(({ score }) => terms.length === 0 || score > 0)
.sort(
(left, right) =>
right.score - left.score || compareText(left.entry.description.path, right.entry.description.path),
)
.map(({ entry }) => entry)
}
const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
_tag: "CodeModeTool",
description: "Search available tools",
@@ -221,14 +247,15 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
const request = input as typeof SearchInput.Type
const query = request.query ?? ""
const offset = request.offset ?? 0
let ns = request.namespace
if (ns !== undefined && !searchIndex.some((entry) => entry.description.path.startsWith("tools."))) {
if (ns === "tools") ns = undefined
else if (ns.startsWith("tools.")) ns = ns.slice("tools.".length)
}
const scoped =
request.namespace === undefined
ns === undefined
? searchIndex
: searchIndex.filter(
(entry) =>
entry.description.path === request.namespace ||
entry.description.path.startsWith(`${request.namespace}.`),
)
: searchIndex.filter((entry) => entry.description.path === ns || entry.description.path.startsWith(`${ns}.`))
const trimmed = query.trim()
const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed
const exact =
@@ -237,32 +264,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
: scoped.find(
(entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed,
)
const terms = tokenize(query).map(termForms)
const ranked =
exact !== undefined
? [exact]
: scoped
.map((entry) => {
const path = entry.description.path.toLowerCase()
const description = entry.description.description.toLowerCase()
const score = terms.reduce(
(total, forms) =>
total +
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
(forms.some((form) => entry.pathWords.includes(form)) ? 12 : 0) +
(forms.some((form) => path.includes(form)) ? 8 : 0) +
(forms.some((form) => description.includes(form)) ? 4 : 0) +
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0),
0,
)
return { entry, score }
})
.filter(({ score }) => terms.length === 0 || score > 0)
.sort(
(left, right) =>
right.score - left.score || compareText(left.entry.description.path, right.entry.description.path),
)
.map(({ entry }) => entry)
const ranked = exact !== undefined ? [exact] : rank(scoped, query)
const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({
...description,
path: toolExpression(description.path),
@@ -279,7 +281,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
/** Exact callable signature of the built-in `search` function, for host-owned instructions. */
export const searchSignature = (() => {
const tool = makeSearchTool([])
return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`
return `search(${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`
})()
const toSearchEntry = <R>(visible: VisibleTool<R>): SearchEntry => ({
@@ -328,13 +330,23 @@ const namespaceKeys = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Reado
return Array.from(node.children.keys())
}
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Tool<R> => {
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>, index: ReadonlyArray<SearchEntry>): Tool<R> => {
const segments = canonicalSegments(path)
const node = lookup(root, segments)
if (node === undefined) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
"The tool may have been removed or renamed. Use search to find available tools.",
])
const name = segments.join(".")
const ns = segments.length > 1 && root.children.has(segments[0]) ? segments[0] : undefined
const closest = rank(
ns ? index.filter((entry) => entry.description.path.startsWith(`${ns}.`)) : index,
ns ? segments.slice(1).join(" ") : name,
)[0]
throw new ToolRuntimeError(
"UnknownTool",
closest
? `Unknown tool '${name}'. Did you mean ${toolExpression(closest.description.path)}?`
: `Unknown tool '${name}'.`,
["Use search to find available tools."],
)
}
if (node.tool === undefined) {
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`)
@@ -439,7 +451,7 @@ export const make = <R>(
// Models often write `tools.search(...)` for the bare `search(...)`; honor it unless a tool owns that path.
if (segments.length === 1 && segments[0] === "search" && lookup(root, segments) === undefined)
return executeTool("search", makeSearchTool(prepared.searchIndex), args)
return executeTool(segments.join("."), resolve(root, path), args)
return executeTool(segments.join("."), resolve(root, path, prepared.searchIndex), args)
}),
}
}
+2 -4
View File
@@ -271,10 +271,8 @@ describe("still-rejected callables get the wrap hint", () => {
test("built-in references work as replacers", async () => {
// Like real JS: JSON.stringify(match, offset, string) quotes the match.
expect(await value(`return "abc".replace(/b/, JSON.stringify)`)).toBe('a"b"c')
// Math methods stay strict about consumed arguments: a match string is not coerced.
expect((await error(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).message).toContain(
"Math.floor expects number arguments",
)
// Math methods coerce the match string, as in JS.
expect(await value(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).toBe("3")
})
test("non-callables still get the plain callback error", async () => {
+14 -11
View File
@@ -581,7 +581,7 @@ describe("CodeMode schema flexibility", () => {
{
path: "adapter.call",
description: "Call an adapter-described tool",
signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise<void>",
signature: "tools.adapter.call({\n id: string,\n count?: number,\n}): Promise<void>",
},
])
@@ -664,7 +664,7 @@ describe("CodeMode schema flexibility", () => {
{
path: "users.lookup",
description: "Look up a user",
signature: "tools.users.lookup(input: {\n login: string,\n}): Promise<{\n login: string,\n id: number,\n}>",
signature: "tools.users.lookup({\n login: string,\n}): Promise<{\n login: string,\n id: number,\n}>",
},
])
@@ -680,7 +680,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({\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)
@@ -737,7 +737,7 @@ describe("CodeMode public contract", () => {
{
path: "orders.lookup",
description: "Look up an order by ID",
signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
signature: "tools.orders.lookup({\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
},
])
@@ -749,8 +749,7 @@ describe("CodeMode public contract", () => {
{
path: "tools.orders.lookup",
description: "Look up an order by ID",
signature:
"tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
signature: "tools.orders.lookup({\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
},
],
remaining: 0,
@@ -792,7 +791,7 @@ describe("CodeMode public contract", () => {
{
path: "context7.resolve-library-id",
description: "Resolve a library ID",
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
signature: 'tools.context7["resolve-library-id"]({\n libraryName: string,\n}): Promise<string>',
},
])
@@ -804,7 +803,7 @@ describe("CodeMode public contract", () => {
{
path: 'tools.context7["resolve-library-id"]',
description: "Resolve a library ID",
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
signature: 'tools.context7["resolve-library-id"]({\n libraryName: string,\n}): Promise<string>',
},
],
remaining: 0,
@@ -857,12 +856,12 @@ describe("CodeMode public contract", () => {
{
path: "tools.thread.uploadFile",
description: "Upload one readable local file to the current Discord thread",
signature: "tools.thread.uploadFile(input: {\n path: string,\n}): Promise<{\n sent: boolean,\n}>",
signature: "tools.thread.uploadFile({\n path: string,\n}): Promise<{\n sent: boolean,\n}>",
},
{
path: "tools.thread.generateImage",
description: "Generate an image and upload it to the current Discord thread",
signature: "tools.thread.generateImage(input: {\n prompt: string,\n}): Promise<{\n sent: boolean,\n}>",
signature: "tools.thread.generateImage({\n prompt: string,\n}): Promise<{\n sent: boolean,\n}>",
},
],
remaining: 0,
@@ -950,7 +949,7 @@ describe("CodeMode public contract", () => {
{
path: "tools.many.tool13",
description: "Numbered tool 13",
signature: "tools.many.tool13(input: {\n id: string,\n}): Promise<string>",
signature: "tools.many.tool13({\n id: string,\n}): Promise<string>",
},
],
remaining: 0,
@@ -996,6 +995,10 @@ describe("CodeMode public contract", () => {
expect(value.items[0]?.path).toBe("tools.linear.list_issues")
}
const prefixed = await Effect.runPromise(runtime.execute(`return search({ query: "", namespace: "tools.github" })`))
expect(prefixed.ok).toBe(true)
if (prefixed.ok) expect((prefixed.value as { items: Array<unknown> }).items).toHaveLength(2)
const invalid = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: 7 })`))
expect(invalid.ok).toBe(false)
if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput")
+1 -1
View File
@@ -156,7 +156,7 @@ describe("host errors escaping built-ins", () => {
test("an un-awaited rejection born inside promise machinery keeps its location in the warning", async () => {
const result = await run(`Promise.all(1); return 1`)
expect(result.ok && result.warnings?.[0]?.message).toEndWith(
"TypeError: Promise.all expects an array or other synchronous iterable. (line 1, col 1)",
"TypeError: Promise.all expects a synchronous iterable, received a number. (line 1, col 1)",
)
})
@@ -529,6 +529,6 @@ describe("Test262 for-await-of adaptations", () => {
const result = await execute(`for await (const item of { values: [1, 2] }) {}`)
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.error.message).toContain("or custom iterator value")
expect(result.error.message).toContain("requires an iterable value, received a data object")
})
})
@@ -23,6 +23,27 @@ const value = async (code: string) => {
}
describe("confined generators", () => {
test("parameters are bound at the call; only the body waits for next()", async () => {
expect(
await value(`
const log = []
function* g(a = log.push("param")) { log.push("body"); yield a }
const it = g()
log.push("created")
it.next()
function* bad({ x } = null) { yield x }
async function* asyncBad({ x } = null) { yield x }
let failures = []
try { bad() } catch (error) { failures.push(error.constructor.name) }
try { asyncBad() } catch (error) { failures.push(error.constructor.name) }
return [log, failures]
`),
).toEqual([
["param", "created", "body"],
["TypeError", "TypeError"],
])
})
// test/built-ins/GeneratorPrototype/next/return-yield-expr.js
test("is lazy and preserves next(value), nested suspension, return, and exhaustion", async () => {
expect(
@@ -1030,7 +1051,7 @@ describe("confined generators", () => {
const params = new URLSearchParams(entries())
return [events, params.toString()]
`),
).toEqual([["first", "second", "pair close", "outer close"], "%5Bobject+Object%5D=2"])
).toEqual([["first", "second", "pair close", "outer close"], "%5Bobject+Promise%5D=2"])
})
test("validates URLSearchParams pair lengths after converting the outer sequence", async () => {
@@ -1246,4 +1267,27 @@ describe("confined generators", () => {
`),
).toEqual(["catch", "reaction"])
})
test("a generator's return() through for...of or destructuring surfaces a failing close, as break does", async () => {
expect(
await value(`
const make = (ret) => ({ [Symbol.iterator]: () => ({ next: () => ({ done: false, value: [1] }), return: ret }) })
const blanks = (ret) => ({ [Symbol.iterator]: () => ({ next: () => ({ done: false }), return: ret }) })
const outcomes = []
for (const [label, ret] of [
["null", () => null],
["throws", () => { throw new RangeError("close") }],
["ok", () => ({ done: true })],
]) {
function* loop() { for (const [a] of make(ret)) yield a }
function* pattern() { for ([ {} = yield ] of [blanks(ret)]) {} }
for (const g of [loop(), pattern()]) {
g.next()
try { g.return(7); outcomes.push(label + " quiet") } catch (e) { outcomes.push(label + " " + e.constructor.name) }
}
}
return outcomes
`),
).toEqual(["null TypeError", "null TypeError", "throws RangeError", "throws RangeError", "ok quiet", "ok quiet"])
})
})
+54 -3
View File
@@ -218,6 +218,19 @@ describe("property deletion", () => {
).toEqual([true, true, { keep: 1 }])
})
test("a non-reference operand is evaluated and the result is true; a variable cannot be deleted", async () => {
expect(
await value(`
let called = false
const results = [delete 0, delete null, delete { x: 1 }, delete void 0, delete (() => { called = true })()]
let variable = 1
let failure
try { delete variable } catch (error) { failure = error.constructor.name }
return [results, called, failure]
`),
).toEqual([[true, true, true, true, true], true, "TypeError"])
})
test("evaluates computed object and key expressions once", async () => {
expect(
await value(`
@@ -771,9 +784,17 @@ describe("destructuring assignment", () => {
).toEqual({ declared: "a", declarationRest: { 1: "b" }, assigned: "c", assignmentRest: { 1: "d" } })
})
test("rejects computed keys that are not confined property keys", async () => {
const err = await error(`const key = {}; const { [key]: value } = {}`)
expect(err.message).toContain("Property key must be a string or number")
test("computed keys of any type become their string form, as in JS", async () => {
expect(
await value(`
const counts = {}
for (const category of ["a", null, undefined, "a", true, 1.5]) counts[category] = (counts[category] ?? 0) + 1
const key = {}
const { [key]: value } = { "[object Object]": 7 }
const o = { null: 1, "1,2": 2 }
return [counts, value, o[null], o[[1, 2]], undefined in o]
`),
).toEqual([{ a: 2, null: 1, undefined: 1, true: 1, "1.5": 1 }, 7, 1, 2, false])
})
})
@@ -818,6 +839,36 @@ describe("coercion parity: global isFinite and isNaN", () => {
})
})
describe("coercion parity: built-in arguments coerce as in JS", () => {
test("numeric arguments apply ToIntegerOrInfinity", async () => {
expect(
await value(`
return [
[1, 2, 3].indexOf(2, "1"), [1, 2, 3].lastIndexOf(3, "5"), [1, 2, 3].includes(1, "1"),
[1, 2, 3, 4].slice("1", "3"), [1, 2, 3].at(null), [1, 2, 3].at(1.7),
[1, [2, [3]]].flat(1.9), [1, 2, 3].with(1.5, 9), [1, 2, 3, 4].splice("1", "2"),
Math.max("3", "2"), Math.floor(null), Math.hypot("3", "4"),
parseInt("11", "2"), Number.parseInt("ff", "16"), (1.5).toFixed("2"), (255).toString("16"),
String.fromCharCode("65", 66.9), new Uint8Array([1, 2, 3]).indexOf(2, "1"),
]
`),
).toEqual([1, 2, false, [2, 3], 1, 2, [1, 2, [3]], [1, 9, 3], [2, 3], 3, 0, 5, 3, 255, "1.50", "ff", "AB", 1])
})
test("join separators, JSON.parse text, and Array.from length coerce", async () => {
expect(
await value(`
return [
[1, 2].join(null), [1, 2].join(0), [1, 2].join(undefined), new Uint8Array([1, 2]).join(null),
JSON.parse(123), JSON.parse(true),
Array.from({ length: "2" }), Array.from({ length: 2.5 }), Array.from({ length: -1 }), Array.from({}),
]
`),
).toEqual(["1null2", "102", "1,2", "1null2", 123, true, [null, null], [null, null], [], []])
expect((await error(`return JSON.parse(undefined)`)).message).toContain("JSON")
})
})
describe("coercion parity: arrays coerce to numbers through their string form", () => {
test("arrays with objects become NaN instead of crashing on host ToPrimitive", async () => {
expect(await value(`let x = [{}]; x++; return Number.isNaN(x)`)).toBe(true)
+38 -3
View File
@@ -730,7 +730,7 @@ describe("Promise.all over arbitrary arrays", () => {
test("a non-collection argument is a clear error", async () => {
const diagnostic = await error(`return await Promise.all(42)`)
expect(diagnostic.message).toContain("Promise.all expects an array")
expect(diagnostic.message).toContain("Promise.all expects a synchronous iterable, received a number")
})
test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => {
@@ -1131,8 +1131,43 @@ describe("unsupported promise surface", () => {
})
test("unknown Promise statics are not functions", async () => {
const diagnostic = await error(`return await Promise.withResolvers()`)
expect(diagnostic.message).toContain("Promise.withResolvers is not a function")
const diagnostic = await error(`return await Promise.settle(() => 1)`)
expect(diagnostic.message).toContain("Promise.settle is not a function")
})
})
describe("Promise.try", () => {
test("runs the function now with its arguments; a throw rejects, a return fulfils, a promise or thenable is adopted", async () => {
expect(
await value(`
const log = []
const sum = Promise.try((a, b) => { log.push("ran"); return a + b }, 1, 2)
log.push("after")
let caught
try { await Promise.try(() => { throw new RangeError("boom") }) } catch (e) { caught = e.constructor.name }
return [log, sum instanceof Promise, await sum, caught, await Promise.try(async () => 5), await Promise.try(() => ({ then: (r) => r("thenable") }))]
`),
).toEqual([["ran", "after"], true, 3, "RangeError", 5, "thenable"])
expect((await error(`return Promise.try(5)`)).message).toContain(
"Promise.try expects a function, received a number.",
)
})
})
describe("Promise.withResolvers", () => {
test("returns a pending promise with resolvers that settle it once and adopt thenables", async () => {
expect(
await value(`
const first = Promise.withResolvers()
first.resolve(Promise.resolve("adopted"))
first.resolve("ignored")
const second = Promise.withResolvers()
second.reject(new Error("no"))
let caught
try { await second.promise } catch (e) { caught = e.message }
return [Object.keys(first), first.promise instanceof Promise, await first.promise, caught]
`),
).toEqual([["promise", "resolve", "reject"], true, "adopted", "no"])
})
})
+5 -5
View File
@@ -739,7 +739,7 @@ describe("JSDoc signatures in catalogs and search results", () => {
" },",
"}",
].join("\n")
const signature = `tools.constrained(input: ${type}): Promise<${type}>`
const signature = `tools.constrained(${type}): Promise<${type}>`
expect(runtime.catalog[0]?.signature).toBe(signature)
const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.constrained" })'))
expect(result.ok).toBe(true)
@@ -761,7 +761,7 @@ describe("JSDoc signatures in catalogs and search results", () => {
const item = items.find(({ path }) => path === "tools.github.list_issues")!
expect(item.signature).toBe(
[
"tools.github.list_issues(input: {",
"tools.github.list_issues({",
" /** Repository owner */",
" owner: string,",
" /** Cursor from the previous response's pageInfo */",
@@ -782,7 +782,7 @@ describe("JSDoc signatures in catalogs and search results", () => {
const item = items.find(({ path }) => path === "tools.orders.lookup")!
expect(item.signature).toBe(
[
"tools.orders.lookup(input: {",
"tools.orders.lookup({",
" /** Order identifier */",
" id: string,",
" verbose?: boolean,",
@@ -825,7 +825,7 @@ describe("non-identifier tool paths", () => {
test("catalog signatures use bracket notation for dashed tool names", () => {
expect(runtime.catalog[0]?.signature).toBe(
'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise<unknown>',
'tools.context7["resolve-library-id"]({\n query: string,\n libraryName: string,\n}): Promise<unknown>',
)
})
@@ -836,6 +836,6 @@ describe("non-identifier tool paths", () => {
const value = result.value as { items: Array<{ path: string; signature: string }> }
expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]')
expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"](input: {')
expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"]({')
})
})
+187
View File
@@ -947,6 +947,19 @@ describe("Map", () => {
})
describe("Set", () => {
test("forEach is live on Map and Set: deleted entries are skipped and added ones visited", async () => {
expect(
await value(`
const m = new Map([[1, "a"], [2, "b"]])
const s = new Set([1, 2])
const seen = []
m.forEach((v, k) => { seen.push(k); if (k === 1) { m.delete(2); m.set(3, "c") } })
s.forEach((v) => { seen.push(v); if (v === 1) { s.delete(2); s.add(3) } })
return seen
`),
).toEqual([1, 3, 1, 3])
})
test("add/has/delete/size with chaining", async () => {
expect(
await value(`
@@ -1165,6 +1178,147 @@ describe("TextEncoder and TextDecoder", () => {
})
})
describe("Iterator helpers", () => {
test("lazy helpers chain over collection iterators and generators, one source step per result", async () => {
expect(
await value(`
const pulled = []
function* naturals() { let n = 0; while (true) { pulled.push(n); yield n++ } }
const squares = naturals().map((n) => n * n).filter((n) => n % 2 === 0).drop(1).take(3)
const first = squares.next()
return {
first,
rest: squares.toArray(),
after: squares.next(),
pulled,
values: new Map([["a", 1], ["b", 2]]).values().map((v, i) => v * 10 + i).toArray(),
flat: [1, 2].values().flatMap((n) => [n, [n]]).toArray(),
entries: [...new Map([[1, 2]]).entries().map(([k, v]) => k + v)],
}
`),
).toEqual({
first: { value: 4, done: false },
rest: [16, 36],
after: { done: true },
pulled: [0, 1, 2, 3, 4, 5, 6],
values: [10, 21],
flat: [1, [1], 2, [2]],
entries: [3],
})
})
test("eager helpers consume the source and close it on early exit", async () => {
expect(
await value(`
const log = []
function* g() { try { yield 1; yield 2; yield 3 } finally { log.push("closed") } }
const seen = []
g().forEach((v, i) => seen.push([v, i]))
return {
seen,
sum: g().reduce((a, b) => a + b),
sumFrom: g().reduce((a, b) => a + b, 10),
some: g().some((v) => v === 2),
every: g().every((v) => v < 2),
find: g().find((v) => v > 1),
missing: g().find((v) => v > 5),
log,
}
`),
).toEqual({
seen: [
[1, 0],
[2, 1],
[3, 2],
],
sum: 6,
sumFrom: 16,
some: true,
every: false,
find: 2,
log: ["closed", "closed", "closed", "closed", "closed", "closed", "closed"],
})
})
test("a helper closes its source when a callback throws, on return(), and on early for...of exit", async () => {
expect(
await value(`
const log = []
function* g(name) { try { yield 1; yield 2 } finally { log.push(name) } }
const throwing = g("throw").map((v) => { if (v === 2) throw new Error("boom"); return v })
throwing.next()
let message
try { throwing.next() } catch (error) { message = error.message }
const returned = g("return").map((v) => v)
returned.next()
const closed = returned.return()
for (const v of g("loop").filter((v) => true)) break
return { message, afterThrow: throwing.next(), closed, afterReturn: returned.next(), log }
`),
).toEqual({
message: "boom",
afterThrow: { done: true },
closed: { done: true },
afterReturn: { done: true },
log: ["throw", "return", "loop"],
})
})
test("collection iterators have no return() and continue after an early exit, as in JS", async () => {
expect(
await value(`
const it = [1, 2, 3].values()
const found = it.some((v) => v === 2)
return [typeof it.return, found, it.next().value, typeof it.map(x => x).return]
`),
).toEqual(["undefined", true, 3, "function"])
})
test("Iterator.from and the abstract Iterator constructor", async () => {
expect(
await value(`
const it = [1].values()
let n = 0
const errors = []
for (const attempt of [() => Iterator(), () => new Iterator(), () => Iterator.from(5)]) {
try { attempt() } catch (error) { errors.push(error.name) }
}
return [
Iterator.from(it) === it,
Iterator.from("ab").toArray(),
Iterator.from([1, 2]).map((v) => v * 2).toArray(),
Iterator.from({ next: () => ({ done: n > 1, value: n++ }) }).toArray(),
it instanceof Iterator,
errors,
]
`),
).toEqual([true, ["a", "b"], [2, 4], [0, 1], true, ["TypeError", "TypeError", "TypeError"]])
})
test("argument validation", async () => {
const cases: Array<[string, string]> = [
["[1].values().map(1)", "Iterator.prototype.map expects a function callback."],
["[1].values().take(-1)", "Iterator.prototype.take expects a non-negative count, received -1."],
["[1].values().drop()", "Iterator.prototype.drop expects a non-negative count, received NaN."],
[
"[1].values().flatMap((v) => 'ab').toArray()",
"Iterator.prototype.flatMap expects an iterable or iterator, received a string.",
],
["[].values().reduce((a, b) => a)", "Iterator.prototype.reduce of an empty iterator with no initial value."],
]
for (const [code, message] of cases) {
expect((await error(`return ${code}`)).message).toContain(message)
}
expect(
await error(`
function* g() { while (true) yield 1 }
const it = g().map(() => it.next())
return it.next()
`),
).toMatchObject({ message: expect.stringContaining("Iterator helper is already running.") })
})
})
describe("built-in iterators", () => {
test("keys/values/entries and [Symbol.iterator] step with next() and stay live", async () => {
expect(
@@ -1267,6 +1421,39 @@ describe("built-in iterators", () => {
})
})
describe("Object.prototype.toString", () => {
test("reports the built-in kind it is inherited by, as JS does through Symbol.toStringTag", async () => {
expect(
await value(`
return [
new Map().toString(), new Set().toString(), new Headers().toString(), Promise.resolve(1).toString(),
[1].values().toString(), ({}).toString(), String(new Map()), String(Promise.resolve(1)),
\`\${new Set([1])}\`, [new Map()] + "", new Map() == "[object Map]",
]
`),
).toEqual([
"[object Map]",
"[object Set]",
"[object Headers]",
"[object Promise]",
"[object Iterator]",
"[object Object]",
"[object Map]",
"[object Promise]",
"[object Set]",
"[object Map]",
true,
])
})
})
describe("console.log of errors", () => {
test("prints name and message, nested too", async () => {
const result = await run(`console.log(new Error("boom"), { e: new RangeError("r") })`)
expect(result.logs).toEqual(['Error: boom {"e":RangeError: r}'])
})
})
describe("toLocaleString", () => {
test("numbers and dates format as en-US in UTC; everything else falls back to toString", async () => {
expect(
+4 -2
View File
@@ -1,6 +1,6 @@
{
"revision": "250f204f23a9249ff204be2baec29600faae7b75",
"directories": ["built-ins/Array/prototype", "language/statements"],
"directories": ["built-ins/Array/prototype", "language/statements", "built-ins/Iterator"],
"harness": ["assert.js", "sta.js", "compareArray.js", "doneprintHandle.js"],
"flags": ["module", "raw", "noStrict"],
"boundaries": {
@@ -101,6 +101,8 @@
"legacy-regexp",
"__proto__",
"__getter__",
"__setter__"
"__setter__",
"iterator-sequencing",
"joint-iteration"
]
}
+14 -11
View File
@@ -9,7 +9,7 @@ import type { Interpreter } from "../../src/interpreter/interpreter.js"
import { Throw } from "../../src/interpreter/model.js"
import { createErrorValue } from "../../src/interpreter/intrinsics.js"
import { constructor, fn, methods } from "../../src/interpreter/native.js"
import { Callable, define, get, hidden, Arr, Fn, Obj } from "../../src/interpreter/objects.js"
import { Callable, define, get, hidden, Arr, Fn, Obj, type Value } from "../../src/interpreter/objects.js"
import { ToolRuntime } from "../../src/tool-runtime.js"
export const root = import.meta.dir
@@ -64,10 +64,7 @@ export const run = async (file: string): Promise<Outcome> => {
return { status: "pass" }
}
const harness = <R>(
ctx: Interpreter<R>,
onDone: (error: unknown) => void,
): ReadonlyArray<readonly [string, unknown]> => {
const harness = <R>(ctx: Interpreter<R>, onDone: (error: Value) => void): ReadonlyArray<readonly [string, Value]> => {
const builtins = ctx.builtins
const test262Prototype = new Obj(builtins.Object)
define(test262Prototype, "name", "Test262Error", hidden)
@@ -90,7 +87,7 @@ const harness = <R>(
methods(builtins, compareArray, [["format", 1, (_, args) => show(args[0])]])
const assert = fn<R>(builtins, "assert", 2, (_, args) =>
args[0] === true
? Effect.void
? Effect.undefined
: fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])),
)
methods(builtins, assert, [
@@ -99,7 +96,7 @@ const harness = <R>(
3,
(_, args) =>
Object.is(args[0], args[1])
? Effect.void
? Effect.undefined
: fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`),
],
[
@@ -108,14 +105,14 @@ const harness = <R>(
(_, args) =>
Object.is(args[0], args[1])
? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`)
: Effect.void,
: Effect.undefined,
],
[
"compareArray",
3,
(_, args) =>
compare(args[0], args[1])
? Effect.void
? Effect.undefined
: fail(
`Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`,
),
@@ -132,7 +129,7 @@ const harness = <R>(
const thrown = materialize(ctx, Cause.squash(cause))
if (!(thrown instanceof Obj)) return fail(`${prefix(args[2])}Thrown value was not an object!`)
const actual = get(thrown, "constructor")
if (actual === args[0]) return Effect.void
if (actual === args[0]) return Effect.undefined
return fail(`${prefix(args[2])}Expected a ${expected} but got a ${show(actual)}`)
},
onSuccess: () =>
@@ -146,7 +143,13 @@ const harness = <R>(
["assert", assert],
["compareArray", compareArray],
["Test262Error", test262Error],
["$DONE", fn<R>(builtins, "$DONE", 1, (_, args) => onDone(args[0]))],
[
"$DONE",
fn<R>(builtins, "$DONE", 1, (_, args) => {
onDone(args[0])
return undefined
}),
],
[
"$DONOTEVALUATE",
fn<R>(builtins, "$DONOTEVALUATE", 0, () =>
+5 -144
View File
@@ -1,22 +1,17 @@
built-ins/Array/prototype/at/index-argument-tointeger.js # Array.at expects index to be a number.
built-ins/Array/prototype/at/index-non-numeric-argument-tointeger.js # Array.at expects index to be a number.
built-ins/Array/prototype/concat/S15.4.4.4_A2_T1.js # Array.prototype.concat called on incompatible receiver a data object.
built-ins/Array/prototype/concat/S15.4.4.4_A2_T2.js # Array.prototype.concat called on incompatible receiver a data object.
built-ins/Array/prototype/concat/S15.4.4.4_A3_T1.js # arr.hasOwnProperty("1") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/S15.4.4.4_A3_T2.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/S15.4.4.4_A3_T3.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/create-ctor-non-object.js # a.concat() throws a TypeError exception Expected a TypeError to be thrown but no exception was throw
built-ins/Array/prototype/copyWithin/coerced-values-end.js # Array.copyWithin expects end to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-start-change-target.js # Array.copyWithin expects start to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-start.js # Array.copyWithin expects start to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-target.js # Array.copyWithin expects target index to be a number.
built-ins/Array/prototype/copyWithin/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/every/15.4.4.16-7-6.js # res Expected SameValue(«true», «false») to be true
built-ins/Array/prototype/every/15.4.4.16-7-c-i-8.js # [, , , ].every(callbackfn) Expected SameValue(«true», «false») to be true
built-ins/Array/prototype/every/15.4.4.16-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/every/15.4.4.16-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -24,11 +19,9 @@ built-ins/Array/prototype/every/15.4.4.16-8-5.js # … cannot be constructed: u
built-ins/Array/prototype/every/15.4.4.16-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/fill/coerced-indexes.js # Array.fill expects start to be a number.
built-ins/Array/prototype/fill/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/fill/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/filter/15.4.4.20-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-10-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/filter/15.4.4.20-6-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-6-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-6-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -39,12 +32,10 @@ built-ins/Array/prototype/filter/15.4.4.20-6-8.js # … cannot be constructed:
built-ins/Array/prototype/filter/15.4.4.20-9-6.js # resArr.length Expected SameValue(«3», «4») to be true
built-ins/Array/prototype/filter/15.4.4.20-9-c-i-8.js # newArr.length Expected SameValue(«0», «1») to be true
built-ins/Array/prototype/filter/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/flat/non-numeric-depth-should-not-throw.js # Array.flat expects depth to be a number.
built-ins/Array/prototype/flat/non-object-ctor-throws.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/forEach/15.4.4.18-7-5.js # callCnt Expected SameValue(«4», «5») to be true
built-ins/Array/prototype/forEach/15.4.4.18-7-c-i-8.js # testResult !== true
built-ins/Array/prototype/forEach/15.4.4.18-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-12.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/forEach/15.4.4.18-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -53,61 +44,33 @@ built-ins/Array/prototype/forEach/15.4.4.18-8-6.js # … cannot be constructed:
built-ins/Array/prototype/forEach/15.4.4.18-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/includes/length-zero-returns-false.js # Array.includes expects a value and optional start index.
built-ins/Array/prototype/includes/no-arg.js # Array.includes expects a value and optional start index.
built-ins/Array/prototype/includes/return-abrupt-tointeger-fromindex.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/includes/tointeger-fromindex.js # Array.includes expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-1.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-15.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-16.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-17.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-18.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-19.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-20.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-21.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-22.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-23.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-24.js # toStringAccessed
built-ins/Array/prototype/indexOf/15.4.4.14-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/indexOf/15.4.4.14-5-3.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-5.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-7.js # Array assignment result contains a circular value.
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-3.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-6.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-b-i-7.js # [, , , ].indexOf(true) Expected SameValue(«-1», «0») to be true
built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/join/S15.4.4.5_A1.2_T2.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A2_T1.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A2_T4.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T2.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A3.2_T2.js # x.join() must return "*" Expected SameValue(«"[object Object]"», «"*"») to be true
built-ins/Array/prototype/join/S15.4.4.5_A4_T3.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A5_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.join() === "0,1". Actual: 0,
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-15.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-16.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-17.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-18.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-19.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-20.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-21.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-22.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-23.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-24.js # toStringAccessed
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js # a.lastIndexOf(2,undefined) Expected SameValue(«1», «-1») to be true
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-7.js # Array assignment result contains a circular value.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-3.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-6.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-b-i-7.js # [, , , ].lastIndexOf(true) Expected SameValue(«-1», «0») to be true
built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/map/15.4.4.19-8-6.js # resArr[4] Expected SameValue(«undefined», «1») to be true
built-ins/Array/prototype/map/15.4.4.19-8-c-i-8.js # newArr[1] Expected SameValue(«13», «true») to be true
built-ins/Array/prototype/map/15.4.4.19-9-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/map/15.4.4.19-9-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/map/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/pop/S15.4.4.6_A2_T1.js # Array.prototype.pop called on incompatible receiver a data object.
built-ins/Array/prototype/pop/S15.4.4.6_A2_T4.js # Array.prototype.pop called on incompatible receiver a data object.
@@ -127,7 +90,6 @@ built-ins/Array/prototype/reduce/15.4.4.21-10-3.js # … cannot be constructed:
built-ins/Array/prototype/reduce/15.4.4.21-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/reduce/15.4.4.21-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -151,7 +113,6 @@ built-ins/Array/prototype/reduceRight/15.4.4.22-10-3.js # … cannot be constru
built-ins/Array/prototype/reduceRight/15.4.4.22-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/reduceRight/15.4.4.22-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -181,7 +142,6 @@ built-ins/Array/prototype/shift/S15.4.4.9_A2_T5.js # Array.prototype.shift call
built-ins/Array/prototype/shift/S15.4.4.9_A3_T3.js # Array.prototype.shift called on incompatible receiver a data object.
built-ins/Array/prototype/shift/S15.4.4.9_A4_T1.js # #2: Array.prototype[1] = 1; x = [0]; x.length = 2; x.shift(); x[0] === 1. Actual: undefined
built-ins/Array/prototype/shift/S15.4.4.9_A4_T2.js # Array.prototype.shift called on incompatible receiver a data object.
built-ins/Array/prototype/slice/S15.4.4.10_A2.1_T5.js # Array.slice expects start to be a number.
built-ins/Array/prototype/slice/S15.4.4.10_A2.2_T5.js # Array.slice expects end to be a number.
built-ins/Array/prototype/slice/S15.4.4.10_A2_T1.js # Array.prototype.slice called on incompatible receiver a data object.
built-ins/Array/prototype/slice/S15.4.4.10_A2_T2.js # Array.prototype.slice called on incompatible receiver a data object.
@@ -197,7 +157,6 @@ built-ins/Array/prototype/slice/create-ctor-non-object.js # null value Expected
built-ins/Array/prototype/some/15.4.4.17-7-6.js # res Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/some/15.4.4.17-7-c-i-8.js # [, ].some(callbackfn) !== true
built-ins/Array/prototype/some/15.4.4.17-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/some/15.4.4.17-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -213,7 +172,6 @@ built-ins/Array/prototype/sort/S15.4.4.11_A4_T3.js # Array.prototype.sort calle
built-ins/Array/prototype/sort/S15.4.4.11_A6_T2.js # Array.prototype.sort called on incompatible receiver a data object.
built-ins/Array/prototype/sort/bug_596_1.js # #1: [object, object].sort(); counter < 2. Actual: 0
built-ins/Array/prototype/sort/precise-prototype-element.js # Expected SameValue(«undefined», «4») to be true
built-ins/Array/prototype/splice/S15.4.4.12_A2.1_T5.js # Array.splice expects start to be a number.
built-ins/Array/prototype/splice/S15.4.4.12_A2.2_T5.js # Array.splice expects delete count to be a number.
built-ins/Array/prototype/splice/S15.4.4.12_A2_T1.js # Array.prototype.splice called on incompatible receiver a data object.
built-ins/Array/prototype/splice/S15.4.4.12_A2_T2.js # Array.prototype.splice called on incompatible receiver a data object.
@@ -236,53 +194,13 @@ built-ins/Array/prototype/unshift/S15.4.4.13_A2_T3.js # Array.prototype.unshift
built-ins/Array/prototype/unshift/S15.4.4.13_A3_T2.js # Array.prototype.unshift called on incompatible receiver a data object.
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T1.js # Array.prototype.unshift called on incompatible receiver a data object.
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T2.js # #3: Array.prototype[0] = 1; x = []; x.length = 1; x.unshift(0); x[1] === 1. Actual: undefined
language/statements/async-generator/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
built-ins/Iterator/from/return-method-returns-iterator-result.js # Iterator next must be a function.
built-ins/Iterator/prototype/drop/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/drop/limit-tonumber.js # Iterator.prototype.drop expects a non-negative count, received NaN.
built-ins/Iterator/prototype/take/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/take/limit-tonumber.js # Iterator.prototype.take expects a non-negative count, received NaN.
language/statements/async-generator/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret"
language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
@@ -290,7 +208,6 @@ language/statements/for-await-of/async-func-decl-dstr-obj-empty-num.js # TypeEr
language/statements/for-await-of/async-func-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-func-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-func-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-gen-decl-dstr-array-elem-iter-rtrn-close-null.js # "Promise incorrectly fulfilled."
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
@@ -298,10 +215,7 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeE
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-in/head-lhs-member.js # Unsupported for...in binding.
language/statements/for-of/dstr/array-elem-iter-rtrn-close-err.js # Iterator next must be a function.
language/statements/for-of/dstr/array-elem-iter-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-of/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/statements/for-of/dstr/array-elem-trlg-iter-list-rtrn-close-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/for-of/dstr/array-elem-trlg-iter-list-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-of/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/statements/for-of/dstr/array-elem-trlg-iter-rest-rtrn-close-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/for-of/dstr/array-elem-trlg-iter-rest-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all
@@ -335,63 +249,10 @@ language/statements/function/S13.2_A1_T1.js # #1: __func.prototype !== undefine
language/statements/function/S13.2_A1_T2.js # #1: __func.prototype !== undefined
language/statements/function/S13.2_A4_T1.js # Unknown identifier '…'.
language/statements/function/S13.2_A4_T2.js # #1: typeof __gunc.prototype === '…'. Actual: typeof __gunc.prototype ===undefined
language/statements/function/S13_A3_T1.js # Unknown identifier '…'.
language/statements/function/S13_A6_T1.js # Identifier '…' has already been declared.
language/statements/function/S14_A2.js # Unknown identifier '…'.
language/statements/function/S14_A5_T1.js # Identifier '…' has already been declared.
language/statements/function/S14_A5_T2.js # Identifier '…' has already been declared.
language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/has-instance.js # The right-hand side of '…' has no '…' object.
language/statements/generators/prototype-typeof.js # Expected SameValue(«"undefined"», «"object"») to be true
language/statements/generators/prototype-uniqueness.js # Expected true but got false
language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected.
language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/return/S12.9_A1_T1.js # expected SyntaxError but the program ran
+6 -3
View File
@@ -106,9 +106,12 @@ describe("callable namespaces", () => {
const diagnostic = await failure(runtime, `return await tools.issues.missing({})`)
expect(diagnostic.kind).toBe("UnknownTool")
expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
expect(diagnostic.suggestions).toEqual([
"The tool may have been removed or renamed. Use search to find available tools.",
])
expect(diagnostic.suggestions).toEqual(["Use search to find available tools."])
})
test("an unknown tool names the closest match", async () => {
const diagnostic = await failure(runtime, `return await tools.issues["get-list"]({})`)
expect(diagnostic.message).toBe("Unknown tool 'issues.get-list'. Did you mean tools.issues.list?")
})
test("a namespace without its own tool stays non-callable", async () => {
@@ -234,6 +234,44 @@ describe("var semantics beyond Test262", () => {
})
})
describe("function declarations and expressions", () => {
test("the last of repeated function declarations wins, and a var may share the name", async () => {
expect(
await value(`
function f() { return 1 }
const first = f()
function f() { return 2 }
var g = 1
function g() { return 3 }
return [first, f(), typeof g]
`),
).toEqual([2, 2, "number"])
})
test("a named function expression sees its own name inside its body, read-only", async () => {
expect(
await value(`
const fact = function inner(n) { return n <= 1 ? 1 : n * inner(n - 1) }
const reassign = function inner() { inner = 5 }
let failure
try { reassign() } catch (error) { failure = error.constructor.name }
return [fact(4), typeof inner, failure]
`),
).toEqual([24, "undefined", "TypeError"])
})
test("generator functions have their own prototype", async () => {
expect(
await value(`
function* g() {}
async function* ag() {}
function f() {}
return [g() instanceof g, ag() instanceof ag, g.prototype === ag.prototype, typeof g.prototype, typeof f.prototype]
`),
).toEqual([true, true, false, "object", "undefined"])
})
})
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")
+11 -12
View File
@@ -254,7 +254,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.banner.text": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة",
"go.meta.description":
"يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"يبلغ سعر Go $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@@ -263,9 +263,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "اشترك في Go",
"go.cta.price": "$10/شهر",
"go.cta.promo": "$5 للشهر الأول",
"go.pricing.body":
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"استخدمه مع أي وكيل. $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية",
"go.graph.go": "Go",
@@ -296,20 +295,20 @@ export const dict = {
"go.testimonials.frank.quote": "أتمنى لو كنت لا أزال في Nvidia.",
"go.problem.title": "ما المشكلة التي يحلها Go؟",
"go.problem.body":
"نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة: $5 للشهر الأول، ثم $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.",
"نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة بسعر $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.",
"go.problem.subtitle": " ",
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
"go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4": "مجموعة منسقة من النماذج المختبرة للبرمجة الوكيلة",
"go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.body": "يبلغ سعر Go $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا",
"go.how.step1.beforeLink": "اتبع",
"go.how.step1.link": "تعليمات الإعداد",
"go.how.step2.title": "اشترك في Go",
"go.how.step2.link": "$5 للشهر الأول",
"go.how.step2.afterLink": "ثم $10/شهر مع حدود سخية",
"go.how.step2.link": "$10/شهر",
"go.how.step2.afterLink": "مع حدود سخية",
"go.how.step3.title": "ابدأ البرمجة",
"go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر",
"go.privacy.title": "خصوصيتك مهمة بالنسبة لنا",
@@ -325,11 +324,11 @@ export const dict = {
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
"go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3":
"لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.",
"لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبلغ سعر Go $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.",
"go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
"go.faq.a4.p1.afterPricing": "ثم $10/شهر مع حدود سخية.",
"go.faq.a4.p1.pricingLink": "$10/شهر",
"go.faq.a4.p1.afterPricing": "مع حدود سخية.",
"go.faq.a4.p2.beforeAccount": "يمكنك إدارة اشتراكك في",
"go.faq.a4.p2.accountLink": "حسابك",
"go.faq.a4.p3": "ألغِ في أي وقت.",
@@ -671,8 +670,8 @@ export const dict = {
"workspace.lite.other.message":
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.description":
"يبدأ OpenCode Go بسعر {{price}}، ثم $10/شهر، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.",
"workspace.lite.promo.price": "$5 للشهر الأول",
"يبلغ سعر OpenCode Go {{price}}، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.",
"workspace.lite.promo.price": "$10/شهر",
"workspace.lite.promo.modelsTitle": "ما يتضمنه",
"workspace.lite.promo.footer":
"صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.",

Some files were not shown because too many files have changed in this diff Show More