Skip to content

Commit 1452aad

Browse files
authored
feat(codemode): add substr, well-formed, and Date string methods (#48275)
1 parent 1417976 commit 1452aad

6 files changed

Lines changed: 207 additions & 8 deletions

File tree

‎packages/codemode/interpreter-support.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,13 @@ ultimate source of truth.
250250
## Strings
251251

252252
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
253-
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`.
253+
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
254254
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
255-
- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
255+
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
256256
- [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`.
257257
- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
258258
- [x] `localeCompare`; locale and options arguments are currently ignored.
259+
- [x] `isWellFormed` and `toWellFormed`.
259260
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
260261
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
261262
- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like
@@ -316,6 +317,7 @@ ultimate source of truth.
316317
- [x] Local and UTC Date setters, including native argument coercion, mutation, rollover, invalid-Date recovery, and
317318
`TimeClip` behavior.
318319
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
320+
- [x] `toDateString` and `toTimeString` in the host's local timezone.
319321
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
320322
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
321323
representation for the string primitive.

‎packages/codemode/src/interpreter/methods.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,11 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
118118
result = value.trim()
119119
break
120120
case "trimStart":
121+
case "trimLeft":
121122
result = value.trimStart()
122123
break
123124
case "trimEnd":
125+
case "trimRight":
124126
result = value.trimEnd()
125127
break
126128
// Locale/options are deliberately unsupported; comparison uses the host default locale.
@@ -241,6 +243,15 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
241243
case "substring":
242244
result = value.substring(optNum(0) ?? 0, optNum(1))
243245
break
246+
case "substr":
247+
result = value.substr(optNum(0) ?? 0, optNum(1))
248+
break
249+
case "isWellFormed":
250+
result = value.isWellFormed()
251+
break
252+
case "toWellFormed":
253+
result = value.toWellFormed()
254+
break
244255
case "charCodeAt":
245256
result = value.charCodeAt(optNum(0) ?? 0)
246257
break

‎packages/codemode/src/stdlib/date.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const dateMethods = new Set([
2929
"toISOString",
3030
"toJSON",
3131
"toString",
32+
"toDateString",
33+
"toTimeString",
3234
"toUTCString",
3335
"toGMTString",
3436
"getFullYear",
@@ -103,6 +105,10 @@ export const invokeDateMethod = (
103105
return Number.isFinite(value.time) ? hosted.toISOString() : null
104106
case "toString":
105107
return coerceToString(value)
108+
case "toDateString":
109+
return hosted.toDateString()
110+
case "toTimeString":
111+
return hosted.toTimeString()
106112
case "toUTCString":
107113
case "toGMTString":
108114
return hosted.toUTCString()

‎packages/codemode/src/stdlib/string.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ export const stringMethods = new Set([
88
"trim",
99
"trimStart",
1010
"trimEnd",
11+
"trimLeft",
12+
"trimRight",
1113
"split",
1214
"slice",
1315
"substring",
16+
"substr",
1417
"includes",
1518
"startsWith",
1619
"endsWith",
@@ -32,6 +35,8 @@ export const stringMethods = new Set([
3235
"search",
3336
"localeCompare",
3437
"normalize",
38+
"isWellFormed",
39+
"toWellFormed",
3540
])
3641

3742
const codeUnits = (name: string, op: (...codes: Array<number>) => string) =>

‎packages/codemode/test/parity.test.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -491,12 +491,8 @@ describe("CodeMode-specific string behavior", () => {
491491
expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
492492
})
493493

494-
test("does not expose obsolete string aliases", async () => {
495-
expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([
496-
"undefined",
497-
"undefined",
498-
"undefined",
499-
])
494+
test("exposes the Annex B string aliases every engine ships", async () => {
495+
expect(await value(`return [" x ".trimLeft(), " x ".trimRight(), "abc".substr(1, 1)]`)).toEqual(["x ", " x", "b"])
500496
})
501497
})
502498

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
3+
* - test/annexB/built-ins/String/prototype/substr/length-falsey.js
4+
* - test/annexB/built-ins/String/prototype/substr/length-negative.js
5+
* - test/annexB/built-ins/String/prototype/substr/length-positive.js
6+
* - test/annexB/built-ins/String/prototype/substr/length-undef.js
7+
* - test/annexB/built-ins/String/prototype/substr/start-negative.js
8+
* - test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js
9+
* - test/built-ins/String/prototype/isWellFormed/returns-boolean.js
10+
* - test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js
11+
* - test/built-ins/Date/prototype/toDateString/format.js
12+
* - test/built-ins/Date/prototype/toDateString/invalid-date.js
13+
* - test/built-ins/Date/prototype/toDateString/negative-year.js
14+
* - test/built-ins/Date/prototype/toTimeString/format.js
15+
* - test/built-ins/Date/prototype/toTimeString/invalid-date.js
16+
*
17+
* Copyright (C) 2016, 2017 the V8 project authors. All rights reserved.
18+
* Copyright (C) 2018 Richard Gibson. All rights reserved.
19+
* Copyright (C) 2022 Jordan Harband. All rights reserved.
20+
* Test262 portions are governed by the BSD license in LICENSE.test262.
21+
*
22+
* The `typeof String.prototype.method` checks are replaced with `typeof "".method` because
23+
* CodeMode has no prototype objects.
24+
*/
25+
import { describe, expect, test } from "bun:test"
26+
import { Effect } from "effect"
27+
import { CodeMode } from "../src/index.js"
28+
29+
const value = async (code: string) => {
30+
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
31+
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
32+
return result.value
33+
}
34+
35+
describe("String.prototype.substr Test262 parity", () => {
36+
test("test/annexB/built-ins/String/prototype/substr/length-falsey.js", async () => {
37+
expect(
38+
await value(`
39+
return [false, NaN, "", null].flatMap((length) => [0, 1, 2, 3].map((start) => "abc".substr(start, length)))
40+
`),
41+
).toEqual(Array(16).fill(""))
42+
})
43+
44+
test("test/annexB/built-ins/String/prototype/substr/length-negative.js", async () => {
45+
expect(
46+
await value(`
47+
return [0, 1, 2, 3].flatMap((start) => [-1, -2, -3, -4].map((length) => "abc".substr(start, length)))
48+
`),
49+
).toEqual(Array(16).fill(""))
50+
})
51+
52+
test("test/annexB/built-ins/String/prototype/substr/length-positive.js", async () => {
53+
expect(
54+
await value(`
55+
return [0, 1, 2, 3].map((start) => [1, 2, 3, 4].map((length) => "abc".substr(start, length)))
56+
`),
57+
).toEqual([
58+
["a", "ab", "abc", "abc"],
59+
["b", "bc", "bc", "bc"],
60+
["c", "c", "c", "c"],
61+
["", "", "", ""],
62+
])
63+
})
64+
65+
test("test/annexB/built-ins/String/prototype/substr/length-undef.js", async () => {
66+
expect(
67+
await value(`
68+
return [
69+
"abc".substr(0), "abc".substr(1), "abc".substr(2), "abc".substr(3),
70+
"abc".substr(0, undefined), "abc".substr(1, undefined), "abc".substr(2, undefined), "abc".substr(3, undefined),
71+
]
72+
`),
73+
).toEqual(["abc", "bc", "c", "", "abc", "bc", "c", ""])
74+
})
75+
76+
test("test/annexB/built-ins/String/prototype/substr/start-negative.js", async () => {
77+
expect(
78+
await value(`
79+
return ["abc".substr(-1), "abc".substr(-2), "abc".substr(-3), "abc".substr(-4), "abc".substr(-1.1)]
80+
`),
81+
).toEqual(["c", "bc", "abc", "abc", "c"])
82+
})
83+
84+
test("test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js", async () => {
85+
expect(
86+
await value(`
87+
const pair = "\\ud834\\udf06"
88+
return [pair.substr(0), pair.substr(1), pair.substr(2), pair.substr(0, 0), pair.substr(0, 1), pair.substr(0, 2)]
89+
`),
90+
).toEqual(["\ud834\udf06", "\udf06", "", "", "\ud834", "\ud834\udf06"])
91+
})
92+
})
93+
94+
describe("String well-formedness Test262 parity", () => {
95+
test("test/built-ins/String/prototype/isWellFormed/returns-boolean.js", async () => {
96+
expect(
97+
await value(`
98+
const leadingPoo = "\\uD83D"
99+
const trailingPoo = "\\uDCA9"
100+
const wholePoo = leadingPoo + trailingPoo
101+
return [
102+
typeof "".isWellFormed,
103+
("a" + leadingPoo + "c" + leadingPoo + "e").isWellFormed(),
104+
("a" + trailingPoo + "c" + trailingPoo + "e").isWellFormed(),
105+
("a" + trailingPoo + leadingPoo + "d").isWellFormed(),
106+
"a💩c".isWellFormed(),
107+
"a\\uD83D\\uDCA9c".isWellFormed(),
108+
("a" + leadingPoo + trailingPoo + "d").isWellFormed(),
109+
wholePoo.slice(0, 1).isWellFormed(),
110+
wholePoo.slice(1).isWellFormed(),
111+
"abc".isWellFormed(),
112+
"a\\u25A8c".isWellFormed(),
113+
]
114+
`),
115+
).toEqual(["function", false, false, false, true, true, true, false, false, true, true])
116+
})
117+
118+
test("test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js", async () => {
119+
expect(
120+
await value(`
121+
const replacementChar = "\\uFFFD"
122+
const leadingPoo = "\\uD83D"
123+
const trailingPoo = "\\uDCA9"
124+
const wholePoo = leadingPoo + trailingPoo
125+
return [
126+
typeof "".toWellFormed,
127+
("a" + leadingPoo + "c" + leadingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
128+
("a" + trailingPoo + "c" + trailingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
129+
("a" + trailingPoo + leadingPoo + "d").toWellFormed() === "a" + replacementChar + replacementChar + "d",
130+
"a💩c".toWellFormed() === "a💩c",
131+
"a\\uD83D\\uDCA9c".toWellFormed() === "a\\uD83D\\uDCA9c",
132+
("a" + leadingPoo + trailingPoo + "d").toWellFormed() === "a" + wholePoo + "d",
133+
wholePoo.slice(0, 1).toWellFormed() === replacementChar,
134+
wholePoo.slice(1).toWellFormed() === replacementChar,
135+
"abc".toWellFormed() === "abc",
136+
"a\\u25A8c".toWellFormed() === "a\\u25A8c",
137+
]
138+
`),
139+
).toEqual(["function", true, true, true, true, true, true, true, true, true, true])
140+
})
141+
})
142+
143+
describe("Date string formatting Test262 parity", () => {
144+
test("test/built-ins/Date/prototype/toDateString/format.js", async () => {
145+
expect(
146+
await value(`
147+
const dateRegExp = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{2} [0-9]{4}$/
148+
return [dateRegExp.test(new Date(0).toDateString()), dateRegExp.test(new Date("0020-01-01T00:00:00Z").toDateString())]
149+
`),
150+
).toEqual([true, true])
151+
})
152+
153+
test("test/built-ins/Date/prototype/toDateString/invalid-date.js", async () => {
154+
expect(await value(`return new Date(NaN).toDateString()`)).toBe("Invalid Date")
155+
})
156+
157+
test("test/built-ins/Date/prototype/toDateString/negative-year.js", async () => {
158+
expect(
159+
await value(`
160+
return ["-000001", "-000012", "-000123", "-001234", "-012345", "-123456"].map(
161+
(year) => new Date(year + "-07-01T00:00Z").toDateString().split(" ")[3],
162+
)
163+
`),
164+
).toEqual(["-0001", "-0012", "-0123", "-1234", "-12345", "-123456"])
165+
})
166+
167+
test("test/built-ins/Date/prototype/toTimeString/format.js", async () => {
168+
expect(
169+
await value(`
170+
const timeRegExp = /^[0-9]{2}:[0-9]{2}:[0-9]{2} GMT[+-][0-9]{4}( \\(.+\\))?$/
171+
return timeRegExp.test(new Date(0).toTimeString())
172+
`),
173+
).toBe(true)
174+
})
175+
176+
test("test/built-ins/Date/prototype/toTimeString/invalid-date.js", async () => {
177+
expect(await value(`return new Date(NaN).toTimeString()`)).toBe("Invalid Date")
178+
})
179+
})

0 commit comments

Comments
 (0)