mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-21 08:07:34 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7d831d21 | ||
|
|
1233a112d2 | ||
|
|
cbdd1f66da | ||
|
|
788eb0fa29 | ||
|
|
58fcad77a8 | ||
|
|
c555559ac1 | ||
|
|
e3a3fa7108 | ||
|
|
6238af397e | ||
|
|
7ffd75faf6 | ||
|
|
1f8ab95695 | ||
|
|
62dc1f7696 | ||
|
|
66f10ab7bf | ||
|
|
1ca8f63a79 | ||
|
|
03bcdd580d | ||
|
|
cc502f7e5f | ||
|
|
64ce8771c0 | ||
|
|
702a73d91a | ||
|
|
991b727eb8 | ||
|
|
ba342ce227 | ||
|
|
1464545665 | ||
|
|
fecacc9e68 | ||
|
|
1f73b4806b | ||
|
|
da2ce02596 | ||
|
|
25f35dcfb8 |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -175,7 +175,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -214,7 +214,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -250,7 +250,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -294,7 +294,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -318,7 +318,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -406,7 +406,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -492,7 +492,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -508,7 +508,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -527,7 +527,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -554,7 +554,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -569,7 +569,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -608,7 +608,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -638,7 +638,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -653,7 +653,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -677,7 +677,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -698,7 +698,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -720,7 +720,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -755,7 +755,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -775,7 +775,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -809,7 +809,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -828,7 +828,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -874,7 +874,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -888,7 +888,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -923,7 +923,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -958,7 +958,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -991,7 +991,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1032,7 +1032,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -19,6 +19,13 @@ export function mountBrowserPane() {
|
||||
session: "Alpha",
|
||||
mounted: true,
|
||||
visible: true,
|
||||
url: undefined as string | undefined,
|
||||
loading: false,
|
||||
generation: 0,
|
||||
delayNavigation: false,
|
||||
pendingURL: undefined as string | undefined,
|
||||
loadErrors: {} as Record<string, string | undefined>,
|
||||
error: undefined as string | undefined,
|
||||
layouts: {} as Record<string, BrowserPaneLayout | undefined>,
|
||||
})
|
||||
const tabs = ["Alpha", "Beta"].map((name) => ({
|
||||
@@ -47,13 +54,33 @@ export function mountBrowserPane() {
|
||||
opened: () => !!registrations.get(store.session),
|
||||
state: () => ({ tabs: tabs.filter((tab) => tab.title === store.session), focusedTabID: null }),
|
||||
tabs: () => tabs.filter((tab) => tab.title === store.session),
|
||||
active: () => tabs.find((tab) => tab.title === store.session) ?? tabs[0],
|
||||
active: () => {
|
||||
const tab = tabs.find((tab) => tab.title === store.session) ?? tabs[0]
|
||||
return {
|
||||
...tab,
|
||||
url: store.url ?? tab.url,
|
||||
loading: store.loading,
|
||||
generation: store.generation,
|
||||
loadError: store.loadErrors[store.session],
|
||||
}
|
||||
},
|
||||
registration: () => registrations.get(store.session),
|
||||
error: () => undefined,
|
||||
error: () => store.error ?? (store.loadErrors[store.session] ? "Request failed" : undefined),
|
||||
suspended: () => false,
|
||||
close: () => undefined,
|
||||
open: () => undefined,
|
||||
command: () => undefined,
|
||||
command: (command) => {
|
||||
setStore("error", undefined)
|
||||
if (command.type === "navigate" || command.type === "reload") setStore("loadErrors", store.session, undefined)
|
||||
if (command.type === "navigate") {
|
||||
if (store.delayNavigation) {
|
||||
setStore("pendingURL", command.url)
|
||||
return
|
||||
}
|
||||
setStore({ url: command.url, loading: false, generation: store.generation + 1 })
|
||||
}
|
||||
if (command.type === "stop") setStore("loading", false)
|
||||
},
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -64,6 +91,32 @@ export function mountBrowserPane() {
|
||||
{(name) => <button onClick={() => setStore({ session: name, mounted: name !== "Empty" })}>{name}</button>}
|
||||
</For>
|
||||
<button onClick={() => setStore("mounted", false)}>Unmount pane</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: false })}>Blank page</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: true })}>Loading page</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({ loading: true, generation: store.generation + 1, loadErrors: { [store.session]: undefined } })
|
||||
}
|
||||
>
|
||||
Load current page
|
||||
</button>
|
||||
<button onClick={() => setStore("loadErrors", store.session, "ERR_CONNECTION_REFUSED")}>Failed page</button>
|
||||
<button onClick={() => setStore("delayNavigation", true)}>Delay navigation</button>
|
||||
<button onClick={() => setStore({ error: "ERR_BLOCKED_BY_CLIENT", pendingURL: undefined })}>
|
||||
Block navigation
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({
|
||||
url: store.pendingURL,
|
||||
pendingURL: undefined,
|
||||
loading: false,
|
||||
generation: store.generation + 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Complete navigation
|
||||
</button>
|
||||
<button onClick={() => setStore("visible", (visible) => !visible)}>Toggle Review tab</button>
|
||||
</nav>
|
||||
<div style={{ width: "640px", height: "360px", border: "1px solid #555" }}>
|
||||
|
||||
@@ -57,3 +57,153 @@ story("hides the native view immediately while the pane stays mounted", async ({
|
||||
await toggle.click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText('Or prompt "Open in the app browser"', { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
story("keeps Stop available and hides the empty state while a blank page loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Loading page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await root.getByRole("button", { name: "Stop", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("keeps the submitted URL visible until the browser reports navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await address.fill("https://unsubmitted.example/")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("restores the current URL when a submitted navigation is blocked", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://blocked.example/")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByText("ERR_BLOCKED_BY_CLIENT", { exact: true })).toBeVisible()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Check the URL and your connection, then try again.", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Request failed", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("textbox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
"https://alpha.example/",
|
||||
)
|
||||
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Alpha", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
|
||||
await root.getByRole("button", { name: "Reload", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("returns a failed tab to the empty state when an empty URL is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("")
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("selects the full URL when the address field gains focus", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
await address.pressSequentially("https://example.com/")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await address.focus()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
await address.press("ArrowRight")
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
})
|
||||
|
||||
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("keeps the current page and restores its URL when an empty address is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -65,14 +65,14 @@
|
||||
}
|
||||
|
||||
/* Keep the 1000px conversation intact, with 320px for the summary and its gutters.
|
||||
Move only as far as needed; wider panels already have enough space in the margin. */
|
||||
@container session-chat (min-width: 1320px) {
|
||||
When they overlap, center the conversation in the space that remains beside the summary. */
|
||||
@container session-chat (min-width: 1320px) and (width < 1640px) {
|
||||
[data-slot="session-chat-panel"][data-summary-open="true"]
|
||||
:is([data-timeline-virtual-content], [data-component="session-composer-dock"] > div) {
|
||||
--session-summary-translate: min(0px, calc(50cqi - 820px));
|
||||
--session-summary-translate: -160px;
|
||||
|
||||
&:dir(rtl) {
|
||||
--session-summary-translate: max(0px, calc(820px - 50cqi));
|
||||
--session-summary-translate: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,6 +906,10 @@ export const dict = {
|
||||
"session.browser.replaced": "Browser control moved to another desktop window.",
|
||||
"session.browser.suspended": "Browser suspended. Interact with this session to reconnect.",
|
||||
"session.browser.address.placeholder": "Enter URL",
|
||||
"session.browser.empty.title": "Enter URL",
|
||||
"session.browser.empty.description": 'Or prompt "Open in the app browser"',
|
||||
"session.browser.failed.title": "URL can't be reached",
|
||||
"session.browser.failed.description": "Check the URL and your connection, then try again.",
|
||||
|
||||
"titlebar.update": "Update",
|
||||
"titlebar.tabs": "Tabs",
|
||||
|
||||
@@ -20,13 +20,18 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const command = useCommand()
|
||||
const state = props.browser.active
|
||||
const address = () => (state()?.url === "about:blank" ? "" : (state()?.url ?? ""))
|
||||
const failed = () => !!state()?.loadError
|
||||
const registration = props.browser.registration
|
||||
const button = { variant: "ghost", size: "large" } as const
|
||||
const [store, setStore] = createStore({
|
||||
address: "",
|
||||
editing: false,
|
||||
submitted: false,
|
||||
// A submitted navigation the browser has not reported yet; keeps the empty state hidden meanwhile.
|
||||
navigating: false,
|
||||
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||
})
|
||||
const empty = () => !address() && !state()?.loading && !store.navigating
|
||||
let surface: HTMLDivElement | undefined
|
||||
let addressDisplay: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
@@ -43,7 +48,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
title: language.t("command.browser.reload"),
|
||||
category: language.t("command.category.view"),
|
||||
keybind: "f5",
|
||||
disabled: !props.visible || !state(),
|
||||
disabled: !props.visible || !address(),
|
||||
onSelect: () => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: "reload", tabID: tab.id })
|
||||
@@ -71,7 +76,9 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const top = Math.round(rect.top * zoom)
|
||||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
const visible = props.visible && store.visible && !dialog.active && !covered(rect)
|
||||
// The desktop page hides blank and loading documents itself; only hide here
|
||||
// while the pane shows its own empty or failed state over the surface.
|
||||
const visible = props.visible && store.visible && !empty() && !failed() && !dialog.active && !covered(rect)
|
||||
// The cutout exposes the app backdrop outside the rounded Review card,
|
||||
// not the browser surface inside it.
|
||||
const color = getComputedStyle(
|
||||
@@ -107,7 +114,25 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
if (frame === undefined) frame = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
createEffect(() => !store.editing && setStore("address", address()))
|
||||
createEffect(on([() => state()?.id, address], () => !store.editing && setStore("address", address())))
|
||||
// Any reported movement, including a rejected or blocked request, ends the submitted navigation.
|
||||
createEffect(
|
||||
on(
|
||||
[() => state()?.id, () => state()?.generation, () => state()?.loading, () => props.browser.error()],
|
||||
() => setStore("navigating", false),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
// A blocked or rejected submission leaves the page where it was; show that page's URL again.
|
||||
createEffect(
|
||||
on(
|
||||
() => props.browser.error(),
|
||||
(error) => {
|
||||
if (error && !store.editing) setStore("address", address())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(
|
||||
on(registration, (current) => {
|
||||
// Session routes can change before this pane unmounts. Hide the registration
|
||||
@@ -123,6 +148,8 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
() => store.visible,
|
||||
() => props.visible,
|
||||
() => state()?.id,
|
||||
empty,
|
||||
failed,
|
||||
registration,
|
||||
],
|
||||
() => {
|
||||
@@ -157,7 +184,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
|
||||
return (
|
||||
<aside id="browser-panel" class="relative size-full min-w-0 overflow-hidden bg-v2-background-bg-base flex flex-col">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-2 border-b border-v2-border-border-muted">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-3 border-b border-v2-border-border-muted">
|
||||
<For each={["back", "forward"] as const}>
|
||||
{(direction) => (
|
||||
<Tooltip placement="top" value={language.t(direction === "back" ? "common.goBack" : "common.goForward")}>
|
||||
@@ -193,7 +220,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
>
|
||||
<IconButton
|
||||
{...button}
|
||||
disabled={!state()}
|
||||
disabled={!state()?.loading && !address()}
|
||||
aria-label={language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
@@ -212,8 +239,13 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const tab = state()
|
||||
if (tab && store.address.trim())
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: store.address })
|
||||
const url = store.address.trim()
|
||||
if (!tab) return
|
||||
if (url || failed()) {
|
||||
setStore({ submitted: true, address: url, navigating: true })
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: url || "about:blank" })
|
||||
}
|
||||
event.currentTarget.querySelector("input")?.blur()
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -224,8 +256,14 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
disabled={!state()}
|
||||
placeholder={language.t("session.browser.address.placeholder")}
|
||||
aria-label={language.t("session.browser.address")}
|
||||
onFocus={() => setStore("editing", true)}
|
||||
onBlur={() => setStore({ editing: false, address: address() })}
|
||||
onFocus={(event) => {
|
||||
setStore("editing", true)
|
||||
event.currentTarget.select()
|
||||
}}
|
||||
onClick={(event) => event.currentTarget.select()}
|
||||
onBlur={() =>
|
||||
setStore({ editing: false, address: store.submitted ? store.address : address(), submitted: false })
|
||||
}
|
||||
onInput={(event) => setStore("address", event.currentTarget.value)}
|
||||
onScroll={(event) => {
|
||||
if (addressDisplay) addressDisplay.scrollLeft = event.currentTarget.scrollLeft
|
||||
@@ -243,12 +281,27 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Show when={props.browser.error()}>
|
||||
<Show when={props.browser.error() && !failed()}>
|
||||
<div class="shrink-0 px-3 py-1.5 text-12-regular text-text-danger-base border-b border-v2-border-border-muted">
|
||||
{props.browser.error()}
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={surface} class="min-h-0 flex-1 bg-v2-background-bg-base flex items-center justify-center">
|
||||
<Show when={(empty() || failed()) && !props.browser.suspended()}>
|
||||
{/* Add the 40px toolbar to the file empty state's 160px bottom padding to align their centers. */}
|
||||
<div
|
||||
dir="auto"
|
||||
class="flex size-full flex-col items-center justify-center gap-2 p-6 pb-[200px] text-center text-text-weak"
|
||||
>
|
||||
<Icon name="globe" size="large" class="mb-2 shrink-0" />
|
||||
<div class="text-[13px] font-medium leading-[var(--line-height-compact)] text-text-strong">
|
||||
{language.t(failed() ? "session.browser.failed.title" : "session.browser.empty.title")}
|
||||
</div>
|
||||
<div class="text-13-regular leading-[var(--line-height-base)]">
|
||||
{language.t(failed() ? "session.browser.failed.description" : "session.browser.empty.description")}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.browser.suspended()}>
|
||||
<p class="px-6 text-center text-13-regular text-v2-text-text-subtle" role="status">
|
||||
{language.t("session.browser.suspended")}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { children, createMemo, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Keybind } from "@opencode/ui/keybind"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { Tabs } from "@opencode/ui/tabs"
|
||||
@@ -69,9 +71,19 @@ export function SortableTab(props: {
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<Tabs.CloseButton
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.onTabClose(props.tab)}
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.onTabClose(props.tab)
|
||||
}}
|
||||
icon={<Icon name="xmark-small" />}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-titlebar-tab] {
|
||||
[data-titlebar-tab],
|
||||
[data-titlebar-tab-action] {
|
||||
--tab-base: var(--v2-background-bg-deep);
|
||||
--tab-overlay: transparent;
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
@@ -68,6 +69,14 @@
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:hover, :focus-visible) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:active, [data-state="pressed"]) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] {
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
@@ -351,9 +351,10 @@ export function Titlebar(props: {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-home"
|
||||
data-state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base data-[state=pressed]:bg-v2-background-bg-layer-02 data-[state=pressed]:text-v2-text-text-base"
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base data-[state=pressed]:text-v2-text-text-base"
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
@@ -650,8 +651,9 @@ export function Titlebar(props: {
|
||||
{homeButton(true)}
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base"
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
||||
import { Effect } from "effect"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { getErrorReported } from "effect/Runtime"
|
||||
import { Commands } from "./commands/commands"
|
||||
import { Runtime } from "./framework/runtime"
|
||||
import { Observability } from "@opencode/util/observability"
|
||||
@@ -130,5 +131,15 @@ Effect.gen(function* () {
|
||||
Effect.provide(NodeServices.layer),
|
||||
Effect.scoped,
|
||||
Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),
|
||||
NodeRuntime.runMain,
|
||||
// runMain's default reporter logs the fatal cause to stdout. Write it to stderr instead: the
|
||||
// desktop and `Service.ensure` only capture stderr from `serve --service`, so this is the only
|
||||
// channel through which a startup failure's reason reaches the user.
|
||||
Effect.tapCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return
|
||||
if (!getErrorReported(Cause.squash(cause))) return
|
||||
process.stderr.write(Cause.pretty(cause) + "\n")
|
||||
}),
|
||||
),
|
||||
NodeRuntime.runMain({ disableErrorReporting: true }),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
and Map, RegExp, and generators serialize as `{}`. A bare `undefined` result is `null`.
|
||||
Tool results come back the way `JSON.parse(JSON.stringify(result))` would. The table, where a value cannot
|
||||
be JSON but what the program meant is clear: a promise is awaited (a rejection fails the program), a Set
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own }`, a
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own enumerable }`, a
|
||||
Uint8Array is rejected with a hint to encode as text, and own `__proto__` keys are dropped so merging tool
|
||||
inputs or results cannot replace a prototype. In-program `JSON.stringify` keeps JS behavior except for the
|
||||
Error form and a promise, which is a `TypeError` with an await hint rather than a silent `{}`.
|
||||
@@ -38,8 +38,11 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
10,000,000 elements (`Array(n)`, `length =`, `Array.from`, `split`, `matchAll`, `concat`, `flat`; below the JS
|
||||
maximum of 2^32 - 1), and 10,000 pending promises at once. Exceeding one throws a `RangeError`. A single regular
|
||||
expression match can still run long on a pathological pattern; the host regex engine has no interrupt hook.
|
||||
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
|
||||
rest parameter are accepted unless the program itself begins with `"use strict"`.
|
||||
- [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`.
|
||||
- [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at
|
||||
parse time. Without it, `yield` is an ordinary binding.
|
||||
- [ ] Duplicate parameter names in non-strict code throw when the function is called, instead of binding the last
|
||||
parameter as JavaScript does.
|
||||
|
||||
## Values and literals
|
||||
|
||||
@@ -94,7 +97,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `for`, `while`, and `do...while`.
|
||||
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, Headers, Uint8Arrays, built-in iterators, custom
|
||||
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references; other values iterate nothing.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
|
||||
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
|
||||
- [x] Unlabeled `break` and `continue`.
|
||||
- [x] `try`, `catch`, optional catch bindings, and `finally`.
|
||||
- [x] `throw` with arbitrary values.
|
||||
@@ -108,7 +112,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
## Functions and callbacks
|
||||
|
||||
- [x] Function declarations, function expressions, and arrow functions.
|
||||
- [x] Synchronous and `async` functions.
|
||||
- [x] Synchronous and `async` functions. A line break between `function` and the name is allowed, as in JavaScript;
|
||||
a line break between `async` and `function` is not an async function.
|
||||
- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters.
|
||||
- [x] A call depth limit of 10000: deeper nesting throws a catchable `RangeError: Maximum call stack size exceeded`
|
||||
at the overflowing call instead of running until the timeout. Callbacks invoked by built-ins count below the
|
||||
@@ -143,7 +148,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`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.
|
||||
- [ ] A line terminator between `async function` and the function name.
|
||||
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
|
||||
rather than at the call, so their errors are not thrown synchronously.
|
||||
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
|
||||
@@ -180,7 +184,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
on any other value throws a catchable `TypeError` naming the callee: other built-in functions such as `Number`
|
||||
say `new` is unsupported and point at the plain call, user-defined functions report the constructor gap below,
|
||||
and non-callable values are not constructors. Error constructors take the ES2022 options object, so
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present.
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present. `Error.isError`
|
||||
is true for every Error value.
|
||||
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
|
||||
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
|
||||
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
|
||||
@@ -270,8 +275,9 @@ 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`, `valueOf`,
|
||||
`hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
|
||||
- [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
|
||||
|
||||
@@ -285,12 +291,14 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`lastIndexOf`.
|
||||
- [x] Aggregation: `reduce` and `reduceRight`.
|
||||
- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, and `join`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, `join`, and `toLocaleString` (each element's
|
||||
`toLocaleString`, holes and nullish elements as empty strings).
|
||||
- [x] Mutation: `push`, `pop`, `shift`, `unshift`, `splice`, `fill`, and `copyWithin`.
|
||||
- [x] `keys`, `values`, `entries`, and `[Symbol.iterator]` (the same function as `values`) return live iterator objects
|
||||
with `next()` and `[Symbol.iterator]`, as in JS. Iterator objects are opaque references: they print as
|
||||
`[opaque reference]`, serialize to `{}`, and cannot be passed to extensions. Every built-in collection iterator
|
||||
shares one prototype, which is only observable through `getPrototypeOf`.
|
||||
shares one prototype. JavaScript gives each collection its own; the difference is not observable here because
|
||||
`Object.getPrototypeOf` is not exposed.
|
||||
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
||||
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
||||
- [x] `Array.prototype.toSpliced`.
|
||||
@@ -303,12 +311,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
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; `includes()`/`indexOf()` with no argument should search for
|
||||
`undefined`.
|
||||
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
|
||||
`undefined`; `includes()` still requires a value.
|
||||
|
||||
## Strings
|
||||
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`, and the `toLocaleLowerCase`/`toLocaleUpperCase`
|
||||
aliases, which ignore their locale argument and apply the default Unicode casing.
|
||||
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
|
||||
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
|
||||
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
|
||||
@@ -332,7 +341,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Coercion functions: `Number`, `parseInt`, and `parseFloat`.
|
||||
- [x] Number predicates/parsers: `Number.isInteger`, `Number.isFinite`, `Number.isNaN`, `Number.isSafeInteger`,
|
||||
`Number.parseInt`, and `Number.parseFloat`.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, and `valueOf`.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, `valueOf`, and `toLocaleString`, which
|
||||
always formats as `en-US` (`"1,234.5"`) so output does not depend on the host.
|
||||
- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`,
|
||||
`POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`.
|
||||
- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`.
|
||||
@@ -345,7 +355,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
use their epoch time) and reject opaque runtime references as data errors.
|
||||
- [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined`
|
||||
for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
|
||||
example `Math.sum is not a function.` Unknown `Promise` statics keep their descriptive error.
|
||||
example `Math.sum is not a function.` or `search(...).catch is not a function.` Unknown `Promise` statics keep
|
||||
their descriptive error.
|
||||
- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number
|
||||
elements without coercion.
|
||||
- [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
|
||||
@@ -379,6 +390,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`TimeClip` behavior.
|
||||
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
|
||||
- [x] `toDateString` and `toTimeString` in the host's local timezone.
|
||||
- [x] `toLocaleString`, `toLocaleDateString`, and `toLocaleTimeString` always format as `en-US` in UTC
|
||||
(`"1/1/1970, 12:00:00 AM"`) so output does not depend on the host.
|
||||
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
|
||||
- [ ] Date setters and multi-argument construction coerce object arguments through `valueOf`/`toString` and surface
|
||||
their throws.
|
||||
@@ -404,7 +417,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
|
||||
- [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`, and `forEach`.
|
||||
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, `forEach`, `getOrInsert`, and `getOrInsertComputed`.
|
||||
- [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()`
|
||||
@@ -425,7 +438,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams.
|
||||
- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`,
|
||||
`entries`, `[Symbol.iterator]`, `toString`, and `size`.
|
||||
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
|
||||
- [x] URL values are their href in `JSON.stringify` and at the host boundary. URLSearchParams are `{}` in
|
||||
`JSON.stringify` and their query string at the host boundary.
|
||||
|
||||
## Uint8Array
|
||||
|
||||
@@ -470,7 +484,8 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] Every value crossing in either direction is converted, never shared: plain objects and arrays are copied,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Headers`, `Map`, `Set`, and `Uint8Array` become fresh copies with
|
||||
their contents converted (a host `ArrayBuffer` comes in as a `Uint8Array`; other typed arrays cannot come out),
|
||||
errors cross as errors with their name and message, and a `__proto__` key is dropped. Functions, generators,
|
||||
errors cross as errors with their name, message, `cause`, and own enumerable data, and a `__proto__` key is
|
||||
dropped. Functions, generators,
|
||||
un-awaited promises, and symbols cannot be passed in; a class instance, a symbol, or a BigInt cannot come out.
|
||||
- [x] A host function inside a result becomes a program function whose calls cross the same way, so a result can
|
||||
carry methods (`res.json()`) whose host closures keep the host state. Diagnostics name it by its path
|
||||
@@ -495,8 +510,10 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
|
||||
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
|
||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. `message` is an own
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` while the host still
|
||||
receives `{ name, message }`. Errors have no `stack`; the diagnostic carries the source location instead.
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` for a plain error. The
|
||||
result boundary still emits `{ name, message, ...own enumerable }`, so a field such as `code` crosses. `cause` is
|
||||
non-enumerable: an extension Error carries it, and this JSON form does not. Errors have no `stack`; the diagnostic
|
||||
carries a 1-based line and column in the submitted source instead.
|
||||
- [x] `instanceof` against any constructor with a `prototype`, including every built-in and `Function`.
|
||||
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
|
||||
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
|
||||
@@ -505,8 +522,9 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
short orientation to the supported subset; this matrix is the full reference.
|
||||
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
|
||||
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
|
||||
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
|
||||
This is deliberate: the program should handle a failure the same way regardless of where it originated.
|
||||
Error-shaped value in `catch`, rejection handlers, and `Promise.allSettled` reasons. It always has `name` and
|
||||
`message`, plus `cause` and own data when the failure carried them. This is deliberate: the program should
|
||||
handle a failure the same way regardless of where it originated.
|
||||
- [x] Failures raised by the interpreter are `TypeError`s unless JavaScript names them otherwise (`RangeError`,
|
||||
`ReferenceError`, `SyntaxError`, `URIError`), so `e instanceof TypeError` and `e.constructor === TypeError`
|
||||
hold. Unsupported syntax reached at runtime is a `SyntaxError`; awaited tool failures stay plain `Error`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Effect, Exit } from "effect"
|
||||
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
|
||||
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 { Callable, get, Native, DateObj, Obj, coerceToNumber, coerceToString, 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 next: Effect.Effect<{ readonly done: boolean; readonly value: Value }, unknown, R>
|
||||
readonly close: Effect.Effect<void, unknown, R>
|
||||
}
|
||||
|
||||
@@ -26,9 +26,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"]
|
||||
@@ -43,24 +43,31 @@ export const toPrimitive = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
/** Invoke(value, name): calls the method the value would find through its prototype. */
|
||||
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: 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(
|
||||
|
||||
@@ -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"
|
||||
@@ -168,6 +179,7 @@ export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
|
||||
methods(builtins, prototype, [
|
||||
["toString", 0, (thisValue) => errorToString(receiver(Obj, thisValue, "Error.prototype.toString"))],
|
||||
])
|
||||
methods(builtins, ctor, [["isError", 1, (_, args) => args[0] instanceof ErrorObj]])
|
||||
}
|
||||
return ctor
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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> = {
|
||||
@@ -100,7 +101,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)
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
Statement,
|
||||
Super,
|
||||
SwitchStatement,
|
||||
Literal,
|
||||
TemplateLiteral,
|
||||
ThrowStatement,
|
||||
TryStatement,
|
||||
@@ -62,7 +63,7 @@ import {
|
||||
} from "./model.js"
|
||||
import { checkStringLength } from "./limits.js"
|
||||
import { locate, materialize } from "./errors.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { type Builtins, primitivePrototype } from "./intrinsics.js"
|
||||
import { globals } from "./globals.js"
|
||||
import {
|
||||
assign,
|
||||
@@ -75,20 +76,16 @@ import {
|
||||
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 +93,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.
|
||||
@@ -110,24 +107,36 @@ const loopExit = (result: StatementResult, labels: ReadonlySet<string> | undefin
|
||||
return undefined
|
||||
}
|
||||
|
||||
const calleeDescription = (callee: Expression | Super | undefined): string => {
|
||||
if (callee?.type === "Identifier") return callee.name
|
||||
if (callee?.type === "MemberExpression") {
|
||||
const object = callee.object
|
||||
const property = callee.property
|
||||
const key =
|
||||
!callee.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (object.type === "Identifier" && key !== undefined) return `${object.name}.${key}`
|
||||
// Native engines name the callee (`search(...).catch is not a function`), including call chains. Returns
|
||||
// undefined when a link cannot be named, so a chain is either named completely or not at all.
|
||||
const calleeDescription = (node: Expression | Super | undefined): string | undefined => {
|
||||
if (node?.type === "Identifier") return node.name
|
||||
if (node?.type === "CallExpression") {
|
||||
const target = calleeDescription(node.callee)
|
||||
return target === undefined ? undefined : `${target}(...)`
|
||||
}
|
||||
return "The called value"
|
||||
if (node?.type !== "MemberExpression") return undefined
|
||||
const property = node.property
|
||||
const key =
|
||||
!node.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (key === undefined) return undefined
|
||||
const object = calleeDescription(node.object)
|
||||
return object === undefined ? undefined : `${object}.${key}`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -224,7 +233,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
|
||||
|
||||
type CustomIterator = {
|
||||
iterator: Obj
|
||||
next: unknown
|
||||
next: Value
|
||||
asynchronous: boolean
|
||||
}
|
||||
|
||||
@@ -232,13 +241,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 = {
|
||||
@@ -264,7 +273,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
|
||||
@@ -278,27 +287,27 @@ 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)
|
||||
}
|
||||
|
||||
/** 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))
|
||||
@@ -321,7 +330,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()
|
||||
@@ -329,7 +338,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)
|
||||
@@ -354,15 +363,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)
|
||||
@@ -453,7 +459,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
@@ -655,7 +661,7 @@ class Frame<R> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -671,7 +677,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()
|
||||
@@ -720,7 +726,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),
|
||||
@@ -731,10 +737,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))
|
||||
@@ -746,7 +752,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
iterate(value: unknown, node?: AstNode) {
|
||||
iterate(value: Value, node?: AstNode) {
|
||||
const cursor = this.hostCursor(value)
|
||||
if (cursor !== undefined) return Effect.succeed(cursor)
|
||||
const self = this
|
||||
@@ -760,40 +766,24 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
|
||||
private hostCursor(value: unknown) {
|
||||
private hostCursor(value: Value) {
|
||||
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
|
||||
typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof Obj
|
||||
? value.iterator(this.ctx.builtins)
|
||||
: undefined
|
||||
if (iterator === undefined) return undefined
|
||||
const proto = this.ctx.builtins.Array
|
||||
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,
|
||||
}
|
||||
return { done: Boolean(step.done), value: step.value }
|
||||
}),
|
||||
close: Effect.void,
|
||||
}
|
||||
}
|
||||
|
||||
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
|
||||
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)
|
||||
@@ -883,18 +873,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))
|
||||
@@ -1058,7 +1048,7 @@ class Frame<R> {
|
||||
|
||||
private declarePattern(
|
||||
pattern: Pattern,
|
||||
value: unknown,
|
||||
value: Value,
|
||||
mutable: boolean,
|
||||
node: AstNode,
|
||||
initialize = false,
|
||||
@@ -1118,7 +1108,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") {
|
||||
@@ -1170,7 +1160,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)
|
||||
@@ -1178,8 +1168,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* () {
|
||||
@@ -1203,7 +1193,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
|
||||
@@ -1233,13 +1223,12 @@ class Frame<R> {
|
||||
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))
|
||||
@@ -1254,7 +1243,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)
|
||||
}
|
||||
@@ -1295,7 +1284,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)
|
||||
@@ -1305,7 +1294,7 @@ class Frame<R> {
|
||||
// `new` itself is supported, so a non-constructible callee is a TypeError like JS rather than
|
||||
// unsupported syntax. Built-ins like Number are real constructors in JS, so do not claim
|
||||
// otherwise; say `new` is unsupported for them and point at the plain call.
|
||||
const name = calleeDescription(node.callee)
|
||||
const name = calleeDescription(node.callee) ?? "The called value"
|
||||
const message =
|
||||
callee instanceof Fn
|
||||
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
|
||||
@@ -1319,7 +1308,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)
|
||||
@@ -1332,7 +1321,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)) {
|
||||
@@ -1341,14 +1330,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)
|
||||
@@ -1402,7 +1386,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)
|
||||
@@ -1413,7 +1397,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)
|
||||
@@ -1428,13 +1412,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)
|
||||
@@ -1452,7 +1431,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
|
||||
@@ -1492,9 +1471,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
|
||||
@@ -1519,7 +1498,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
|
||||
@@ -1532,7 +1511,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)
|
||||
}
|
||||
@@ -1561,7 +1540,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
|
||||
@@ -1579,7 +1558,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 }
|
||||
@@ -1590,12 +1569,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) {
|
||||
@@ -1608,12 +1587,12 @@ class Frame<R> {
|
||||
if (callable instanceof Native) {
|
||||
return yield* self.native(() => (callable as Native<R>).call(thisValue, args), node)
|
||||
}
|
||||
throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
|
||||
throw typeError(`${calleeDescription(callee) ?? "The called value"} is not a function.`, callee ?? node)
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -1623,10 +1602,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)
|
||||
@@ -1646,7 +1625,7 @@ 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
|
||||
@@ -1693,16 +1672,16 @@ class Frame<R> {
|
||||
|
||||
private createGenerator(
|
||||
invocation: Frame<R>,
|
||||
run: Effect.Effect<unknown, unknown, R>,
|
||||
run: Effect.Effect<Value, unknown, R>,
|
||||
asynchronous: boolean,
|
||||
): GeneratorObj {
|
||||
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
|
||||
@@ -1773,7 +1752,7 @@ class Frame<R> {
|
||||
|
||||
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)
|
||||
@@ -1819,7 +1798,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* () {
|
||||
@@ -1834,7 +1813,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 })))
|
||||
@@ -1849,20 +1828,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.hostCursor(value)
|
||||
if (cursor !== undefined) {
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return undefined
|
||||
@@ -1886,7 +1856,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) {
|
||||
@@ -1906,7 +1876,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")
|
||||
@@ -1915,7 +1885,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
|
||||
@@ -1956,7 +1926,7 @@ class Frame<R> {
|
||||
} else if (keyNode.type === "Identifier") {
|
||||
key = keyNode.name
|
||||
} else if (keyNode.type === "Literal") {
|
||||
key = self.toPropertyKey(keyNode.value, keyNode)
|
||||
key = self.toPropertyKey(literal(keyNode), keyNode)
|
||||
} else {
|
||||
throw typeError("Unsupported object property key shape.", keyNode)
|
||||
}
|
||||
@@ -1975,7 +1945,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* () {
|
||||
@@ -2030,13 +2000,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)
|
||||
}
|
||||
@@ -2045,7 +2015,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)
|
||||
@@ -2071,16 +2041,14 @@ class Frame<R> {
|
||||
|
||||
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
|
||||
|
||||
// Primitives read through their wrapper prototype without being boxed; strings own length and indexes.
|
||||
const builtins = self.ctx.builtins
|
||||
// Strings own length and indexes; every other primitive property reads through the wrapper prototype.
|
||||
if (typeof objectValue === "string") {
|
||||
if (key === "length") return { value: objectValue.length }
|
||||
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
|
||||
if (index !== undefined) return { value: objectValue[index] }
|
||||
return { target: builtins.String, key, receiver: objectValue }
|
||||
}
|
||||
if (typeof objectValue === "number") return { target: builtins.Number, key, receiver: objectValue }
|
||||
if (typeof objectValue === "boolean") return { target: builtins.Boolean, key, receiver: objectValue }
|
||||
const proto = primitivePrototype(self.ctx.builtins, objectValue)
|
||||
if (proto !== undefined) return { target: proto, key, receiver: objectValue }
|
||||
|
||||
if (objectValue === null || objectValue === undefined) {
|
||||
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
|
||||
@@ -2089,7 +2057,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(
|
||||
@@ -2101,7 +2069,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) {
|
||||
@@ -2109,7 +2077,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
|
||||
@@ -2118,7 +2086,7 @@ 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 }))
|
||||
}
|
||||
|
||||
@@ -2140,8 +2108,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)
|
||||
@@ -2161,7 +2129,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(
|
||||
@@ -2179,7 +2147,7 @@ class Frame<R> {
|
||||
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
|
||||
}
|
||||
|
||||
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
|
||||
private toPropertyKey(value: Value, node: AstNode): PropertyKey {
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return 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",
|
||||
@@ -52,6 +52,14 @@ export const createErrorValue = (prototype: Obj, message: string | undefined): E
|
||||
return value
|
||||
}
|
||||
|
||||
/** The prototype a primitive reads its methods from without being boxed; none for null, undefined, and symbols. */
|
||||
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
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const createBuiltins = (): Builtins => {
|
||||
const object = new Obj(null)
|
||||
// Function.prototype is itself callable and returns undefined.
|
||||
|
||||
@@ -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,12 +83,10 @@ 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: Math.max(1, (node.loc?.start.line ?? 2) - 1),
|
||||
column: Math.max(1, (node.loc?.start.column ?? 4) - 3),
|
||||
line: node.loc?.start.line ?? 1,
|
||||
column: (node.loc?.start.column ?? 0) + 1,
|
||||
})
|
||||
|
||||
export const formatLocation = (node?: AstNode): string => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BlockStatement, Expression, Pattern } from "acorn"
|
||||
import type { Effect, 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,119 @@ 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}`
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -77,8 +165,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 +191,163 @@ 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 {
|
||||
export class IteratorObj extends Opaque {
|
||||
override readonly tag = "Iterator"
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly iterator: IteratorObject<unknown>,
|
||||
readonly source: IteratorObject<Value, undefined>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
override get describe() {
|
||||
return "an iterator"
|
||||
}
|
||||
override iterator() {
|
||||
return this.source
|
||||
}
|
||||
}
|
||||
|
||||
export class DateObj extends Obj {
|
||||
/** 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 +357,52 @@ 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)
|
||||
}
|
||||
|
||||
/** 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 +420,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 +441,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 +467,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 +490,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 +518,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 +569,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
|
||||
|
||||
@@ -2,7 +2,7 @@ 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"
|
||||
@@ -10,7 +10,7 @@ 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,7 +131,7 @@ 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))
|
||||
}
|
||||
@@ -141,8 +141,8 @@ const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"]
|
||||
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])
|
||||
}
|
||||
@@ -175,7 +175,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 +223,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,12 +237,12 @@ const instanceMethod = <R>(
|
||||
return chainReaction(ctx, promise, onFulfilled, onRejected, method)
|
||||
}
|
||||
|
||||
const constructPromise = <R>(ctx: Interpreter<R>, executor: unknown): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
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 deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const deferred = Deferred.makeUnsafe<Value, unknown>()
|
||||
const promise = yield* ctx.pending.createWithSelf((self) =>
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)),
|
||||
)
|
||||
@@ -262,10 +262,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 +279,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,14 +2,24 @@ 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,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.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 } => {
|
||||
const arrayLikeSource = (source: Value): { 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)
|
||||
@@ -21,7 +31,7 @@ const arrayLikeSource = (source: unknown): { readonly length: number; readonly s
|
||||
)
|
||||
}
|
||||
|
||||
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 =
|
||||
@@ -33,14 +43,14 @@ const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<
|
||||
throw typeError("Array.from expects a synchronous iterable or array-like value.")
|
||||
}
|
||||
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
|
||||
@@ -53,21 +63,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 +98,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,8 +119,8 @@ 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 => {
|
||||
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") {
|
||||
throw typeError(`Array.${name} expects ${label} to be a number.`)
|
||||
@@ -122,11 +132,11 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
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,
|
||||
@@ -214,7 +224,7 @@ 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)
|
||||
checkArrayLength(flattened.length)
|
||||
@@ -340,6 +350,19 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"toLocaleString",
|
||||
0,
|
||||
(thisValue) =>
|
||||
Effect.map(
|
||||
Effect.forEach(self(thisValue, "toLocaleString").items, (item) =>
|
||||
item === null || item === undefined
|
||||
? Effect.succeed("")
|
||||
: Effect.map(invoke(ctx, item, "toLocaleString", "Array.prototype.toLocaleString"), coerceToString),
|
||||
),
|
||||
(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())],
|
||||
[
|
||||
@@ -356,7 +379,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
|
||||
@@ -368,7 +391,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])
|
||||
@@ -381,7 +404,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]
|
||||
|
||||
@@ -2,13 +2,24 @@ 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,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
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 +38,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 +59,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,13 +74,13 @@ 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 => {
|
||||
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, 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 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)],
|
||||
@@ -222,8 +233,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)
|
||||
|
||||
@@ -7,15 +7,16 @@ import {
|
||||
get,
|
||||
getOwn,
|
||||
hidden,
|
||||
isWrapper,
|
||||
Arr,
|
||||
coerceToString,
|
||||
IteratorObj,
|
||||
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 +26,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)
|
||||
@@ -81,7 +82,7 @@ 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* () {
|
||||
@@ -105,7 +106,7 @@ 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* () {
|
||||
@@ -130,8 +131,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])],
|
||||
@@ -145,6 +145,29 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsert",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsert").map
|
||||
if (!target.has(args[0])) target.set(args[0], args[1])
|
||||
return target.get(args[0])
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsertComputed",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsertComputed").map
|
||||
const apply = applyCollectionCallback(ctx, args[1], "Map.getOrInsertComputed")
|
||||
if (target.has(args[0])) return target.get(args[0])
|
||||
// The callback sees the stored key (-0 is +0) and its result wins over anything it inserted itself.
|
||||
return Effect.map(apply([args[0] === 0 ? 0 : args[0]]), (value) => {
|
||||
target.set(args[0], value)
|
||||
return value
|
||||
})
|
||||
},
|
||||
],
|
||||
["delete", 1, (thisValue, args) => self(thisValue, "delete").map.delete(args[0])],
|
||||
[
|
||||
"clear",
|
||||
@@ -156,17 +179,7 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
],
|
||||
["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])),
|
||||
),
|
||||
],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
[
|
||||
"forEach",
|
||||
1,
|
||||
@@ -186,26 +199,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()),
|
||||
})
|
||||
}
|
||||
@@ -224,10 +237,10 @@ 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)
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result): Effect.Effect<Iterable<Value>> => {
|
||||
if (result instanceof IteratorObj) return Effect.succeed(result.source)
|
||||
if (result instanceof Arr) return Effect.succeed(result.items)
|
||||
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
|
||||
}),
|
||||
@@ -239,8 +252,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 = () => {
|
||||
@@ -319,8 +332,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -105,6 +104,13 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["toTimeString", 0, (thisValue) => new Date(self(thisValue, "toTimeString").time).toTimeString()],
|
||||
["toUTCString", 0, (thisValue) => new Date(self(thisValue, "toUTCString").time).toUTCString()],
|
||||
["toGMTString", 0, (thisValue) => new Date(self(thisValue, "toGMTString").time).toUTCString()],
|
||||
...(["toLocaleString", "toLocaleDateString", "toLocaleTimeString"] as const).map(
|
||||
(name): Method => [
|
||||
name,
|
||||
0,
|
||||
(thisValue) => new Date(self(thisValue, name).time)[name]("en-US", { timeZone: "UTC" }),
|
||||
],
|
||||
),
|
||||
...getters.map((name): Method => [name, 0, (thisValue) => new Date(self(thisValue, name).time)[name]()]),
|
||||
...setters.map(
|
||||
([name, length]): Method => [
|
||||
|
||||
@@ -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,
|
||||
IteratorObj,
|
||||
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,7 +102,8 @@ 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.
|
||||
@@ -104,15 +117,7 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
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])),
|
||||
),
|
||||
],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
[
|
||||
"forEach",
|
||||
1,
|
||||
|
||||
@@ -11,7 +11,7 @@ export const iteratorGlobals = <R>(ctx: Interpreter<R>): void => {
|
||||
"next",
|
||||
0,
|
||||
(thisValue) => {
|
||||
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").iterator.next()
|
||||
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").source.next()
|
||||
return record(builtins.Object, { value: step.value, done: Boolean(step.done) })
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { checkStringLength } from "../interpreter/limits.js"
|
||||
import { syntaxError, typeError } 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, record, remove, set, type Value } from "../interpreter/objects.js"
|
||||
|
||||
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const json = new Obj(ctx.builtins.Object)
|
||||
@@ -17,7 +17,7 @@ export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return json
|
||||
}
|
||||
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
|
||||
|
||||
@@ -31,7 +31,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 +46,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]
|
||||
|
||||
@@ -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, type Value } from "../interpreter/objects.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 => {
|
||||
const number = (name: string, args: Array<Value>, 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.`)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { constructor, constants, methods } from "../interpreter/native.js"
|
||||
import { 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
|
||||
@@ -39,17 +40,18 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["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 => {
|
||||
const optNum = (name: string, arg: Value): number | undefined => {
|
||||
if (arg === undefined) return undefined
|
||||
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
|
||||
return arg
|
||||
}
|
||||
methods(builtins, builtins.Number, [
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
|
||||
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
|
||||
[
|
||||
"toExponential",
|
||||
1,
|
||||
@@ -88,7 +90,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.`)
|
||||
}
|
||||
|
||||
@@ -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 { preserveConsumerError } from "../interpreter/callback.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)
|
||||
@@ -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
|
||||
@@ -196,7 +187,7 @@ export const objectGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
true,
|
||||
],
|
||||
["toString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => invoke(ctx, thisValue, "toString", "Object.prototype.toLocaleString")],
|
||||
[
|
||||
"valueOf",
|
||||
0,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
IteratorObj,
|
||||
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))
|
||||
@@ -99,7 +108,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
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 +116,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)
|
||||
@@ -150,6 +159,8 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
simple("valueOf", 0, (value) => value),
|
||||
simple("toLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("toLocaleLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toLocaleUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("trim", 0, (value) => value.trim()),
|
||||
simple("trimStart", 0, (value) => value.trimStart()),
|
||||
simple("trimLeft", 0, (value) => value.trimStart()),
|
||||
@@ -216,7 +227,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))
|
||||
|
||||
@@ -7,17 +7,17 @@ import {
|
||||
entries,
|
||||
get,
|
||||
hidden,
|
||||
isWrapper,
|
||||
Arr,
|
||||
IteratorObj,
|
||||
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.`)
|
||||
@@ -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* () {
|
||||
@@ -161,7 +161,7 @@ export const readPairs = <R>(
|
||||
|
||||
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"}.`)
|
||||
}
|
||||
@@ -272,17 +271,7 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
],
|
||||
["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])),
|
||||
),
|
||||
],
|
||||
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
|
||||
[
|
||||
"forEach",
|
||||
|
||||
@@ -1,64 +1,13 @@
|
||||
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,15 +15,6 @@ 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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,6 +22,16 @@ describe("source syntax", () => {
|
||||
})
|
||||
|
||||
describe("error identity", () => {
|
||||
test("Error.isError is true for every Error value and nothing else", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const caught = (() => { try { null.foo } catch (error) { return error } })()
|
||||
return [Error.isError(new Error("x")), Error.isError(new RangeError("x")), Error.isError(caught),
|
||||
Error.isError({ name: "Error", message: "x" }), Error.isError("Error"), Error.isError(null)]
|
||||
`),
|
||||
).toEqual([true, true, true, false, false, false])
|
||||
})
|
||||
|
||||
test("awaiting the same rejected promise twice yields the same error object", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
@@ -95,6 +105,24 @@ describe("uncaught program throws", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("source locations", () => {
|
||||
test("uses the submitted line and 1-based column", async () => {
|
||||
const failure = await error("const value = 1\nreturn value()")
|
||||
expect(failure.location).toEqual({ line: 2, column: 8 })
|
||||
expect(failure.message).toBe("TypeError: value is not a function. (line 2, col 8)")
|
||||
})
|
||||
|
||||
test("names a missing method on a call instead of the previous line", async () => {
|
||||
const failure = await error(`// Try search with different namespaces
|
||||
for (const ns of ["github", "tools.github", "tools", ""]) {
|
||||
const s = await search({query: "star", namespace: ns, limit: 100}).catch(e=>({items:[],error:String(e)}));
|
||||
return s
|
||||
}`)
|
||||
expect(failure.location).toEqual({ line: 3, column: 19 })
|
||||
expect(failure.message).toBe("TypeError: search(...).catch is not a function. (line 3, col 19)")
|
||||
})
|
||||
})
|
||||
|
||||
describe("host errors escaping built-ins", () => {
|
||||
test("become the same-named program error", async () => {
|
||||
expect(
|
||||
@@ -107,7 +135,7 @@ describe("host errors escaping built-ins", () => {
|
||||
test("report the location of the call that raised them", async () => {
|
||||
const failure = await error(`return [1].map((n) => n.toFixed(200))`)
|
||||
expect(failure.kind).toBe("ExecutionFailure")
|
||||
expect(failure.message).toBe("RangeError: toFixed() argument must be between 0 and 100 (line 1, col 19)")
|
||||
expect(failure.message).toBe("RangeError: toFixed() argument must be between 0 and 100 (line 1, col 23)")
|
||||
})
|
||||
|
||||
test("a built-in that rejects its arguments before doing any work is located at the call", async () => {
|
||||
@@ -115,13 +143,13 @@ describe("host errors escaping built-ins", () => {
|
||||
})
|
||||
|
||||
test("a rejection born inside a promise the built-in created is located at the creating call", async () => {
|
||||
expect((await error(`return await Promise.all(1)`)).message).toEndWith("(line 1, col 10)")
|
||||
expect((await error(`return await Promise.race([])`)).message).toEndWith("(line 1, col 10)")
|
||||
expect((await error(`return await Promise.all(1)`)).message).toEndWith("(line 1, col 14)")
|
||||
expect((await error(`return await Promise.race([])`)).message).toEndWith("(line 1, col 14)")
|
||||
expect((await error(`return await Promise.all({ [Symbol.iterator]: () => ({ next: 1 }) })`)).message).toEndWith(
|
||||
"(line 1, col 10)",
|
||||
"(line 1, col 14)",
|
||||
)
|
||||
expect((await error(`let p; p = Promise.resolve().then(() => p); return await p`)).message).toEndWith(
|
||||
"(line 1, col 8)",
|
||||
"(line 1, col 12)",
|
||||
)
|
||||
})
|
||||
|
||||
@@ -134,7 +162,7 @@ describe("host errors escaping built-ins", () => {
|
||||
|
||||
test("a failure inside a built-in called by another built-in is located at the outer call", async () => {
|
||||
const failure = await error(`return Array.from({ [Symbol.iterator]: () => ({ next: 1 }) })`)
|
||||
expect(failure.message).toBe("TypeError: Iterator next must be a function. (line 1, col 4)")
|
||||
expect(failure.message).toBe("TypeError: Iterator next must be a function. (line 1, col 8)")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -153,7 +181,7 @@ describe("call depth", () => {
|
||||
test("uncaught overflow reports the call that overflowed", async () => {
|
||||
const failure = await error(`const f = (n) => f(n + 1); return f(0)`)
|
||||
expect(failure.kind).toBe("ExecutionFailure")
|
||||
expect(failure.message).toBe("RangeError: Maximum call stack size exceeded (line 1, col 14)")
|
||||
expect(failure.message).toBe("RangeError: Maximum call stack size exceeded (line 1, col 18)")
|
||||
})
|
||||
|
||||
test("the limit is 10000 nested calls", async () => {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1030,7 +1030,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 () => {
|
||||
|
||||
@@ -502,6 +502,12 @@ describe("CodeMode-specific array behavior", () => {
|
||||
expect(err.message).toContain("circular")
|
||||
})
|
||||
|
||||
test("indexOf and lastIndexOf with no argument search for undefined", async () => {
|
||||
expect(await value(`return [1, undefined, 3].indexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, undefined, 3].lastIndexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, 2, 3].indexOf()`)).toBe(-1)
|
||||
})
|
||||
|
||||
test("keys/values/entries return iterators usable with for...of and spread", async () => {
|
||||
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
||||
expect(await value(`return [...["x","y"].values()]`)).toEqual(["x", "y"])
|
||||
@@ -940,6 +946,12 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
expect(await value(`try { JSON.rawJSON("1") } catch (e) { return e.message }`)).toBe(
|
||||
"JSON.rawJSON is not a function.",
|
||||
)
|
||||
expect(await value(`try { search({ query: "star" }).catch(() => 1) } catch (e) { return e.message }`)).toBe(
|
||||
"search(...).catch is not a function.",
|
||||
)
|
||||
expect(
|
||||
await value(`const foo = () => ({ bar: () => ({}) }); try { foo().bar().baz() } catch (e) { return e.message }`),
|
||||
).toBe("foo(...).bar(...).baz is not a function.")
|
||||
})
|
||||
|
||||
test("built-ins are objects on a real prototype chain", async () => {
|
||||
@@ -961,6 +973,17 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("async function line breaks", () => {
|
||||
test("a line break between function and the name is an async function", async () => {
|
||||
expect(await value(`async function\nfoo() { return 1 }\nreturn await foo()`)).toBe(1)
|
||||
})
|
||||
|
||||
test("a line break between async and function is not an async function", async () => {
|
||||
const failure = await error(`async\nfunction foo() { return 1 }\nreturn foo()`)
|
||||
expect(failure.message).toContain("Unknown identifier 'async'")
|
||||
})
|
||||
})
|
||||
|
||||
describe("functions are objects", () => {
|
||||
test("name follows NamedEvaluation and length counts required parameters", async () => {
|
||||
expect(
|
||||
|
||||
@@ -20,9 +20,9 @@ import { CodeMode, Tool } from "../src/index.js"
|
||||
|
||||
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
|
||||
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
|
||||
// values, while at the host boundary (final result, tool arguments, JSON.stringify) they
|
||||
// serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
|
||||
// URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
|
||||
// values. JSON.stringify keeps Date -> ISO string (invalid -> null), URL -> href, and
|
||||
// RegExp/Map/Set/URLSearchParams -> {}. The host boundary matches that except URLSearchParams,
|
||||
// which cross as their query string, and Set, which crosses as an array.
|
||||
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
const value = async (code: string) => {
|
||||
const result = await run(code)
|
||||
@@ -898,6 +898,42 @@ describe("Map", () => {
|
||||
).toEqual({ a: 3, b: 1, c: 1 })
|
||||
})
|
||||
|
||||
test("getOrInsert and getOrInsertComputed insert only when the key is missing", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const groups = new Map()
|
||||
groups.getOrInsert("a", []).push(1)
|
||||
groups.getOrInsert("a", []).push(2)
|
||||
let calls = 0
|
||||
const computed = (key) => { calls++; return key + "!" }
|
||||
const first = groups.getOrInsertComputed("b", computed)
|
||||
const second = groups.getOrInsertComputed("b", computed)
|
||||
const zero = groups.getOrInsertComputed(-0, (key) => 1 / key === Infinity)
|
||||
return [[...groups], first, second, calls, zero]
|
||||
`),
|
||||
).toEqual([
|
||||
[
|
||||
["a", [1, 2]],
|
||||
["b", "b!"],
|
||||
[0, true],
|
||||
],
|
||||
"b!",
|
||||
"b!",
|
||||
1,
|
||||
true,
|
||||
])
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map()
|
||||
const outer = m.getOrInsertComputed("k", () => { m.set("k", "inner"); return "outer" })
|
||||
let thrown
|
||||
try { m.getOrInsertComputed("j", () => { throw new Error("boom") }) } catch (error) { thrown = error.message }
|
||||
return [outer, m.get("k"), thrown, m.has("j")]
|
||||
`),
|
||||
).toEqual(["outer", "outer", "boom", false])
|
||||
expect((await error(`new Map().getOrInsertComputed("k", 5)`)).message).toContain("expects a function callback")
|
||||
})
|
||||
|
||||
test("maps serialize to {} at the boundary, like JSON", async () => {
|
||||
expect(await value(`return new Map([["a", 1]])`)).toEqual({})
|
||||
expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
|
||||
@@ -1061,7 +1097,7 @@ describe("Uint8Array", () => {
|
||||
console.log(b, new Uint8Array())
|
||||
return [String(b), b + "", +new Uint8Array([5]), Number.isNaN(Number(b)), b == "1,2", JSON.stringify(b), b.toLocaleString(), typeof b, b instanceof Uint8Array]
|
||||
`),
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "[object Uint8Array]", "object", true])
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "1,2", "object", true])
|
||||
expect((await run(`console.log(new Uint8Array([1, 2]), new Uint8Array())`)).logs).toEqual([
|
||||
"Uint8Array(2) [1,2] Uint8Array(0) []",
|
||||
])
|
||||
@@ -1231,6 +1267,75 @@ 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("toLocaleString", () => {
|
||||
test("numbers and dates format as en-US in UTC; everything else falls back to toString", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
(1234567.891).toLocaleString(), new Date(0).toLocaleString(), new Date(0).toLocaleDateString(),
|
||||
new Date(0).toLocaleTimeString(), "a".toLocaleString(), true.toLocaleString(), ({}).toLocaleString(),
|
||||
({ toString: () => "custom" }).toLocaleString(), new Uint8Array([1, 2]).toLocaleString(),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
"1,234,567.891",
|
||||
"1/1/1970, 12:00:00 AM",
|
||||
"1/1/1970",
|
||||
"12:00:00 AM",
|
||||
"a",
|
||||
"true",
|
||||
"[object Object]",
|
||||
"custom",
|
||||
"1,2",
|
||||
])
|
||||
})
|
||||
|
||||
test("arrays join each element's toLocaleString, skipping holes and nullish elements", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let calls = 0
|
||||
const item = { toLocaleString() { calls++; return "o" } }
|
||||
return [[1234.5, "x", null, undefined, item, new Date(0)].toLocaleString(), [, item, , item].toLocaleString(), calls]
|
||||
`),
|
||||
).toEqual(["1,234.5,x,,,o,1/1/1970, 12:00:00 AM", ",o,,o", 3])
|
||||
expect((await error(`const f = ({}).toLocaleString; f()`)).message).toContain(
|
||||
"Object.prototype.toLocaleString called on null or undefined",
|
||||
)
|
||||
})
|
||||
|
||||
test("toLocaleLowerCase and toLocaleUpperCase ignore the locale argument", async () => {
|
||||
expect(
|
||||
await value(`return ["ABC".toLocaleLowerCase("tr"), "abc".toLocaleUpperCase(), "İ".toLocaleLowerCase()]`),
|
||||
).toEqual(["abc", "ABC", "i̇"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdlib integration", () => {
|
||||
test("constructor follows own keys, shadowing, writes, and new", async () => {
|
||||
expect(
|
||||
|
||||
@@ -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, () =>
|
||||
|
||||
@@ -225,7 +225,6 @@ built-ins/Array/prototype/splice/S15.4.4.12_A4_T1.js # Array.prototype.splice c
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A4_T2.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A4_T3.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/splice/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
|
||||
built-ins/Array/prototype/toLocaleString/S15.4.4.3_A1_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; var arr = [undefined, obj, null, obj, o
|
||||
built-ins/Array/prototype/toLocaleString/S15.4.4.3_A3_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; Array.prototype[1] = obj; x = [obj]; x.
|
||||
built-ins/Array/prototype/toReversed/holes-not-preserved.js # Actual [4, undefined, 2, undefined, 0] and expected [4, 3, 2, undefined, 0] should have the same con
|
||||
built-ins/Array/prototype/toSorted/holes-not-preserved.js # Actual [1, 3, 4, undefined, undefined] and expected [1, 2, 3, 4, undefined] should have the same con
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -27,6 +27,7 @@ export function resolve(model: Model.Info, supports: readonly Support[] = [{ typ
|
||||
const EFFORTS = ["low", "medium", "high"]
|
||||
const ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
const ADAPTIVE_THINKING = { type: "adaptive", display: "summarized" }
|
||||
const ANTHROPIC_OUTPUT_TOKEN_MAX = 32_000
|
||||
|
||||
const variant = (id: string, overlay: Overlay): Variants[number] => ({ id: Model.VariantID.make(id), ...overlay })
|
||||
|
||||
@@ -39,8 +40,9 @@ function budgets(
|
||||
model: Model.Info,
|
||||
support: Extract<Support, { type: "budget_tokens" }>,
|
||||
spell: (tokens: number) => Overlay,
|
||||
ceiling = model.limit.output,
|
||||
): Variants {
|
||||
const maximum = Math.min(support.max ?? model.limit.output - 1, model.limit.output - 1)
|
||||
const maximum = Math.min(support.max ?? ceiling - 1, model.limit.output - 1, ceiling - 1)
|
||||
if (maximum <= 0) return []
|
||||
const high = Math.min(Math.max(support.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
|
||||
return [variant("high", spell(high)), variant("max", spell(maximum))]
|
||||
@@ -273,9 +275,12 @@ const anthropicMessages: Protocol = (model, support) => {
|
||||
return toggle({ settings: { thinking: { type: "disabled" } } }, thinking)
|
||||
}
|
||||
case "budget_tokens":
|
||||
return budgets(model, support, (tokens) => ({
|
||||
settings: { thinking: { type: "enabled", budgetTokens: tokens } },
|
||||
}))
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => ({ settings: { thinking: { type: "enabled", budgetTokens: tokens } } }),
|
||||
ANTHROPIC_OUTPUT_TOKEN_MAX,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,10 +386,14 @@ const bedrockConverse: Protocol = (model, support) => {
|
||||
? toggle(fields({ thinking: { type: "disabled" } }), fields({ thinking: ADAPTIVE_THINKING }))
|
||||
: toggle(fields({ reasoningConfig: { type: "disabled" } }), fields({ reasoningConfig: { type: "enabled" } }))
|
||||
case "budget_tokens":
|
||||
return budgets(model, support, (tokens) =>
|
||||
claude
|
||||
? fields({ thinking: { type: "enabled", budget_tokens: tokens } })
|
||||
: fields({ reasoningConfig: { type: "enabled", budgetTokens: tokens } }),
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) =>
|
||||
claude
|
||||
? fields({ thinking: { type: "enabled", budget_tokens: tokens } })
|
||||
: fields({ reasoningConfig: { type: "enabled", budgetTokens: tokens } }),
|
||||
claude ? ANTHROPIC_OUTPUT_TOKEN_MAX : model.limit.output,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -436,9 +445,12 @@ const bedrockAISDK: Protocol = (model, support) => {
|
||||
{ settings: { additionalModelRequestFields: { reasoningConfig: { type: "enabled" } } } },
|
||||
)
|
||||
case "budget_tokens":
|
||||
return budgets(model, support, (tokens) => ({
|
||||
settings: { reasoningConfig: { type: "enabled", budgetTokens: tokens } },
|
||||
}))
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => ({ settings: { reasoningConfig: { type: "enabled", budgetTokens: tokens } } }),
|
||||
claude ? ANTHROPIC_OUTPUT_TOKEN_MAX : model.limit.output,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,8 +501,11 @@ const sapAICore: Protocol = (model, support) => {
|
||||
return []
|
||||
case "budget_tokens":
|
||||
if (id.includes("anthropic"))
|
||||
return budgets(model, support, (tokens) =>
|
||||
sap({ additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: tokens } } }),
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => sap({ additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: tokens } } }),
|
||||
ANTHROPIC_OUTPUT_TOKEN_MAX,
|
||||
)
|
||||
if (id.includes("gemini"))
|
||||
return budgets(model, support, (tokens) =>
|
||||
|
||||
@@ -1018,11 +1018,11 @@ describe("ModelsDevPlugin", () => {
|
||||
const budgetModel = yield* modelState.get(Provider.ID.anthropic, Model.ID.make("claude-budget"))
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 32000 } },
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||
})
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: Model.VariantID.make("max"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 63999 } },
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
|
||||
})
|
||||
|
||||
const anthropicEffortModel = yield* modelState.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4.7"))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Agent } from "@opencode/core/agent"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { PluginHost } from "@opencode/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode/core/plugin/promise"
|
||||
import { Tool } from "@opencode/core/tool"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
it.live("Promise tool executors receive interruption through their AbortSignal", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const tools = yield* Tool.Service
|
||||
const started = yield* Deferred.make<AbortSignal>()
|
||||
yield* PluginPromise.fromPromise({
|
||||
id: "cancel-tool",
|
||||
async setup(context) {
|
||||
await context.tool.transform((editor) =>
|
||||
editor.add({
|
||||
name: "wait",
|
||||
description: "Wait until cancelled",
|
||||
input: { type: "object", properties: {}, additionalProperties: false },
|
||||
options: { codemode: false },
|
||||
execute: (_input, context) =>
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
context.signal.addEventListener("abort", () => reject(new Error("cancelled")), { once: true })
|
||||
Effect.runSync(Deferred.succeed(started, context.signal))
|
||||
}),
|
||||
}),
|
||||
)
|
||||
},
|
||||
}).effect(yield* PluginHost.make(plugins))
|
||||
|
||||
const snapshot = yield* tools.snapshot()
|
||||
const fiber = yield* snapshot
|
||||
.execute({
|
||||
sessionID: Session.ID.make("ses_promise_tool_cancel"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_promise_tool_cancel"),
|
||||
call: { type: "tool-call", id: "call_promise_tool_cancel", name: "wait", input: {} },
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
const signal = yield* Deferred.await(started)
|
||||
expect(signal.aborted).toBe(false)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
}),
|
||||
)
|
||||
@@ -93,6 +93,15 @@ test("recognizes Claude version spellings and future models", () => {
|
||||
settings: { effort, thinking: { type: "adaptive", display: "summarized" } },
|
||||
})),
|
||||
)
|
||||
|
||||
expect(
|
||||
resolve(model("@opencode/ai/providers/anthropic", "claude-haiku-4-5", 64_000), [
|
||||
{ type: "budget_tokens", min: 1_024, max: 64_000 },
|
||||
]),
|
||||
).toEqual([
|
||||
{ id: "high", settings: { thinking: { type: "enabled", budgetTokens: 16_000 } } },
|
||||
{ id: "max", settings: { thinking: { type: "enabled", budgetTokens: 31_999 } } },
|
||||
])
|
||||
})
|
||||
|
||||
test("spells Cloudflare AI Gateway variants for their upstream routes", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/desktop",
|
||||
"private": true,
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -103,11 +103,17 @@ export function createBrowserPage(
|
||||
revision++
|
||||
})
|
||||
let closed = false
|
||||
// Whether the native surface holds a real document worth showing. Chromium keeps the
|
||||
// previous document painted until the next one renders, so a shown page stays shown
|
||||
// through later navigations; blank and failed documents hide until a real one is ready.
|
||||
let content = false
|
||||
let failure: { url: string; message: string } | undefined
|
||||
const state = (): Browser.Tab => ({
|
||||
id: options.id,
|
||||
url: contents.getURL().slice(0, 16_384),
|
||||
url: (failure?.url ?? contents.getURL()).slice(0, 16_384),
|
||||
title: contents.getTitle().slice(0, 2_048),
|
||||
loading: contents.isLoading(),
|
||||
...(failure ? { loadError: failure.message } : {}),
|
||||
canGoBack: contents.navigationHistory.canGoBack(),
|
||||
canGoForward: contents.navigationHistory.canGoForward(),
|
||||
generation,
|
||||
@@ -115,16 +121,40 @@ export function createBrowserPage(
|
||||
const publish = () => {
|
||||
if (!closed) options.publish()
|
||||
}
|
||||
const reset = (event: Electron.Event<{ isMainFrame: boolean; isSameDocument: boolean }>) => {
|
||||
const reset = (event: Electron.Event<{ url: string; isMainFrame: boolean; isSameDocument: boolean }>) => {
|
||||
if (!event.isMainFrame || event.isSameDocument) return
|
||||
failure = undefined
|
||||
generation++
|
||||
documents.clear()
|
||||
refs.clear()
|
||||
diagnostics.clear()
|
||||
publish()
|
||||
}
|
||||
const settle = () => {
|
||||
content = contents.getURL() !== "about:blank" && !failure
|
||||
updateVisibility()
|
||||
}
|
||||
contents.on("did-start-navigation", reset)
|
||||
contents.on("did-stop-loading", publish)
|
||||
contents.on("did-navigate", (_event, url, status, statusText) => {
|
||||
// The server-network proxy answers an unreachable HTTP target with an empty 502. Other
|
||||
// error statuses are real documents from the user's server and stay visible.
|
||||
if (status === 502) failure = { url, message: `${status} ${statusText}`.trim().slice(0, 2_048) }
|
||||
// A blank or failed document paints at commit; a real one waits for dom-ready.
|
||||
if (url === "about:blank" || failure) settle()
|
||||
publish()
|
||||
})
|
||||
contents.on("did-fail-load", (_event, code, description, url, isMainFrame) => {
|
||||
// Cancelled navigation and failed subframes do not replace the current page.
|
||||
if (!isMainFrame || code === -3) return
|
||||
failure = { url, message: description.slice(0, 2_048) }
|
||||
settle()
|
||||
publish()
|
||||
})
|
||||
contents.on("dom-ready", settle)
|
||||
contents.on("did-stop-loading", () => {
|
||||
settle()
|
||||
publish()
|
||||
})
|
||||
contents.on("did-navigate-in-page", publish)
|
||||
contents.on("page-title-updated", publish)
|
||||
contents.on("render-process-gone", () => {
|
||||
@@ -252,6 +282,14 @@ export function createBrowserPage(
|
||||
corner.setVisible(false)
|
||||
win.contentView.addChildView(corner)
|
||||
})
|
||||
let visible = false
|
||||
const updateVisibility = () => {
|
||||
// The renderer's layout requests may lag behind navigation; the page decides
|
||||
// whether there is a document worth exposing over the themed background.
|
||||
const show = visible && content
|
||||
view.setVisible(show)
|
||||
corners.forEach((corner) => corner.setVisible(show && !!cornerKey))
|
||||
}
|
||||
const ready = Promise.all([
|
||||
files.ready,
|
||||
...(options.initialize === false
|
||||
@@ -302,9 +340,9 @@ export function createBrowserPage(
|
||||
)
|
||||
})
|
||||
},
|
||||
setVisible(visible: boolean) {
|
||||
view.setVisible(visible)
|
||||
corners.forEach((corner) => corner.setVisible(visible && !!cornerKey))
|
||||
setVisible(value: boolean) {
|
||||
visible = value
|
||||
updateVisibility()
|
||||
},
|
||||
async execute(command: Browser.Command, signal: AbortSignal): Promise<Browser.Result> {
|
||||
await ready
|
||||
|
||||
@@ -31,7 +31,25 @@ async function main() {
|
||||
app.setPath("userData", path.join(root, "electron-data"))
|
||||
app.on("window-all-closed", () => {})
|
||||
await app.whenReady()
|
||||
let unavailable = true
|
||||
let finishLoading: (() => void) | undefined
|
||||
const web = createServer((request, response) => {
|
||||
if (request.url === "/loading") {
|
||||
response.writeHead(200, { "content-type": "text/html" })
|
||||
response.write("<!doctype html><html><body>Loading")
|
||||
finishLoading = () => response.end("</body></html>")
|
||||
return
|
||||
}
|
||||
if (request.url === "/finish-loading") {
|
||||
finishLoading?.()
|
||||
finishLoading = undefined
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
if (request.url === "/unreachable" && unavailable) {
|
||||
request.socket.destroy()
|
||||
return
|
||||
}
|
||||
if (request.url === "/cors-allowed" || request.url === "/cors-denied") {
|
||||
if (request.url === "/cors-allowed") response.setHeader("access-control-allow-origin", "*")
|
||||
response.setHeader("content-type", "text/plain")
|
||||
@@ -204,6 +222,19 @@ async function main() {
|
||||
assert.equal((await call("tabs.list", {})).tabs.length, 2)
|
||||
const tabID = first.id
|
||||
assert.equal(first.url, fixture + "/")
|
||||
assert.equal((await call("navigate", { tabID, url: `${fixture}/unreachable` })).loadError, "502 Bad Gateway")
|
||||
const failed = await until(async () => {
|
||||
const tab = (await call("tabs.list", {})).tabs.find((tab) => tab.id === tabID)
|
||||
return tab?.loadError && !tab.loading ? tab : undefined
|
||||
})
|
||||
assert.equal(failed.url, `${fixture}/unreachable`)
|
||||
assert.equal(failed.loadError, "502 Bad Gateway")
|
||||
await until(async () => inventories.get("suite")?.tabs.find((tab) => tab.id === tabID)?.loadError)
|
||||
assert.equal((await call("tabs.list", {})).tabs.find((tab) => tab.id === second.id)?.loadError, undefined)
|
||||
unavailable = false
|
||||
assert.equal((await call("reload", { tabID })).loadError, undefined)
|
||||
assert.equal((await call("navigate", { tabID, url: fixture })).loadError, undefined)
|
||||
console.log("PASS browser navigation failure state and retry")
|
||||
const networkProof = await call("evaluate", {
|
||||
tabID,
|
||||
script: `(async () => ({
|
||||
|
||||
@@ -29,6 +29,58 @@ export async function verifyTargets(win: BrowserWindow, url: string) {
|
||||
}
|
||||
try {
|
||||
await page.ready
|
||||
const started = new Promise<void>((resolve) => page.contents.once("did-navigate", () => resolve()))
|
||||
page.setVisible(true)
|
||||
const navigation = execute({ type: "navigate", tabID, url: url + "/loading" })
|
||||
await started
|
||||
assert(page.state().loading)
|
||||
assert(win.contentView.children.slice(children).every((view) => !view.getVisible()))
|
||||
page.setVisible(true)
|
||||
assert(win.contentView.children.slice(children).every((view) => !view.getVisible()))
|
||||
const stopped = new Promise<void>((resolve) => page.contents.once("did-stop-loading", () => resolve()))
|
||||
await fetch(url + "/finish-loading")
|
||||
await navigation
|
||||
await stopped
|
||||
assert.equal(page.state().loading, false)
|
||||
assert(page.view.getVisible(), "The requested visibility must return after loading finishes")
|
||||
console.log("PASS native loading from a blank page keeps the themed background visible until the document is ready")
|
||||
const again = new Promise<void>((resolve) => page.contents.once("did-navigate", () => resolve()))
|
||||
const reloading = execute({ type: "navigate", tabID, url: url + "/loading" })
|
||||
await again
|
||||
assert(page.state().loading)
|
||||
assert(page.view.getVisible(), "A shown page must stay visible while the next document loads")
|
||||
await fetch(url + "/finish-loading")
|
||||
await reloading
|
||||
assert(page.view.getVisible())
|
||||
console.log("PASS native loading from a shown page keeps the previous document visible")
|
||||
await execute({ type: "navigate", tabID, url: url + "/missing" })
|
||||
assert.equal(page.state().loadError, undefined, "An HTTP error page from the server is a real document")
|
||||
page.setVisible(true)
|
||||
assert(page.view.getVisible(), "An HTTP error page from the server must stay visible")
|
||||
console.log("PASS native HTTP error pages stay visible")
|
||||
await assert.rejects(execute({ type: "navigate", tabID, url: url + "/unreachable" }), /ERR_EMPTY_RESPONSE/)
|
||||
assert.equal(page.state().loadError, "ERR_EMPTY_RESPONSE")
|
||||
assert.equal(page.state().url, url + "/unreachable")
|
||||
page.setVisible(true)
|
||||
assert(win.contentView.children.slice(children).every((view) => !view.getVisible()))
|
||||
const clearing = new Promise<void>((resolve) => {
|
||||
page.contents.once("did-start-navigation", () => {
|
||||
page.setVisible(true)
|
||||
assert(win.contentView.children.slice(children).every((view) => !view.getVisible()))
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
await execute({ type: "navigate", tabID, url: "about:blank" })
|
||||
await clearing
|
||||
assert.equal(page.state().loadError, undefined)
|
||||
page.setVisible(true)
|
||||
assert(win.contentView.children.slice(children).every((view) => !view.getVisible()))
|
||||
const restored = new Promise<void>((resolve) => page.contents.once("did-stop-loading", () => resolve()))
|
||||
await execute({ type: "navigate", tabID, url })
|
||||
await restored
|
||||
page.setVisible(true)
|
||||
assert(page.view.getVisible())
|
||||
console.log("PASS native blank navigation stays hidden through stale layout updates")
|
||||
for (const color of [
|
||||
[255, 255, 255, 255],
|
||||
[20, 24, 30, 255],
|
||||
@@ -91,6 +143,7 @@ export async function verifyTargets(win: BrowserWindow, url: string) {
|
||||
|
||||
await execute({ type: "evaluate", tabID, script: "document.querySelector('a[href=\"/download\"]').click()" })
|
||||
const fileID = await waitForFile()
|
||||
assert.equal(page.state().loadError, undefined, "A download must not mark the page as unreachable")
|
||||
const fileAction: Browser.Action = { type: "files.get", tabID, fileID }
|
||||
const original = await inspect(fileAction)
|
||||
assert(original.resources.includes(url + "/download"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"name": "@opencode/http-recorder",
|
||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"description": "OpenCode's desktop browser plugin",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -54,6 +54,7 @@ export const Tab = Schema.Struct({
|
||||
url: Schema.String.check(Schema.isMaxLength(16_384)),
|
||||
title: short,
|
||||
loading: Schema.Boolean,
|
||||
loadError: optional(short),
|
||||
canGoBack: Schema.Boolean,
|
||||
canGoForward: Schema.Boolean,
|
||||
generation: count,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -237,25 +237,26 @@ export function fromPromise(plugin: Plugin) {
|
||||
const VcsEndpoints = ClientApi.groups["server.vcs"].endpoints
|
||||
const WebSearchEndpoints = ClientApi.groups["server.websearch"].endpoints
|
||||
const WorktreeEndpoints = ClientApi.groups["server.worktree"].endpoints
|
||||
const context = yield* Effect.context<Scope.Scope>()
|
||||
const runtime = yield* Effect.context<Scope.Scope>()
|
||||
const streams = yield* makeStreams()
|
||||
|
||||
// Run a hook registration on the plugin scope and resolve once it is registered.
|
||||
const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
|
||||
Effect.runPromiseWith(context)(effect).then((registration) => ({
|
||||
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
||||
Effect.runPromiseWith(runtime)(effect).then((registration) => ({
|
||||
dispose: () => Effect.runPromiseWith(runtime)(registration.dispose),
|
||||
}))
|
||||
|
||||
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect)
|
||||
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(runtime)(effect)
|
||||
|
||||
const promiseExecutor =
|
||||
(execute: Tool.Info["execute"]): Info["execute"] =>
|
||||
(input, context) =>
|
||||
run(
|
||||
Effect.runPromiseWith(runtime)(
|
||||
execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.promise(() => context.progress(update)),
|
||||
}),
|
||||
{ signal: context.signal },
|
||||
)
|
||||
|
||||
const adaptApiMethod = <PromiseMethod>(
|
||||
@@ -273,7 +274,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
const result = yield* method(Object.assign({}, ...decoded) as never)
|
||||
if (compiled.noContent) return undefined
|
||||
return yield* compiled.encode(result)
|
||||
}).pipe(Effect.runPromiseWith(context))) as PromiseMethod
|
||||
}).pipe(Effect.runPromiseWith(runtime))) as PromiseMethod
|
||||
}
|
||||
|
||||
const transform =
|
||||
@@ -424,8 +425,8 @@ export function fromPromise(plugin: Plugin) {
|
||||
),
|
||||
reload: () => run(host.integration.reload()),
|
||||
connection: {
|
||||
active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)),
|
||||
resolve: (connection) => Effect.runPromiseWith(context)(host.integration.connection.resolve(connection)),
|
||||
active: (id) => Effect.runPromiseWith(runtime)(host.integration.connection.active(id)),
|
||||
resolve: (connection) => Effect.runPromiseWith(runtime)(host.integration.connection.resolve(connection)),
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
@@ -612,9 +613,10 @@ function attempt<A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) {
|
||||
type RuntimeSchema = Schema.Codec<unknown, unknown>
|
||||
|
||||
const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
|
||||
Effect.promise(() =>
|
||||
Effect.promise((signal) =>
|
||||
tool.execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.runPromise(context.progress(update)),
|
||||
signal,
|
||||
progress: (update) => Effect.runPromise(context.progress(update), { signal }),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Types } from "effect"
|
||||
import type { Hooks, Transform } from "./registration.js"
|
||||
|
||||
export interface ToolContext extends Omit<Tool.Context, "progress"> {
|
||||
readonly signal: AbortSignal
|
||||
readonly progress: (update: Tool.Metadata) => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"name": "@opencode/sdk",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { useConfig } from "../config"
|
||||
import { useSessionTabs } from "../context/session-tabs"
|
||||
import { useData } from "../context/data"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
import { useTheme } from "../context/theme"
|
||||
import {
|
||||
adaptiveSessionTabLayout,
|
||||
@@ -44,6 +45,7 @@ import { SESSION_SIDEBAR_WIDTH, SESSION_TABS_COMPACT_BREAKPOINT } from "../ui/la
|
||||
import { projectName } from "../util/project"
|
||||
import { marqueeCycleWidth, marqueeOverflows, marqueeTextParts } from "../util/marquee"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Keymap } from "../context/keymap"
|
||||
import { registerOpencodeSpinner } from "./register-spinner"
|
||||
@@ -75,7 +77,7 @@ const FADE_WIDTH = 4
|
||||
const ADD_TAB_WIDTH = 3
|
||||
const MARQUEE_DELAY = 600
|
||||
const MARQUEE_INTERVAL = 80
|
||||
const CONTEXT_MENU_WIDTH = 16
|
||||
const CONTEXT_MENU_WIDTH = 20
|
||||
const MIDDLE_MOUSE_BUTTON = 1
|
||||
const RIGHT_MOUSE_BUTTON = 2
|
||||
const MOUSE_CLOSE_HOLD_MS = 5_000
|
||||
@@ -370,6 +372,8 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo
|
||||
const background = () => theme.background.raised.base
|
||||
const actionHovered = () => theme.background.raised.high
|
||||
const dialog = useDialog()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
onCleanup(Keymap.use().mode.push("menu"))
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "menu",
|
||||
@@ -387,6 +391,14 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo
|
||||
run: () =>
|
||||
props.tabs.rename ? props.tabs.rename(sessionID) : DialogSessionRename.show(dialog, sessionID, title),
|
||||
},
|
||||
{
|
||||
title: "Copy session ID",
|
||||
run: () =>
|
||||
void clipboard
|
||||
.write(sessionID)
|
||||
.then(() => toast.show({ message: "Session ID copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error),
|
||||
},
|
||||
{ title: "Close", run: () => props.tabs.close(sessionID) },
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -34,6 +34,7 @@ for (const orientation of ["horizontal", "vertical"] as const) {
|
||||
const [active, setActive] = createSignal("second")
|
||||
const [newTab, setNewTab] = createSignal(false)
|
||||
const settings: Info = { tabs: { enabled: true } }
|
||||
const copied: string[] = []
|
||||
let config!: ReturnType<typeof useConfig>
|
||||
let theme!: ReturnType<typeof useTheme>
|
||||
function Colors() {
|
||||
@@ -61,7 +62,10 @@ for (const orientation of ["horizontal", "vertical"] as const) {
|
||||
} satisfies SessionTabsController
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts paths={{ state: temporary.path }}>
|
||||
<TestTuiContexts
|
||||
paths={{ state: temporary.path }}
|
||||
clipboard={{ read: async () => undefined, write: async (text) => void copied.push(text) }}
|
||||
>
|
||||
<TuiAppProvider value={{ name: "test", version: "test", channel: "test" }}>
|
||||
<StorageProvider>
|
||||
<ConfigProvider
|
||||
@@ -202,10 +206,12 @@ for (const orientation of ["horizontal", "vertical"] as const) {
|
||||
await app.mockMouse.click(column, row, MouseButton.RIGHT)
|
||||
await app.waitForFrame((frame) => frame.includes("Rename"))
|
||||
expect(app.captureCharFrame().split("\n")[row + 1]!.indexOf("Rename")).toBe(column + 1)
|
||||
expect(app.captureCharFrame()).toContain("Copy session ID")
|
||||
expect(app.captureCharFrame()).toContain("Close")
|
||||
expect(app.captureCharFrame()).not.toContain("Keep open")
|
||||
expect(active()).toBe("second")
|
||||
app.mockInput.pressKey("c", { ctrl: true })
|
||||
await app.mockMouse.click(column + 1, row + 2)
|
||||
expect(copied).toEqual(["first"])
|
||||
await app.waitForFrame((frame) => !frame.includes("Rename"))
|
||||
|
||||
setNewTab(true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -553,8 +553,9 @@
|
||||
}
|
||||
|
||||
&[data-value="open-file"],
|
||||
&[data-value^="file://"]
|
||||
{
|
||||
&[data-value^="file://"],
|
||||
&[data-value="browser"],
|
||||
&[data-value^="browser:"] {
|
||||
gap: 8px;
|
||||
padding-inline-end: 4px;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "2.0.10",
|
||||
"version": "2.0.11",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user