Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/codemode/interpreter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ ultimate source of truth.
- [x] Materialized iteration helpers: `keys`, `values`, and `entries` return arrays rather than iterators.
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
- [ ] `Array.prototype.toSpliced`.
- [x] `Array.prototype.toSpliced`.
- [x] Canonical array/string index parsing: keys such as `"01"` remain non-index properties rather than aliasing index
`1`; arbitrary array-property assignment remains unsupported.
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
Expand Down Expand Up @@ -266,7 +266,7 @@ ultimate source of truth.
- [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`.
- [x] Date values serialize to ISO strings; invalid dates serialize to `null`.
- [ ] Date setters.
- [ ] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
- [ ] Native Date loose-equality and default primitive-coercion semantics.
- [x] Native `RangeError` branding for invalid `toISOString()` calls.
Expand All @@ -278,7 +278,7 @@ ultimate source of truth.
- [x] Readable `source`, `flags`, `lastIndex`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, and `dotAll`.
- [x] Captures, safe named groups (blocked member names are omitted), match `.index`, and stateful global matching.
- [x] Integration with supported String methods, including function replacers.
- [ ] Writable `lastIndex`.
- [x] Writable `lastIndex`.
- [ ] `hasIndices`, match `indices`, and `unicodeSets` metadata for the `d` and `v` flags.
- [ ] `RegExp.escape`.

Expand Down
13 changes: 13 additions & 0 deletions packages/codemode/src/interpreter/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,19 @@ const invokeArrayMethod = <R>(
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node)
return Effect.succeed(target.splice(start, deleteCount, ...inserted))
}
case "toSpliced": {
if (args.length === 0) return Effect.succeed([...target])
const start = optNumber(args[0], "start") ?? 0
if (args.length === 1) {
const copied = [...target]
copied.splice(start)
return Effect.succeed(copied)
}
const deleteCount = optNumber(args[1], "delete count") ?? 0
const copied = [...target]
copied.splice(start, deleteCount, ...args.slice(2))
return Effect.succeed(copied)
}
case "fill": {
rejectCircularInsertion(target, args[0], "Array.fill result", node)
return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
Expand Down
4 changes: 2 additions & 2 deletions packages/codemode/src/interpreter/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SafeObject } from "../tool-runtime.js"
import type { CodeModePromise, CodeModeURL } from "../values.js"
import type { CodeModePromise, CodeModeRegExp, CodeModeURL } from "../values.js"

export type SourcePosition = {
line: number
Expand Down Expand Up @@ -35,7 +35,7 @@ export type StatementResult =
| { kind: "continue" }

export type MemberReference = {
target: SafeObject | Array<unknown> | CodeModeURL
target: SafeObject | Array<unknown> | CodeModeRegExp | CodeModeURL
key: string | number
}

Expand Down
23 changes: 18 additions & 5 deletions packages/codemode/src/interpreter/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,7 @@ export class Interpreter<R> {
return new ComputedValue(undefined)
}
if (objectValue instanceof CodeModeRegExp) {
if (key === "lastIndex") return { target: objectValue, key }
if (typeof key === "string" && regexpProperties.has(key)) {
return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key])
}
Expand Down Expand Up @@ -2052,6 +2053,7 @@ export class Interpreter<R> {
if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key)
return reference.target[reference.key]
}
if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex
if (reference.target instanceof CodeModeURL) {
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
}
Expand Down Expand Up @@ -2082,6 +2084,9 @@ export class Interpreter<R> {
) {
throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", target, "InvalidDataValue")
}
if (reference.target instanceof CodeModeRegExp) {
return Reflect.deleteProperty(reference.target.regex, reference.key)
}
return Reflect.deleteProperty(reference.target, reference.key)
})
}
Expand Down Expand Up @@ -2114,16 +2119,20 @@ export class Interpreter<R> {
}
}
const key = Array.isArray(reference.target) ? reference.key : String(reference.key)
const current =
reference.target instanceof CodeModeURL
? (reference.target.url as unknown as Record<string, unknown>)[key]
: (reference.target as Record<PropertyKey, unknown>)[key]
const { write, next, result } = yield* compute(current)
const { write, next, result } = yield* compute(self.readReferenceValue(reference, key))
if (write) self.assignToReference(reference, key, next, node)
return result
})
}

private readReferenceValue(reference: MemberReference, key: number | string): unknown {
if (reference.target instanceof CodeModeURL) {
return (reference.target.url as unknown as Record<string, unknown>)[key]
}
if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex
return (reference.target as Record<PropertyKey, unknown>)[key]
}

private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void {
if (Array.isArray(reference.target)) {
const target = reference.target
Expand Down Expand Up @@ -2152,6 +2161,10 @@ export class Interpreter<R> {
throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError")
}
}
if (reference.target instanceof CodeModeRegExp) {
reference.target.lastIndex = next
return
}
const target = reference.target as SafeObject
const objectKey = key as string
rejectCircularInsertion(target, next, "Object assignment result", node)
Expand Down
1 change: 1 addition & 0 deletions packages/codemode/src/stdlib/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const arrayMethods = new Set([
"shift",
"unshift",
"splice",
"toSpliced",
"fill",
"copyWithin",
"keys",
Expand Down
5 changes: 5 additions & 0 deletions packages/codemode/src/stdlib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const dateMethods = new Set([
"toISOString",
"toJSON",
"toString",
"toUTCString",
"toGMTString",
"getFullYear",
"getMonth",
"getDate",
Expand Down Expand Up @@ -51,6 +53,9 @@ export const invokeDateMethod = (value: CodeModeDate, name: string, node: AstNod
return Number.isFinite(value.time) ? hosted.toISOString() : null
case "toString":
return coerceToString(value)
case "toUTCString":
case "toGMTString":
return hosted.toUTCString()
case "getFullYear":
return hosted.getFullYear()
case "getMonth":
Expand Down
21 changes: 18 additions & 3 deletions packages/codemode/src/stdlib/regexp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ export const invokeRegExpMethod = (
): unknown => {
switch (name) {
case "test":
return value.regex.test(coerceToString(args[0]))
case "exec": {
const matched = value.regex.exec(coerceToString(args[0]))
const input = coerceToString(args[0])
const lastIndex = value.lastIndex
const stateful = value.regex.global || value.regex.sticky
value.regex.lastIndex = toLength(lastIndex)
if (name === "test") {
const matched = value.regex.test(input)
if (!stateful) value.lastIndex = lastIndex
return matched
}
const matched = value.regex.exec(input)
if (!stateful) value.lastIndex = lastIndex
return matched === null ? null : matchToValue(matched)
}
case "toString":
Expand All @@ -70,7 +79,13 @@ export const invokeRegExpMethod = (
throw new InterpreterRuntimeError(`RegExp method '${name}' is not available in CodeMode.`, node)
}
}

const toLength = (value: unknown): number => {
const number = coerceToNumber(value)
if (Number.isNaN(number) || number <= 0) return 0
return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER)
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
import { CodeModeRegExp } from "../values.js"
import { coerceToString } from "./value.js"
import { coerceToNumber, coerceToString } from "./value.js"
8 changes: 8 additions & 0 deletions packages/codemode/src/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export class CodeModeRegExp {
constructor(pattern: string, flags: string) {
this.regex = new RegExp(pattern, flags)
}

get lastIndex(): unknown {
return Reflect.get(this.regex, "lastIndex")
}

set lastIndex(value: unknown) {
Reflect.set(this.regex, "lastIndex", value)
}
}

export class CodeModeMap {
Expand Down
32 changes: 32 additions & 0 deletions packages/codemode/test/array-core-test262.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
* - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js
* - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js
* - test/built-ins/Array/prototype/splice/called_with_one_argument.js
* - test/built-ins/Array/prototype/toSpliced/holes-not-preserved.js
* - test/built-ins/Array/prototype/toSpliced/immutable.js
* - test/built-ins/Array/prototype/toSpliced/start-and-deleteCount-undefineds.js
* - test/built-ins/Array/prototype/toSpliced/start-and-deleteCount-missing.js
* - test/built-ins/Array/prototype/toSpliced/start-undefined-and-deleteCount-missing.js
* - test/built-ins/Array/prototype/fill/fill-values.js
* - test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js
* - test/built-ins/Array/prototype/fill/return-this.js
Expand Down Expand Up @@ -59,6 +64,8 @@
* Copyright 2015 Microsoft Corporation. All rights reserved.
* Copyright 2016 The V8 project authors. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
* The toSpliced hole case omits the source test's inherited Array.prototype element because CodeMode does not expose
* prototype mutation; it retains the source test's hole-densification assertions.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
Expand Down Expand Up @@ -227,6 +234,31 @@ const cases = [
code: `const input = ["first", "second", "third"]; const removed = input.splice(1); return [input, removed]`,
expected: [["first"], ["second", "third"]],
},
{
path: "test/built-ins/Array/prototype/toSpliced/immutable.js",
code: `const input = [2, 0, 1]; const inserted = input.toSpliced(0, 0, -1); const replaced = input.toSpliced(0, 1, -1); return [input, inserted, replaced, inserted !== input, replaced !== input]`,
expected: [[2, 0, 1], [-1, 2, 0, 1], [-1, 0, 1], true, true],
},
{
path: "test/built-ins/Array/prototype/toSpliced/start-and-deleteCount-missing.js",
code: `const input = ["first", "second", "third"]; const result = input.toSpliced(); return [result, result !== input]`,
expected: [["first", "second", "third"], true],
},
{
path: "test/built-ins/Array/prototype/toSpliced/start-undefined-and-deleteCount-missing.js",
code: `return ["first", "second", "third"].toSpliced(undefined)`,
expected: [],
},
{
path: "test/built-ins/Array/prototype/toSpliced/start-and-deleteCount-undefineds.js",
code: `const input = ["first", "second", "third"]; const result = input.toSpliced(undefined, undefined); return [result, result !== input]`,
expected: [["first", "second", "third"], true],
},
{
path: "test/built-ins/Array/prototype/toSpliced/holes-not-preserved.js",
code: `const input = [0, , 2, , 4]; const result = input.toSpliced(0, 0, -1); return [result, 2 in result, 4 in result]`,
expected: [[-1, 0, null, 2, null, 4], true, true],
},
{
path: "test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js",
code: `const input = [0, 0, 0, 0, 0]; input.fill(8, -3, 4); const sparse = []; sparse[4] = 0; sparse.fill(8, 1, 3); return [[0, 0, 0].fill(8, 1, 2), input, [0, 0, 0, 0, 0].fill(8, -2, -1), [0, 0, 0, 0, 0].fill(8, -1, -3), [0 in sparse, sparse[1], sparse[2], 3 in sparse, sparse[4]]]`,
Expand Down
2 changes: 1 addition & 1 deletion packages/codemode/test/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("H3: array property access reads as undefined (not a throw)", () => {
})

test("unknown property reads stay undefined for methods CodeMode does not implement", async () => {
expect(await value(`return [1,2,3].toSpliced === undefined`)).toBe(true)
expect(await value(`return [1,2,3].unknownMethod === undefined`)).toBe(true)
})

test("array indexing still works", async () => {
Expand Down
71 changes: 70 additions & 1 deletion packages/codemode/test/stdlib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/Date/value-to-primitive-result-non-string-prim.js
* - test/built-ins/Date/value-to-primitive-result-string.js
* - test/built-ins/Date/prototype/toUTCString/format.js
* - test/built-ins/Date/prototype/toUTCString/invalid-date.js
* - test/built-ins/RegExp/prototype/exec/S15.10.6.2_A4_T8.js
*
* CodeMode does not support Symbol.toPrimitive, so these cases exercise the same Date-constructor primitive-result
* CodeMode does not support Symbol.toPrimitive, so the Date-constructor cases exercise the same primitive-result
* handling through supported own valueOf and toString functions.
*
* Copyright (C) 2016 the V8 project authors. All rights reserved.
* Copyright (C) 2017 the V8 project authors. All rights reserved.
* Copyright 2009 the Sputnik authors. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
Expand Down Expand Up @@ -175,6 +180,25 @@ describe("Date", () => {
expect(await value(`return typeof new Date(0)`)).toBe("object")
expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
})

test("toUTCString and toGMTString use the native UTC format", async () => {
expect(
await value(`
const date = new Date(0)
return [
date.toUTCString(),
date.toGMTString(),
new Date(NaN).toUTCString(),
new Date("0020-01-01T00:00:00Z").toUTCString(),
]
`),
).toEqual([
"Thu, 01 Jan 1970 00:00:00 GMT",
"Thu, 01 Jan 1970 00:00:00 GMT",
"Invalid Date",
"Wed, 01 Jan 0020 00:00:00 GMT",
])
})
})

describe("RegExp", () => {
Expand Down Expand Up @@ -211,6 +235,51 @@ describe("RegExp", () => {
).toEqual(["1", "22"])
})

test("lastIndex is writable and exec coerces its stored value", async () => {
expect(
await value(`
const pattern = /(?:ab|cd)\\d?/g
pattern.lastIndex = "12"
const stored = [pattern.lastIndex, typeof pattern.lastIndex]
const match = pattern.exec("aacd2233ab12nm444ab42")
pattern.lastIndex = 0
return [stored, match[0], match.index, pattern.lastIndex, delete pattern.lastIndex]
`),
).toEqual([["12", "string"], "ab4", 17, 0, false])
})

test("exec coerces CodeMode data objects assigned to lastIndex", async () => {
expect(
await value(`
const pattern = /a/g
pattern.lastIndex = {}
const stored = pattern.lastIndex
const match = pattern.exec("ba")
pattern.lastIndex = 10
const missed = pattern.exec("a")
return [stored, match.index, pattern.lastIndex, missed]
`),
).toEqual([{}, 1, 0, null])
})

test("non-global exec and test coerce and preserve lastIndex", async () => {
expect(
await value(`
const execPattern = /a/
const execIndex = {}
execPattern.lastIndex = execIndex
const match = execPattern.exec("ba")

const testPattern = /a/
const testIndex = {}
testPattern.lastIndex = testIndex
const matched = testPattern.test("ba")

return [match.index, execPattern.lastIndex === execIndex, matched, testPattern.lastIndex === testIndex]
`),
).toEqual([1, true, true, true])
})

test("an unmatched string pattern returns null", async () => {
expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
})
Expand Down
Loading