Compare commits

...
1 Commits
8 changed files with 66 additions and 70 deletions
+3 -2
View File
@@ -272,8 +272,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]"`, and the class name of
every built-in wrapper), `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`, and
`propertyIsEnumerable`.
## Arrays
+28 -15
View File
@@ -21,6 +21,7 @@ import {
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
type ProgramWrapper,
} from "./interpreter/objects.js"
export const MAX_VALUE_DEPTH = 32
@@ -116,21 +117,8 @@ const copy = (
if (protos !== undefined && mode === "program") {
if (value instanceof ProgramObject) return value
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
if (value instanceof Map) {
const wrapped = new ProgramMap(protos.Map)
for (const [key, item] of value.entries()) wrapped.map.set(next(key), next(item))
return wrapped
}
if (value instanceof Set) {
const wrapped = new ProgramSet(protos.Set)
for (const item of value.values()) wrapped.set.add(next(item))
return wrapped
}
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
if (value instanceof URLSearchParams)
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
const wrapped = wrapHost(protos, value, next)
if (wrapped !== undefined) return wrapped
}
if (value instanceof ProgramDate) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
@@ -222,6 +210,31 @@ const copy = (
return copied
}
/** The built-in wrapper for a host Date, RegExp, Map, Set, URL, or URLSearchParams, copying contents through `next`. */
export const wrapHost = (
protos: Prototypes,
value: object,
next: (item: unknown) => unknown,
): ProgramWrapper | undefined => {
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
if (value instanceof Map) {
const wrapped = new ProgramMap(protos.Map)
for (const [key, item] of value) wrapped.map.set(next(key), next(item))
return wrapped
}
if (value instanceof Set) {
const wrapped = new ProgramSet(protos.Set)
for (const item of value) wrapped.set.add(next(item))
return wrapped
}
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
if (value instanceof URLSearchParams) {
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
}
return undefined
}
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object
// never reaches the Object.prototype setter.
const defineHost = (target: object, key: string, value: unknown): void => {
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { MAX_VALUE_DEPTH } from "../data.js"
import { MAX_VALUE_DEPTH, wrapHost } from "../data.js"
import type { Extension } from "../extension.js"
import { coerceToString } from "../stdlib/value.js"
import type { Host } from "./globals.js"
@@ -109,26 +109,12 @@ export const extensionGlobals = <R>(
handles.set(value, handle)
return handle
}
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
if (value instanceof Error) {
return createErrorValue(protos[isErrorType(value.name) ? value.name : "Error"], value.message)
}
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
if (value instanceof URLSearchParams) {
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
}
const next = (item: unknown) => fromHost(item, label, depth + 1, seen)
if (value instanceof Map) {
const wrapped = new ProgramMap(protos.Map)
for (const [key, item] of value) wrapped.map.set(next(key), next(item))
return wrapped
}
if (value instanceof Set) {
const wrapped = new ProgramSet(protos.Set)
for (const item of value) wrapped.set.add(next(item))
return wrapped
}
const wrapped = wrapHost(protos, value, next)
if (wrapped !== undefined) return wrapped
if (seen.has(value)) throw typeError(`${label} produced a circular value.`)
seen.add(value)
if (Array.isArray(value)) {
+18 -16
View File
@@ -122,7 +122,13 @@ export class ProgramGenerator extends ProgramObject {
}
}
export class ProgramDate extends ProgramObject {
/** A built-in object wrapping a host value; data-like, but never plain data. `kind` is its JS class name. */
export abstract class ProgramWrapper extends ProgramObject {
abstract readonly kind: string
}
export class ProgramDate extends ProgramWrapper {
readonly kind = "Date"
constructor(
proto: ProgramObject,
public time: number,
@@ -131,7 +137,8 @@ export class ProgramDate extends ProgramObject {
}
}
export class ProgramRegExp extends ProgramObject {
export class ProgramRegExp extends ProgramWrapper {
readonly kind = "RegExp"
readonly regex: RegExp
constructor(proto: ProgramObject, pattern: string, flags: string) {
super(proto)
@@ -140,15 +147,18 @@ export class ProgramRegExp extends ProgramObject {
}
}
export class ProgramMap extends ProgramObject {
export class ProgramMap extends ProgramWrapper {
readonly kind = "Map"
readonly map = new Map<unknown, unknown>()
}
export class ProgramSet extends ProgramObject {
export class ProgramSet extends ProgramWrapper {
readonly kind = "Set"
readonly set = new Set<unknown>()
}
export class ProgramURLSearchParams extends ProgramObject {
export class ProgramURLSearchParams extends ProgramWrapper {
readonly kind = "URLSearchParams"
constructor(
proto: ProgramObject,
readonly params: URLSearchParams,
@@ -157,7 +167,8 @@ export class ProgramURLSearchParams extends ProgramObject {
}
}
export class ProgramURL extends ProgramObject {
export class ProgramURL extends ProgramWrapper {
readonly kind = "URL"
readonly searchParams: ProgramURLSearchParams
constructor(
proto: ProgramObject,
@@ -179,16 +190,7 @@ export class ProgramHandle extends ProgramObject {
}
}
/** Built-in objects that wrap a host value; data-like, but never plain data. */
export const isWrapper = (
value: unknown,
): value is ProgramDate | ProgramRegExp | ProgramMap | ProgramSet | ProgramURL | ProgramURLSearchParams =>
value instanceof ProgramDate ||
value instanceof ProgramRegExp ||
value instanceof ProgramMap ||
value instanceof ProgramSet ||
value instanceof ProgramURL ||
value instanceof ProgramURLSearchParams
export const isWrapper = (value: unknown): value is ProgramWrapper => value instanceof ProgramWrapper
const MAX_ARRAY_INDEX = 4_294_967_295
@@ -6,16 +6,11 @@ import {
isWrapper,
ownKeys,
ProgramArray,
ProgramDate,
ProgramGenerator,
ProgramHandle,
ProgramMap,
ProgramObject,
ProgramPromise,
ProgramRegExp,
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
ProgramWrapper,
} from "./objects.js"
/** Values that cannot cross the data boundary. */
@@ -80,12 +75,7 @@ export const describeValue = (value: unknown): string => {
if (value instanceof ProgramArray) return "an array"
if (value instanceof ProgramPromise) return "an un-awaited Promise"
if (value instanceof ToolReference) return "a tool reference"
if (value instanceof ProgramDate) return "a Date"
if (value instanceof ProgramRegExp) return "a RegExp"
if (value instanceof ProgramMap) return "a Map"
if (value instanceof ProgramSet) return "a Set"
if (value instanceof ProgramURL) return "a URL"
if (value instanceof ProgramURLSearchParams) return "a URLSearchParams"
if (value instanceof ProgramWrapper) return `a ${value.kind}`
if (value instanceof ProgramGenerator) return "a generator"
if (value instanceof ProgramHandle) return `a ${value.instance.constructor.name}`
if (isRuntimeReference(value)) return "a function"
+2 -4
View File
@@ -21,11 +21,10 @@ import {
keys,
own,
ProgramArray,
ProgramDate,
ProgramError,
ProgramObject,
ProgramPromise,
ProgramRegExp,
ProgramWrapper,
set,
} from "../interpreter/objects.js"
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
@@ -108,8 +107,7 @@ export const classTag = (value: unknown): string => {
if (value instanceof ProgramArray) return "Array"
if (value instanceof Callable) return "Function"
if (value instanceof ProgramError) return "Error"
if (value instanceof ProgramDate) return "Date"
if (value instanceof ProgramRegExp) return "RegExp"
if (value instanceof ProgramWrapper) return value.kind
if (typeof value === "string") return "String"
if (typeof value === "number") return "Number"
if (typeof value === "boolean") return "Boolean"
+2 -4
View File
@@ -8,11 +8,10 @@ import {
ProgramArray,
ProgramDate,
ProgramError,
ProgramMap,
ProgramRegExp,
ProgramSet,
ProgramURL,
ProgramURLSearchParams,
ProgramWrapper,
} from "../interpreter/objects.js"
import type { Runner } from "../interpreter/runner.js"
@@ -25,10 +24,9 @@ export const coerceToString = (value: unknown): string => {
if (value instanceof ProgramDate)
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
if (value instanceof ProgramRegExp) return `/${value.regex.source}/${value.regex.flags}`
if (value instanceof ProgramMap) return "[object Map]"
if (value instanceof ProgramSet) return "[object Set]"
if (value instanceof ProgramURL) return value.url.href
if (value instanceof ProgramURLSearchParams) return value.params.toString()
if (value instanceof ProgramWrapper) return `[object ${value.kind}]`
if (value instanceof ProgramError) {
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
const name = get(value, "name")
+8
View File
@@ -681,6 +681,14 @@ describe("Map", () => {
})
describe("Set", () => {
test("wrappers report their class through Object.prototype.toString and in diagnostics", async () => {
expect(
await value(`return [new Map().toString(), new Set().toString(), new URLSearchParams().toString()]`),
).toEqual(["[object Map]", "[object Set]", ""])
expect((await error(`Array.from(new URL("https://a.test/"))`)).message).toContain("received a URL")
expect((await error(`Array.from(/x/)`)).message).toContain("received a RegExp")
})
test("add/has/delete/size with chaining", async () => {
expect(
await value(`