stdlib.test.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. /*
  2. * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
  3. * - test/built-ins/Date/value-to-primitive-result-non-string-prim.js
  4. * - test/built-ins/Date/value-to-primitive-result-string.js
  5. *
  6. * CodeMode does not support Symbol.toPrimitive, so these cases exercise the same Date-constructor primitive-result
  7. * handling through supported own valueOf and toString functions.
  8. *
  9. * Copyright (C) 2016 the V8 project authors. All rights reserved.
  10. * Test262 portions are governed by the BSD license in LICENSE.test262.
  11. */
  12. import { describe, expect, test } from "bun:test"
  13. import { Effect, Schema } from "effect"
  14. import { CodeMode, Tool } from "../src/index.js"
  15. // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
  16. // intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
  17. // values, while at the host boundary (final result, tool arguments, JSON.stringify) they
  18. // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
  19. // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
  20. const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
  21. const value = async (code: string) => {
  22. const result = await run(code)
  23. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  24. return result.value
  25. }
  26. const error = async (code: string) => {
  27. const result = await run(code)
  28. if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  29. return result.error
  30. }
  31. describe("Number and Math", () => {
  32. test("Math.random returns a number in [0, 1)", async () => {
  33. expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
  34. })
  35. test("Number exposes native non-finite constants", async () => {
  36. expect(
  37. await value(
  38. `return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
  39. ),
  40. ).toEqual([true, true, true])
  41. })
  42. test("Number valueOf returns its primitive receiver", async () => {
  43. expect(await value(`return (42).valueOf()`)).toBe(42)
  44. })
  45. test("Number valueOf does not enable boxed numbers", async () => {
  46. expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
  47. })
  48. })
  49. describe("Date", () => {
  50. test("Date.now() returns a number", async () => {
  51. expect(await value(`return typeof Date.now()`)).toBe("number")
  52. })
  53. test("epoch construction and ISO rendering", async () => {
  54. expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z")
  55. })
  56. test("string parsing round-trips", async () => {
  57. expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000)
  58. expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000)
  59. })
  60. test("one-argument construction coerces supported values like JavaScript", async () => {
  61. expect(
  62. await value(`return [new Date(true).getTime(), new Date(false).getTime(), new Date(null).getTime()]`),
  63. ).toEqual([1, 0, 0])
  64. expect(await value(`return Number.isNaN(new Date(undefined).getTime())`)).toBe(true)
  65. expect(await value(`return Number.isNaN(new Date([]).getTime())`)).toBe(true)
  66. expect(await value(`return new Date(["1970-01-01T00:00:00.000Z"]).getTime()`)).toBe(0)
  67. expect(await value(`return Number.isNaN(new Date({}).getTime())`)).toBe(true)
  68. })
  69. test("one-argument construction uses valueOf then toString for objects", async () => {
  70. expect(
  71. await value(`
  72. const calls = []
  73. const number = { valueOf: () => 8 }
  74. const text = {
  75. valueOf: () => { calls.push("valueOf"); return {} },
  76. toString: () => { calls.push("toString"); return "2016-06-05T18:40:00.000Z" },
  77. }
  78. return [new Date(number).getTime(), new Date(text).getTime(), calls]
  79. `),
  80. ).toEqual([8, 1465152000000, ["valueOf", "toString"]])
  81. expect(
  82. await value(`
  83. const values = [
  84. { valueOf: () => undefined },
  85. { valueOf: () => true },
  86. { valueOf: () => false },
  87. { valueOf: () => null },
  88. ]
  89. return values.map((item) => new Date(item).getTime())
  90. `),
  91. ).toEqual([null, 1, 0, 0])
  92. expect(
  93. await value(`
  94. try {
  95. new Date({ valueOf: () => ({}), toString: () => ({}) })
  96. } catch (error) {
  97. return error.name
  98. }
  99. `),
  100. ).toBe("TypeError")
  101. })
  102. test("date arithmetic and comparison use the time value", async () => {
  103. expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000)
  104. expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true)
  105. expect(await value(`return +new Date(42)`)).toBe(42)
  106. })
  107. test("UTC getters read calendar components", async () => {
  108. expect(
  109. await value(
  110. `const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`,
  111. ),
  112. ).toEqual([2024, 2, 5, 6, 7, 8, 9])
  113. })
  114. test("invalid dates yield NaN times, guardable in-CodeMode", async () => {
  115. expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
  116. expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
  117. })
  118. test("toISOString on an invalid date throws RangeError", async () => {
  119. expect(await value(`try { new Date("garbage").toISOString() } catch (error) { return error.name }`)).toBe(
  120. "RangeError",
  121. )
  122. })
  123. test("template interpolation renders the ISO form", async () => {
  124. expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
  125. })
  126. test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
  127. expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
  128. expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
  129. when: "1970-01-01T00:00:00.000Z",
  130. tags: ["1970-01-01T00:00:01.000Z"],
  131. })
  132. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  133. })
  134. test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
  135. expect(await value(`return Number(new Date(5))`)).toBe(5)
  136. expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
  137. expect(await value(`return Boolean(new Date(0))`)).toBe(true)
  138. })
  139. test("sorting dates with a numeric comparator", async () => {
  140. expect(
  141. await value(`
  142. const dates = [new Date(3000), new Date(1000), new Date(2000)]
  143. return dates.sort((a, b) => a - b).map((d) => d.getTime())
  144. `),
  145. ).toEqual([1000, 2000, 3000])
  146. })
  147. test("new Date(year, month, day) accepts component form", async () => {
  148. expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
  149. 2024, 0, 2,
  150. ])
  151. })
  152. test("typeof and unknown properties are forgiving", async () => {
  153. expect(await value(`return typeof new Date(0)`)).toBe("object")
  154. expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
  155. })
  156. })
  157. describe("RegExp", () => {
  158. test("literal test", async () => {
  159. expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
  160. expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
  161. })
  162. test("exec exposes captures and index", async () => {
  163. expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
  164. {
  165. full: "abb",
  166. group: "bb",
  167. index: 2,
  168. },
  169. )
  170. expect(await value(`return /a/.exec("zzz")`)).toBeNull()
  171. })
  172. test("named groups read through", async () => {
  173. expect(
  174. await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
  175. ).toBe("ab42")
  176. })
  177. test("global exec advances lastIndex across calls", async () => {
  178. expect(
  179. await value(`
  180. const r = /\\d+/g
  181. const first = r.exec("a1b22c")
  182. const second = r.exec("a1b22c")
  183. return [first[0], second[0]]
  184. `),
  185. ).toEqual(["1", "22"])
  186. })
  187. test("an unmatched string pattern returns null", async () => {
  188. expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
  189. })
  190. test("matchAll materializes match arrays with captures", async () => {
  191. expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
  192. })
  193. test("function replacers receive captures, offsets, input, and named groups", async () => {
  194. expect(
  195. await value(`
  196. const seen = []
  197. const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => {
  198. seen.push([match, first, second === undefined, offset, input])
  199. return Number(match) * 2
  200. })
  201. return { output, seen }
  202. `),
  203. ).toEqual({
  204. output: "a2b44",
  205. seen: [
  206. ["1", "1", true, 1, "a1b22"],
  207. ["22", "2", false, 3, "a1b22"],
  208. ],
  209. })
  210. expect(
  211. await value(`
  212. return "red-blue".replace(
  213. /(?<left>[a-z]+)-(?<right>[a-z]+)/,
  214. (match, left, right, offset, input, groups) => groups.right + ":" + groups.left,
  215. )
  216. `),
  217. ).toBe("blue:red")
  218. })
  219. test("function replacers support string searches, zero-length matches, and result coercion", async () => {
  220. expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na")
  221. expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2")
  222. expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]")
  223. expect(
  224. await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`),
  225. ).toBe("7null[object Object]")
  226. })
  227. test("promise-returning string replacers are coerced synchronously", async () => {
  228. const decorate = Tool.make({
  229. description: "Decorate a string",
  230. input: Schema.String,
  231. output: Schema.String,
  232. run: (input) => Effect.succeed(`[${input}]`),
  233. })
  234. const result = await Effect.runPromise(
  235. CodeMode.execute({
  236. tools: { host: { decorate } },
  237. code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`,
  238. }),
  239. )
  240. expect(result.ok && result.value).toBe("a[object Promise]b[object Promise]")
  241. const missingAwait = await Effect.runPromise(
  242. CodeMode.execute({
  243. tools: { host: { decorate } },
  244. code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
  245. }),
  246. )
  247. expect(missingAwait.ok && missingAwait.value).toBe("a[object Promise]")
  248. })
  249. test("replaceAll without the g flag is a catchable error", async () => {
  250. expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
  251. })
  252. test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
  253. expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
  254. expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
  255. expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
  256. })
  257. test("invalid patterns fail with actionable messages", async () => {
  258. const fromString = await error(`return "abc".match("(")`)
  259. expect(fromString.message).toContain('String.match received the string "("')
  260. expect(fromString.message).toContain("escape them with a backslash")
  261. const fromConstructor = await error(`return new RegExp("(")`)
  262. expect(fromConstructor.message).toContain('new RegExp(...) received "("')
  263. expect(fromConstructor.message).toContain("escape them with a backslash")
  264. const fromFlags = await error(`return new RegExp("a", "xz")`)
  265. expect(fromFlags.message).toContain('invalid flags "xz"')
  266. expect(fromFlags.message).toContain("Valid flags are")
  267. })
  268. test("missing g-flag errors say how to fix the call", async () => {
  269. expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
  270. expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
  271. })
  272. test("a non-pattern argument names the expected shapes", async () => {
  273. const err = await error(`return "abc".match(42)`)
  274. expect(err.message).toContain("expects a regular expression")
  275. expect(err.message).toContain("not number")
  276. })
  277. test("source and flags properties read through", async () => {
  278. expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
  279. source: "ab",
  280. flags: "gi",
  281. global: true,
  282. })
  283. })
  284. test("regexes serialize to {} at the boundary, like JSON", async () => {
  285. expect(await value(`return /a/`)).toEqual({})
  286. expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
  287. })
  288. test("template interpolation renders the literal form", async () => {
  289. expect(await value("return `${/ab/g}`")).toBe("/ab/g")
  290. })
  291. })
  292. describe("URL and URI helpers", () => {
  293. test("encodes and decodes complete URIs and URI components", async () => {
  294. expect(
  295. await value(`
  296. return [
  297. encodeURI("https://example.test/a b?q=a/b"),
  298. encodeURIComponent("a b/c?"),
  299. decodeURI("https://example.test/a%20b?q=a/b"),
  300. decodeURIComponent("a%20b%2Fc%3F"),
  301. ["a b", "c/d"].map(encodeURIComponent),
  302. ]
  303. `),
  304. ).toEqual([
  305. "https://example.test/a%20b?q=a/b",
  306. "a%20b%2Fc%3F",
  307. "https://example.test/a b?q=a/b",
  308. "a b/c?",
  309. ["a%20b", "c%2Fd"],
  310. ])
  311. expect(
  312. await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`),
  313. ).toBe(true)
  314. })
  315. test("resolves and mutates URLs with linked search parameters", async () => {
  316. expect(
  317. await value(`
  318. const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/")
  319. url.pathname = "/items/a b"
  320. url.searchParams.set("id", "a b")
  321. url.searchParams.append("tag", "x/y")
  322. url.hash = "part 1"
  323. return {
  324. href: url.href,
  325. origin: url.origin,
  326. host: url.host,
  327. pathname: url.pathname,
  328. search: url.search,
  329. id: url.searchParams.get("id"),
  330. string: String(url),
  331. json: url.toJSON(),
  332. instances: [
  333. url instanceof URL,
  334. url.searchParams instanceof URLSearchParams,
  335. url.searchParams === url.searchParams,
  336. ],
  337. }
  338. `),
  339. ).toEqual({
  340. href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  341. origin: "https://example.com:8443",
  342. host: "example.com:8443",
  343. pathname: "/items/a%20b",
  344. search: "?id=a+b&tag=x%2Fy",
  345. id: "a b",
  346. string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  347. json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  348. instances: [true, true, true],
  349. })
  350. })
  351. test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => {
  352. expect(
  353. await value(`
  354. const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]])
  355. const seen = []
  356. params.forEach((value, key) => seen.push(key + "=" + value))
  357. params.delete("tag", "b")
  358. params.append("tag", "c")
  359. params.sort()
  360. return {
  361. text: params.toString(),
  362. size: params.size,
  363. tags: params.getAll("tag"),
  364. has: params.has("tag", "c"),
  365. entries: Array.from(params),
  366. object: Object.fromEntries(params),
  367. record: new URLSearchParams({ page: 2, filter: "open" }).toString(),
  368. seen,
  369. }
  370. `),
  371. ).toEqual({
  372. text: "q=a+b&tag=a&tag=c",
  373. size: 3,
  374. tags: ["a", "c"],
  375. has: true,
  376. entries: [
  377. ["q", "a b"],
  378. ["tag", "a"],
  379. ["tag", "c"],
  380. ],
  381. object: { q: "a b", tag: "c" },
  382. record: "page=2&filter=open",
  383. seen: ["tag=b", "tag=a", "q=a b"],
  384. })
  385. })
  386. test("URL parsing failures are catchable and values use native JSON forms", async () => {
  387. expect(
  388. await value(`
  389. const parsed = URL.parse("/users", "https://example.test/api/")
  390. let invalidIsTypeError = false
  391. try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError }
  392. return {
  393. canParse: URL.canParse("/users", "https://example.test/api/"),
  394. cannotParse: URL.canParse("not relative without a base"),
  395. parsed: parsed.href,
  396. invalidIsTypeError,
  397. boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")],
  398. json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }),
  399. }
  400. `),
  401. ).toEqual({
  402. canParse: true,
  403. cannotParse: false,
  404. parsed: "https://example.test/users",
  405. invalidIsTypeError: true,
  406. boundary: ["https://example.test/a", {}],
  407. json: '{"url":"https://example.test/a","params":{}}',
  408. })
  409. })
  410. test("distinguishes omitted URL arguments from explicit undefined", async () => {
  411. expect(
  412. await value(`
  413. function throwsTypeError(run) {
  414. try { run(); return false } catch (error) { return error instanceof TypeError }
  415. }
  416. const params = new URLSearchParams()
  417. const required = [
  418. () => params.append(),
  419. () => params.delete(),
  420. () => params.get(),
  421. () => params.getAll(),
  422. () => params.has(),
  423. () => params.set(),
  424. () => params.forEach(),
  425. ].map(throwsTypeError)
  426. params.append(undefined, undefined)
  427. return {
  428. construct: throwsTypeError(() => new URL()),
  429. canParse: throwsTypeError(() => URL.canParse()),
  430. parse: throwsTypeError(() => URL.parse()),
  431. explicitUndefined: new URL(undefined, "https://example.test/base/").href,
  432. params: params.toString(),
  433. required,
  434. }
  435. `),
  436. ).toEqual({
  437. construct: true,
  438. canParse: true,
  439. parse: true,
  440. explicitUndefined: "https://example.test/base/undefined",
  441. params: "undefined=undefined",
  442. required: [true, true, true, true, true, true, true],
  443. })
  444. })
  445. })
  446. describe("Map", () => {
  447. test("get/set/has/size with chaining", async () => {
  448. expect(
  449. await value(`
  450. const m = new Map()
  451. m.set("a", 1).set("b", 2)
  452. return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
  453. `),
  454. ).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
  455. })
  456. test("object keys use identity", async () => {
  457. expect(
  458. await value(`
  459. const key = { id: 1 }
  460. const m = new Map()
  461. m.set(key, "hit")
  462. return [m.get(key), m.get({ id: 1 }) === undefined]
  463. `),
  464. ).toEqual(["hit", true])
  465. })
  466. test("construction from entry pairs and another Map", async () => {
  467. expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
  468. expect(
  469. await value(
  470. `const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`,
  471. ),
  472. ).toEqual([1, 2, false])
  473. expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
  474. expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
  475. })
  476. test("keys/values/entries return arrays", async () => {
  477. expect(
  478. await value(`
  479. const m = new Map([["a", 1], ["b", 2]])
  480. return { keys: m.keys(), values: m.values(), entries: m.entries() }
  481. `),
  482. ).toEqual({
  483. keys: ["a", "b"],
  484. values: [1, 2],
  485. entries: [
  486. ["a", 1],
  487. ["b", 2],
  488. ],
  489. })
  490. })
  491. test("Object.fromEntries(map) and Array.from(map)", async () => {
  492. expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
  493. expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
  494. })
  495. test("for...of iterates [key, value] pairs with destructuring", async () => {
  496. expect(
  497. await value(`
  498. const m = new Map([["a", 1], ["b", 2]])
  499. let total = 0
  500. let names = ""
  501. for (const [key, count] of m) { names += key; total += count }
  502. return names + total
  503. `),
  504. ).toBe("ab3")
  505. })
  506. test("spread produces entry pairs", async () => {
  507. expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
  508. })
  509. test("forEach passes (value, key)", async () => {
  510. expect(
  511. await value(`
  512. const m = new Map([["a", 1], ["b", 2]])
  513. const seen = []
  514. m.forEach((count, key) => seen.push(key + count))
  515. return seen
  516. `),
  517. ).toEqual(["a1", "b2"])
  518. })
  519. test("delete and clear", async () => {
  520. expect(
  521. await value(`
  522. const m = new Map([["a", 1], ["b", 2]])
  523. const removed = m.delete("a")
  524. const missed = m.delete("zz")
  525. const sizeAfterDelete = m.size
  526. m.clear()
  527. return [removed, missed, sizeAfterDelete, m.size]
  528. `),
  529. ).toEqual([true, false, 1, 0])
  530. })
  531. test("counting idiom: grouped tallies", async () => {
  532. expect(
  533. await value(`
  534. const words = ["a", "b", "a", "c", "a"]
  535. const counts = new Map()
  536. for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
  537. return Object.fromEntries(counts)
  538. `),
  539. ).toEqual({ a: 3, b: 1, c: 1 })
  540. })
  541. test("maps serialize to {} at the boundary, like JSON", async () => {
  542. expect(await value(`return new Map([["a", 1]])`)).toEqual({})
  543. expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
  544. })
  545. test("console.log renders map contents for debugging", async () => {
  546. const result = await run(`console.log(new Map([["a", 1]])); return null`)
  547. expect(result.ok).toBe(true)
  548. expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
  549. })
  550. })
  551. describe("Set", () => {
  552. test("add/has/delete/size with chaining", async () => {
  553. expect(
  554. await value(`
  555. const s = new Set()
  556. s.add(1).add(2).add(1)
  557. const removed = s.delete(2)
  558. return [s.size, s.has(1), s.has(2), removed]
  559. `),
  560. ).toEqual([1, true, false, true])
  561. })
  562. test("dedupe idiom: [...new Set(items)]", async () => {
  563. expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
  564. })
  565. test("construction from strings and other Sets", async () => {
  566. expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
  567. expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
  568. })
  569. test("SameValueZero: NaN is findable", async () => {
  570. expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
  571. })
  572. test("for...of iterates values", async () => {
  573. expect(
  574. await value(`
  575. let total = 0
  576. for (const n of new Set([1, 2, 3])) total += n
  577. return total
  578. `),
  579. ).toBe(6)
  580. })
  581. test("sets serialize to {} at the boundary, like JSON", async () => {
  582. expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} })
  583. })
  584. })
  585. describe("stdlib integration", () => {
  586. test("Object.is uses SameValue semantics", async () => {
  587. expect(
  588. await value(`
  589. const object = {}
  590. return [
  591. Object.is(NaN, NaN),
  592. Object.is(0, -0),
  593. Object.is(object, object),
  594. Object.is({}, {}),
  595. ]
  596. `),
  597. ).toEqual([true, false, true, false])
  598. })
  599. test("Object.is rejects opaque runtime references", async () => {
  600. expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue")
  601. })
  602. test("Object values and entries accept arrays", async () => {
  603. expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
  604. ["a", "b"],
  605. [
  606. ["0", "a"],
  607. ["1", "b"],
  608. ],
  609. ])
  610. expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
  611. ["a", 1],
  612. [
  613. ["0", "a"],
  614. ["index", 1],
  615. ],
  616. ])
  617. expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"])
  618. })
  619. test("Object.fromEntries accepts every supported entry collection", async () => {
  620. expect(
  621. await value(`
  622. return [
  623. Object.fromEntries([["a", 1]]),
  624. Object.fromEntries(new Map([["b", 2]])),
  625. Object.fromEntries(new Set([["c", 3]])),
  626. Object.fromEntries(new URLSearchParams("d=4")),
  627. Object.fromEntries([{ 0: "e", 1: 5 }]),
  628. Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])),
  629. ]
  630. `),
  631. ).toEqual([
  632. { a: 1 },
  633. { b: 2 },
  634. { c: 3 },
  635. { d: "4" },
  636. { e: 5 },
  637. { "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 },
  638. ])
  639. expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe(
  640. true,
  641. )
  642. expect(
  643. await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`),
  644. ).toBe(true)
  645. })
  646. test("deterministic Math methods match the host runtime", async () => {
  647. const result = await value(`
  648. return [
  649. Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5),
  650. Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5),
  651. Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3),
  652. ]
  653. `)
  654. expect(result).toEqual([
  655. Math.acos(0.5),
  656. Math.acosh(2),
  657. Math.asin(0.5),
  658. Math.asinh(2),
  659. Math.atan(1),
  660. Math.atan2(1, 2),
  661. Math.atanh(0.5),
  662. Math.cos(0.5),
  663. Math.cosh(0.5),
  664. Math.sin(0.5),
  665. Math.sinh(0.5),
  666. Math.tan(0.5),
  667. Math.tanh(0.5),
  668. Math.log1p(0.5),
  669. Math.expm1(0.5),
  670. Math.f16round(1.337),
  671. Math.fround(1.337),
  672. Math.clz32(1),
  673. Math.imul(2, 3),
  674. ])
  675. })
  676. test("Object.assign mutates and returns its target", async () => {
  677. expect(
  678. await value(`
  679. const target = { a: 1 }
  680. const result = Object.assign(target, { b: 2 })
  681. return { target, result, same: target === result }
  682. `),
  683. ).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true })
  684. expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
  685. })
  686. test("assignment resolves and reads its left side before evaluating the right side", async () => {
  687. expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
  688. expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
  689. expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([
  690. [11, 20],
  691. 1,
  692. ])
  693. })
  694. test("typeof reports constructors as functions and never throws", async () => {
  695. expect(await value(`return typeof Map`)).toBe("function")
  696. expect(await value(`return typeof ((x) => x)`)).toBe("function")
  697. expect(await value(`return typeof Math`)).toBe("object")
  698. expect(await value(`return typeof tools`)).toBe("object")
  699. })
  700. test("negation works on any value", async () => {
  701. expect(await value(`return !new Map()`)).toBe(false)
  702. expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
  703. })
  704. test("object spread of CodeMode values is a no-op, like JS", async () => {
  705. expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
  706. })
  707. test("dates inside Map values survive in-CodeMode reads", async () => {
  708. expect(
  709. await value(`
  710. const m = new Map([["start", new Date(1000)]])
  711. return m.get("start").getTime()
  712. `),
  713. ).toBe(1000)
  714. })
  715. test("instanceof recognizes the stdlib value types", async () => {
  716. expect(
  717. await value(
  718. `return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
  719. ),
  720. ).toEqual([true, true, true, true])
  721. expect(
  722. await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
  723. ).toEqual([true, true, true, false])
  724. expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
  725. expect(
  726. await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
  727. ).toBe(true)
  728. })
  729. test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
  730. expect(
  731. await value(`
  732. const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]'
  733. const rows = JSON.parse(raw)
  734. const tags = new Set()
  735. const byDay = new Map()
  736. for (const row of rows) {
  737. for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
  738. const day = new Date(row.at).toISOString().slice(0, 10)
  739. byDay.set(day, (byDay.get(day) ?? 0) + 1)
  740. }
  741. return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
  742. `),
  743. ).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
  744. })
  745. })
  746. describe("CodeMode values at intra-CodeMode checkpoints", () => {
  747. test("Object.values/entries keep Dates usable", async () => {
  748. expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
  749. expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
  750. "d:0",
  751. )
  752. })
  753. test("Object.values/entries preserve nested object identity", async () => {
  754. expect(
  755. await value(`
  756. const child = { selected: false }
  757. const rows = { a: child }
  758. Object.values(rows)[0].selected = true
  759. return child.selected
  760. `),
  761. ).toBe(true)
  762. expect(
  763. await value(`
  764. const child = { selected: false }
  765. const rows = { a: child }
  766. Object.entries(rows)[0][1].selected = true
  767. return child.selected
  768. `),
  769. ).toBe(true)
  770. })
  771. test("Object enumeration preserves promises and callable references", async () => {
  772. expect(
  773. await value(`
  774. const pending = Promise.resolve(1)
  775. const source = { pending }
  776. return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
  777. `),
  778. ).toEqual([["pending"], true, 1, 1])
  779. expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
  780. })
  781. test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
  782. const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
  783. expect(diagnostic.kind).toBe("InvalidDataValue")
  784. expect(diagnostic.message).toContain("await")
  785. expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
  786. })
  787. test("Object.assign keeps Maps usable", async () => {
  788. expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
  789. 1,
  790. )
  791. })
  792. test("object and array spread keep CodeMode values usable", async () => {
  793. expect(
  794. await value(`
  795. const src = { m: new Map([["a", 1]]) }
  796. const copy = { ...src }
  797. copy.m.set("b", 2)
  798. return [copy.m.get("a"), src.m.get("b")]
  799. `),
  800. ).toEqual([1, 2])
  801. expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
  802. })
  803. test("Array.from over arrays keeps nested CodeMode values usable", async () => {
  804. expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
  805. })
  806. test("Array.from and Array.of preserve nested object identity", async () => {
  807. expect(
  808. await value(`
  809. const child = { selected: false }
  810. Array.from([child])[0].selected = true
  811. return child.selected
  812. `),
  813. ).toBe(true)
  814. expect(
  815. await value(`
  816. const child = { selected: false }
  817. Array.of(child)[0].selected = true
  818. return child.selected
  819. `),
  820. ).toBe(true)
  821. })
  822. test("Array.from and Array.of preserve promises and callable references", async () => {
  823. expect(
  824. await value(`
  825. const pending = Promise.resolve(1)
  826. return [await Array.from([pending])[0], await Array.of(pending)[0]]
  827. `),
  828. ).toEqual([1, 1])
  829. expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
  830. })
  831. test("Array.from preserves identity across supported collection shapes", async () => {
  832. expect(
  833. await value(`
  834. const child = { selected: false }
  835. const fromArrayLike = Array.from({ 0: child, length: 1 })
  836. const fromMap = Array.from(new Map([["child", child]]))
  837. const fromSet = Array.from(new Set([child]))
  838. fromArrayLike[0].selected = true
  839. return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
  840. `),
  841. ).toEqual([true, true, true])
  842. })
  843. test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
  844. const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
  845. expect(diagnostic.kind).toBe("InvalidDataValue")
  846. expect(diagnostic.message).toContain("await")
  847. expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
  848. })
  849. test("regexes stay callable through Object.values", async () => {
  850. expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
  851. })
  852. test("Object.* helpers see CodeMode values as empty objects, never internals", async () => {
  853. expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
  854. expect(await value(`return Object.values(new Date(0))`)).toEqual([])
  855. expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
  856. expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
  857. expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
  858. })
  859. test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
  860. expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
  861. d: "1970-01-01T00:00:00.000Z",
  862. m: {},
  863. })
  864. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  865. const observed: Array<unknown> = []
  866. const capture = Tool.make({
  867. description: "Capture the exact input the host receives",
  868. input: { type: "object" },
  869. run: (input) =>
  870. Effect.sync(() => {
  871. observed.push(input)
  872. return "ok"
  873. }),
  874. })
  875. const result = await Effect.runPromise(
  876. CodeMode.execute({
  877. tools: { host: { capture } },
  878. code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
  879. }),
  880. )
  881. expect(result.ok).toBe(true)
  882. expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
  883. })
  884. })