diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index e780547fcd01..072d08dd047a 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -114,10 +114,10 @@ ultimate source of truth. - [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`. - [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`. - [x] Logical operators: `&&`, `||`, `??`, and `!`, with short-circuiting. -- [x] Unary `+`, unary `-`, `typeof`, `instanceof`, and own-property-only `in`. +- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof`, and own-property-only `in`. - [x] Prefix and postfix `++` and `--`. - [x] Plain, arithmetic, bitwise, and logical assignment operators. -- [ ] Unary `void` and property deletion, including computed forms such as `delete object[key]`. +- [ ] Property deletion, including computed forms such as `delete object[key]`. ## Promises and tools @@ -172,7 +172,7 @@ ultimate source of truth. - [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked. - [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and cannot be created, read, or written in CodeMode; tool path segments with those names remain supported. -- [ ] `Object.is` for supported data values. +- [x] `Object.is` for supported data values. - [ ] `Object.groupBy`. ## Arrays diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index b9ed33fd6192..a333744ee08f 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1286,6 +1286,7 @@ export class Interpreter { return Effect.map(this.evaluateExpression(argument), (value) => { if (operator === "typeof") return typeofValue(value) if (operator === "!") return !value + if (operator === "void") return undefined if (containsOpaqueReference(value)) { throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") } diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index def400344bc8..a47370e4c87b 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,4 +1,5 @@ import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { containsOpaqueReference } from "../interpreter/references.js" import { isBlockedMember } from "../tool-runtime.js" import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js" import { boundedData, coerceToString } from "./value.js" @@ -44,6 +45,11 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast return Object.entries(requireObject()).map(([key, item]) => [key, item]) case "hasOwn": return Object.hasOwn(requireObject(), String(args[1])) + case "is": + if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) { + throw new InterpreterRuntimeError("Object.is requires data values in CodeMode.", node, "InvalidDataValue") + } + return Object.is(args[0], args[1]) case "assign": { const target = args[0] if (target === null || typeof target !== "object" || Array.isArray(target) || isCodeModeValue(target)) { diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 132b40b12c73..b30123db170d 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -99,6 +99,19 @@ describe("H4: typeof on an undeclared identifier is 'undefined'", () => { }) }) +describe("unary void", () => { + test("evaluates its operand and returns undefined", async () => { + expect(await value(`let count = 0; const result = void (count += 1); return [count, result === undefined]`)).toEqual([ + 1, + true, + ]) + }) + + test("discards opaque values", async () => { + expect(await value(`return void tools === undefined`)).toBe(true) + }) +}) + describe("H1: NaN/Infinity flow as intermediates and normalize to null at the boundary", () => { test("guards run instead of the program crashing on a transient NaN", async () => { expect(await value(`return parseInt("abc") || 0`)).toBe(0) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index ba779f33d7d0..94d8c57b079b 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -593,6 +593,24 @@ describe("Set", () => { }) describe("stdlib integration", () => { + test("Object.is uses SameValue semantics", async () => { + expect( + await value(` + const object = {} + return [ + Object.is(NaN, NaN), + Object.is(0, -0), + Object.is(object, object), + Object.is({}, {}), + ] + `), + ).toEqual([true, false, true, false]) + }) + + test("Object.is rejects opaque runtime references", async () => { + expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue") + }) + test("Object values and entries accept arrays", async () => { expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([ ["a", "b"],